auto-sync: 2026-05-05 08:10:01
This commit is contained in:
@@ -24,7 +24,22 @@
|
||||
|
||||
### 3.1 Цветовая палитра — две темы
|
||||
|
||||
Тема переключается кнопкой ☀️/🌙 в search bar. Автодетект: если время 07:00–20:00 → светлая, иначе → тёмная.
|
||||
Тема переключается кнопкой ☀️/🌙 в search bar. **Три режима:**
|
||||
|
||||
| Режим | Описание |
|
||||
|-------|----------|
|
||||
| **Авто** (по умолчанию) | Тема определяется по реальному восходу/закату солнца для текущей геолокации. Если браузер отдал геопозицию — используем её; иначе — запасной город (Москва, 55.75°N). День = светлая, ночь = тёмная. Переключение происходит без перезагрузки. |
|
||||
| **Светлая** | Принудительно светлая тема, независимо от времени суток. |
|
||||
| **Тёмная** | Принудительно тёмная тема. |
|
||||
|
||||
**Реализация авто-режима:**
|
||||
- Используется SunCalc (MIT, ~3KB) через CDN: `https://unpkg.com/suncalc@latest`
|
||||
- `SunCalc.getTimes(date, lat, lng)` → `sunrise`, `sunset`
|
||||
- Текущее время между `sunrise` и `sunset` → светлая тема; иначе — тёмная
|
||||
- При изменении геолокации — пересчёт восхода/заката
|
||||
- Проверка раз в минуту (на случай, если сессия длительная)
|
||||
|
||||
**UI переключателя:** тап на ☀️/🌙 циклически переключает: Авто → Светлая → Тёмная → Авто. Текущий режим показан маленькой подписью под иконкой (или tooltip на десктопе): «Авто», «День», «Ночь». В авто-режиме иконка динамическая: ☀️ если сейчас день, 🌙 если ночь.
|
||||
|
||||
**Тёмная (ночная езда):**
|
||||
```
|
||||
@@ -489,7 +504,11 @@ Font: system-ui, -apple-system, 'SF Pro Display', 'Segoe UI', sans-serif
|
||||
|
||||
| ID | Сценарий | Устройство | Шаги | Ожидаемый результат |
|
||||
|----|----------|-----------|------|---------------------|
|
||||
| T13 | Тёмная тема | Любое | Открыть | Фон #0D1117, текст светлый, нет белых вспышек |
|
||||
| T13 | Тема — авто по умолчанию | Любое | Открыть (днём) | Фон светлый (бежевый #F5F5F0), иконка ☀️, подпись «Авто» |
|
||||
| T13a | Тема — авто ночью | Любое | Открыть (после заката) | Фон тёмный #0D1117, иконка 🌙, подпись «Авто» |
|
||||
| T13b | Тема — ручной цикл | Любое | Тап ☀️/🌙 3 раза | Авто → Светлая → Тёмная → Авто, при каждом тапе тема и подпись меняются |
|
||||
| T13c | Тема — ручная не зависит от времени | Любое | Переключить на «Светлая» ночью | Фон бежевый, подпись «День», иконка ☀️ |
|
||||
| T13d | Тема — авто пересчитывает при геолокации | Любое | В авто-режиме, разрешить геолокацию | Восход/закат пересчитаны по реальным координатам, тема соответствует |
|
||||
| T14 | Толстые пальцы в перчатках | iPhone | Нажимать кнопки | Все кнопки min 48×48px, не промахиваешься |
|
||||
| T15 | Landscape поворот | iPhone | Повернуть телефон | UI перестроился, карта видна |
|
||||
| T16 | Десктоп Chrome | MacBook | Открыть | Боковая панель 320px, кнопки слева |
|
||||
@@ -523,7 +542,8 @@ Font: system-ui, -apple-system, 'SF Pro Display', 'Segoe UI', sans-serif
|
||||
- [ ] Все P0 + P1 тест-кейсы прошли на iPhone 14 Pro (393px)
|
||||
- [ ] Bottom sheet плавно открывается/закрывается
|
||||
- [ ] Toolbar: все 6 режимов, min 48px tap target
|
||||
- [ ] Тёмная тема везде, нет белых фонов
|
||||
- [ ] Тема: авто (по восходу/закату SunCalc) + ручной переключатель (3 режима: Авто/Светлая/Тёмная)
|
||||
- [ ] Тёмная тема везде, нет белых вспышек; светлая тема без ослепляющего белого
|
||||
- [ ] SVG иконки (Lucide), никаких emoji в UI-элементах
|
||||
- [ ] Карта видна при активных панелях
|
||||
- [ ] Safe area корректная (notch, home indicator)
|
||||
@@ -537,6 +557,7 @@ Font: system-ui, -apple-system, 'SF Pro Display', 'Segoe UI', sans-serif
|
||||
- MapLibre GL остаётся
|
||||
- Без новых npm-зависимостей (только inline CSS/JS)
|
||||
- Lucide иконки — подключить через CDN: `https://unpkg.com/lucide@latest`
|
||||
- SunCalc — подключить через CDN: `https://unpkg.com/suncalc@latest` (MIT, ~3KB, для расчёта восхода/заката)
|
||||
- Деплой через ssh2 (стандартная схема)
|
||||
|
||||
---
|
||||
|
||||
310
tasks/enduro-trails/DEV_TASK_PHASE5.md
Normal file
310
tasks/enduro-trails/DEV_TASK_PHASE5.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Dev Task: Enduro Trails — Фаза 5 «Редизайн»
|
||||
|
||||
**Приоритет:** HIGH
|
||||
**Проект:** enduro-trails
|
||||
**BRD:** `BRD_PHASE5.md`
|
||||
**Дата:** 2026-05-05
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Фазы 3–4 реализованы: роутинг A→B, альтернативы, Разведка, Связка, Красивый маршрут, Линейка, Метки.
|
||||
UI уже частично обновлён (bottom sheets, toolbar, SVG, тёмная тема), но BRD Phase 5 требует **довести до продакшн-качества** — мобильный first, две темы с авто-режимом по восходу/закату, адаптив, анимации.
|
||||
|
||||
**⚠️ Бэкенд (app.py) НЕ ТРОГАТЬ.** Только фронтенд: `index.html`, `app.css`, `app.js`.
|
||||
|
||||
---
|
||||
|
||||
## Деплой
|
||||
|
||||
SSH бинарник не работает в контейнере (glibc 2.36 < 2.38). Используй Node.js ssh2:
|
||||
```js
|
||||
const { Client } = require('/tmp/node_modules/ssh2');
|
||||
```
|
||||
Шаблон: `/tmp/deploy_app2.js`.
|
||||
Статику заливаем через SFTP в `/home/slin/enduro-trails/prototype/static/`, потом `docker cp` в контейнер + `docker restart`.
|
||||
|
||||
---
|
||||
|
||||
## Что уже есть vs что нужно
|
||||
|
||||
| Компонент | Сейчас (Phase 4) | Надо (Phase 5) |
|
||||
|-----------|-----------------|----------------|
|
||||
| Тема | Только тёмная, кнопка ☀️/🌙 но не работает | Три режима: Авто (SunCalc восход/закат), Светлая, Тёмная |
|
||||
| Цвета | CSS vars есть, но только тёмные | Два набора CSS vars, переключение через `body.theme-dark`/`body.theme-light` |
|
||||
| Кнопка темы | Есть `#btn-theme` в search-bar | Циклическое переключение Авто→Светлая→Тёмная→Авто, подпись режима |
|
||||
| Search bar | Есть, `position:fixed` | Остается, но цвета под тему |
|
||||
| Bottom toolbar | 6 кнопок, SVG, `#toolbar` | Остается, но: активная кнопка = оранжевый фон (не только цвет текста), label появляется под иконкой |
|
||||
| Bottom sheets | 4 sheets (route/recon/scenic/link) | Остаются, но: цвета под тему, skeleton loading, свайп вниз для закрытия |
|
||||
| Map buttons | `#map-controls-r` (2 кнопки) | Остаются, цвета под тему |
|
||||
| Адаптив | Mobile only, без десктопа | Десктоп (≥768px): боковая панель 320px, кнопки слева |
|
||||
| Анимации | Есть sheet slide | + skeleton loading, + кнопка scale(0.94) при tap, + маркер scale появление |
|
||||
| Иконки | SVG inline (lucide-style) | ОК, остаются. **НЕ emoji** |
|
||||
| Карта style | Один style.json | Переключение dark/light style при смене темы |
|
||||
|
||||
---
|
||||
|
||||
## Задача 1: Дизайн-система — Цвета и Тема
|
||||
|
||||
### 1.1 CSS Custom Properties — две темы
|
||||
|
||||
На `:root` определить ВСЕ переменные для тёмной (по умолчанию). На `body.theme-light` — переопределить.
|
||||
|
||||
**Тёмная (default):**
|
||||
```css
|
||||
:root {
|
||||
--bg: #0D1117;
|
||||
--surface: #161B22;
|
||||
--surface2: #21262D;
|
||||
--border: #30363D;
|
||||
--text: #E6EDF3;
|
||||
--text2: #8B949E;
|
||||
--text3: #484F58;
|
||||
--accent: #FF6B00;
|
||||
--accent-hover:#FF8A33;
|
||||
--gold: #FFD700;
|
||||
--red: #FF3B1F;
|
||||
--success: #2EA043;
|
||||
--shadow: rgba(0,0,0,0.5);
|
||||
--overlay: rgba(0,0,0,0.6);
|
||||
}
|
||||
```
|
||||
|
||||
**Светлая:**
|
||||
```css
|
||||
body.theme-light {
|
||||
--bg: #F5F5F0;
|
||||
--surface: #FFFFFF;
|
||||
--surface2: #F0F0EA;
|
||||
--border: #D0CFC8;
|
||||
--text: #1A1A1A;
|
||||
--text2: #6B6B6B;
|
||||
--text3: #9A9A9A;
|
||||
--accent: #E55A00;
|
||||
--accent-hover:#CC4F00;
|
||||
--gold: #C89B00;
|
||||
--red: #CC2200;
|
||||
--success: #1A7A30;
|
||||
--shadow: rgba(0,0,0,0.15);
|
||||
--overlay: rgba(0,0,0,0.3);
|
||||
}
|
||||
```
|
||||
|
||||
**Все компоненты** (search-bar, toolbar, bottom-sheet, map-btn, cards, pills, ruler-info, marker-dialog, search-results) должны использовать ТОЛЬКО CSS vars — никаких хардкоженных цветов.
|
||||
|
||||
### 1.2 Переключатель темы — три режима
|
||||
|
||||
**Состояния:**
|
||||
- `themeMode = 'auto'` — по умолчанию, тема определяется по восходу/закату
|
||||
- `themeMode = 'light'` — принудительно светлая
|
||||
- `themeMode = 'dark'` — принудительно тёмная
|
||||
|
||||
**Циклическое переключение:** тап на `#btn-theme` → `auto → light → dark → auto`
|
||||
|
||||
**UI кнопки:**
|
||||
- В авто-режиме: иконка динамическая (☀️ если день, 🌙 если ночь)
|
||||
- В ручном светлом: всегда ☀️
|
||||
- В ручном тёмном: всегда 🌙
|
||||
- Подпись под иконкой (или рядом, маленький текст): «Авто» / «День» / «Ночь»
|
||||
|
||||
**Сохранение:** `localStorage.setItem('enduro-theme-mode', themeMode)` — при загрузке читать оттуда.
|
||||
|
||||
### 1.3 SunCalc — авто-режим по восходу/закату
|
||||
|
||||
Подключить в `index.html`:
|
||||
```html
|
||||
<script src="https://unpkg.com/suncalc@1.9.0/suncalc.min.js"></script>
|
||||
```
|
||||
(до `app.js`)
|
||||
|
||||
**Логика:**
|
||||
```js
|
||||
function applyAutoTheme() {
|
||||
if (themeMode !== 'auto') return;
|
||||
const now = new Date();
|
||||
// Геолокация: если есть — используем, иначе Москва
|
||||
const lat = userLat || 55.75;
|
||||
const lon = userLon || 37.62;
|
||||
const times = SunCalc.getTimes(now, lat, lon);
|
||||
const isDay = now >= times.sunrise && now < times.sunset;
|
||||
document.body.className = isDay ? 'theme-light' : 'theme-dark';
|
||||
updateThemeButtonIcon();
|
||||
}
|
||||
|
||||
// Проверять раз в минуту
|
||||
setInterval(applyAutoTheme, 60000);
|
||||
```
|
||||
|
||||
**При получении геолокации** (кнопка 🎯): обновить `userLat`/`userLon` и пересчитать тему.
|
||||
|
||||
**При переключении карты:** MapLibre.setStyle() на соответствующий style.json (тёмный/светлый).
|
||||
|
||||
### 1.4 Карта — стиль под тему
|
||||
|
||||
Уже есть два style.json: тёмный и светлый. При смене темы:
|
||||
```js
|
||||
map.setStyle(isDark ? 'style-dark.json' : 'style-light.json');
|
||||
```
|
||||
Или если стили инлайн — переключить `map.setStyle()` с нужным объектом.
|
||||
|
||||
**⚠️ Важно:** после `setStyle` нужно пересоздать все слои (routes, markers, circles). Использовать событие `map.on('style.load', ...)`.
|
||||
|
||||
---
|
||||
|
||||
## Задача 2: Компоненты — все на CSS vars
|
||||
|
||||
### 2.1 Search bar
|
||||
- Фон: `var(--surface)`, бордер: `var(--border)`, тень с `var(--shadow)`
|
||||
- Placeholder: `var(--text3)`
|
||||
- Кнопка темы: иконка + подпись
|
||||
|
||||
### 2.2 Toolbar
|
||||
- Фон: `var(--surface)`, бордер: `var(--border)`
|
||||
- Неактивная кнопка: `var(--text2)` цвет
|
||||
- **Активная кнопка:** `var(--accent)` фон, белый цвет текста/иконки, `border-radius: 10px`
|
||||
- Активная кнопка: подпись под иконкой видна
|
||||
|
||||
### 2.3 Bottom sheets
|
||||
- Фон: `var(--surface)`, бордер: `var(--border)`
|
||||
- Handle: `var(--border)` цвет
|
||||
- Заголовок: `var(--text)`, подзаголовок/label: `var(--text2)`
|
||||
- Close button: `var(--text2)` → hover `var(--text)`
|
||||
- Waypoint chips: `var(--surface2)` фон, `var(--border)` бордер
|
||||
- Route cards: `var(--surface2)` фон, `var(--border)` бордер
|
||||
- Stat pills: `var(--bg)` фон, `var(--border)` бордер
|
||||
- Hint text: `var(--text3)`
|
||||
|
||||
### 2.4 Map buttons
|
||||
- Фон: `var(--surface)`, бордер: `var(--border)`, тень с `var(--shadow)`
|
||||
- Active: `var(--accent)` фон
|
||||
|
||||
### 2.5 Search results
|
||||
- Фон: `var(--surface)`, бордер: `var(--border)`
|
||||
- Hover: `var(--surface2)`
|
||||
|
||||
### 2.6 Marker dialog
|
||||
- Фон: `var(--surface)`, внутренности `var(--surface2)`, бордер `var(--border)`
|
||||
|
||||
---
|
||||
|
||||
## Задача 3: Skeleton Loading
|
||||
|
||||
При загрузке маршрутов/разведки показывать skeleton вместо «Строю маршрут...»:
|
||||
|
||||
```css
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--surface2) 25%, var(--border) 50%, var(--surface2) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
height: 64px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
```
|
||||
|
||||
Использовать в `#route-cards`, `#scenic-cards`, `#link-cards` пока идёт запрос к API.
|
||||
|
||||
---
|
||||
|
||||
## Задача 4: Свайп вниз для закрытия sheet
|
||||
|
||||
**Touch-обработка:**
|
||||
```js
|
||||
// На каждом .bottom-sheet
|
||||
let startY = 0;
|
||||
sheet.addEventListener('touchstart', e => {
|
||||
if (e.target.closest('.sheet-handle') || e.touches[0].clientY < sheet.getBoundingClientRect().top + 40) {
|
||||
startY = e.touches[0].clientY;
|
||||
}
|
||||
});
|
||||
sheet.addEventListener('touchmove', e => {
|
||||
const dy = e.touches[0].clientY - startY;
|
||||
if (dy > 0) sheet.style.transform = `translateY(${dy}px)`;
|
||||
});
|
||||
sheet.addEventListener('touchend', e => {
|
||||
const dy = e.changedTouches[0].clientY - startY;
|
||||
if (dy > 80) closeSheet(sheet.id);
|
||||
else sheet.style.transform = '';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Задача 5: Адаптив — Десктоп
|
||||
|
||||
При `width >= 768px`:
|
||||
|
||||
```css
|
||||
@media (min-width: 768px) {
|
||||
#toolbar {
|
||||
position: fixed;
|
||||
bottom: auto; left: 0; top: 60px;
|
||||
width: 72px; height: auto;
|
||||
flex-direction: column;
|
||||
border-top: none;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.bottom-sheet {
|
||||
left: 72px;
|
||||
max-width: 400px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
#map-controls-r {
|
||||
left: 72px;
|
||||
}
|
||||
#search-bar {
|
||||
left: 84px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Задача 6: Микро-анимации
|
||||
|
||||
1. **Кнопки toolbar** при tap: `transform: scale(0.94)`, 100ms
|
||||
2. **Маркеры на карте** при появлении: `transform: scale(0) → scale(1)`, 200ms (MapLibre marker animation)
|
||||
3. **Карточки** при появлении: `opacity: 0 → 1`, 200ms stagger
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации
|
||||
|
||||
1. **Задача 1** — Дизайн-система: CSS vars для двух тем + SunCalc + переключатель
|
||||
2. **Задача 2** — Все компоненты на CSS vars (замена хардкода)
|
||||
3. **Задача 3** — Skeleton loading
|
||||
4. **Задача 4** — Свайп для закрытия sheets
|
||||
5. **Задача 5** — Десктоп-адаптив
|
||||
6. **Задача 6** — Микро-анимации
|
||||
7. Деплой + проверка
|
||||
|
||||
---
|
||||
|
||||
## Проверка после деплоя
|
||||
|
||||
1. Открыть приложение → проверить тему (если день — светлая, если ночь — тёмная, подпись «Авто»)
|
||||
2. Тапнуть кнопку темы 3 раза → Авто→Светлая→Тёмная→Авто
|
||||
3. В светлом режиме — бежевый фон, тёмный текст, нет белых вспышек
|
||||
4. В тёмном режиме — тёмный фон, светлый текст
|
||||
5. Открыть Маршрут → sheet, карточки, всё в цветах текущей темы
|
||||
6. Переключить тему с открытым sheet → цвета обновились
|
||||
7. Десктоп (широкий экран) → toolbar слева вертикально, sheet с max-width
|
||||
8. Skeleton при построении маршрута
|
||||
9. Свайп вниз по sheet → закрывается
|
||||
10. Геолокация → тема пересчитывается по реальным координатам
|
||||
|
||||
---
|
||||
|
||||
## Технические ограничения
|
||||
|
||||
- **Бэкенд (app.py) НЕ ТРОГАТЬ** — только `index.html`, `app.css`, `app.js`
|
||||
- MapLibre GL остаётся
|
||||
- Без npm-зависимостей (inline CSS/JS)
|
||||
- CDN: `https://unpkg.com/suncalc@1.9.0/suncalc.min.js` (добавить в index.html)
|
||||
- CDN: `https://unpkg.com/maplibre-gl@4.7.0/...` (уже есть)
|
||||
- Деплой через ssh2 (шаблон `/tmp/deploy_app2.js`)
|
||||
@@ -1,13 +1,11 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Enduro Trails — Design System v5.0
|
||||
Dark (night ride) + Light (day ride) themes
|
||||
Mobile-first, thumb-friendly, adventure style
|
||||
Phase 5: Dual themes, skeleton, swipe, desktop, animations
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Reset ────────────────────────────────────── */
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
|
||||
/* ── CSS Variables — Dark Theme (default) ───── */
|
||||
/* ── Dark Theme (default) ───── */
|
||||
body.theme-dark {
|
||||
--bg: #0D1117;
|
||||
--surface: #161B22;
|
||||
@@ -28,9 +26,10 @@ body.theme-dark {
|
||||
--success: #2EA043;
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.6);
|
||||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.4);
|
||||
--overlay: rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* ── CSS Variables — Light Theme ────────────── */
|
||||
/* ── Light Theme ────────────── */
|
||||
body.theme-light {
|
||||
--bg: #F0EFE8;
|
||||
--surface: #FFFFFF;
|
||||
@@ -51,6 +50,7 @@ body.theme-light {
|
||||
--success: #1A6B2A;
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.1);
|
||||
--overlay: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* ── Base ─────────────────────────────────────── */
|
||||
@@ -62,64 +62,51 @@ html, body {
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
/* ── Map Container ────────────────────────────── */
|
||||
#map {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
#map { position: fixed; inset: 0; z-index: 0; }
|
||||
|
||||
/* ── Search Bar ──────────────────────────────── */
|
||||
#search-bar {
|
||||
position: fixed;
|
||||
top: max(env(safe-area-inset-top, 0px), 12px);
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
left: 12px; right: 12px;
|
||||
height: 50px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex; align-items: center;
|
||||
padding: 0 6px 0 14px;
|
||||
gap: 8px;
|
||||
z-index: 300;
|
||||
gap: 8px; z-index: 300;
|
||||
box-shadow: var(--shadow);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
#search-bar:focus-within {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
#search-bar .sb-icon {
|
||||
color: var(--text3);
|
||||
flex-shrink: 0;
|
||||
width: 18px; height: 18px;
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
#search-bar:focus-within { border-color: var(--accent); }
|
||||
#search-bar .sb-icon { color: var(--text3); flex-shrink: 0; width: 18px; height: 18px; }
|
||||
#search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
flex: 1; background: none; border: none;
|
||||
color: var(--text); font-size: 15px; outline: none; min-width: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
#search-input::placeholder { color: var(--text3); }
|
||||
#btn-theme {
|
||||
width: 38px; height: 38px;
|
||||
width: auto; min-width: 38px; height: 38px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
gap: 3px; cursor: pointer; flex-shrink: 0;
|
||||
transition: all 0.15s; padding: 0 8px;
|
||||
}
|
||||
#btn-theme:active { transform: scale(0.9); background: var(--surface3); }
|
||||
#btn-theme:active { transform: scale(0.94); background: var(--surface3); }
|
||||
#btn-theme svg { width: 16px; height: 16px; }
|
||||
#theme-label {
|
||||
font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--text3); line-height: 1;
|
||||
}
|
||||
|
||||
/* Search results */
|
||||
#search-results {
|
||||
@@ -128,11 +115,10 @@ html, body {
|
||||
left: 12px; right: 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
z-index: 300;
|
||||
border-radius: 14px; z-index: 300;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
overflow: hidden; display: none;
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
.search-result-item {
|
||||
padding: 12px 16px;
|
||||
@@ -143,19 +129,16 @@ html, body {
|
||||
}
|
||||
.search-result-item:last-child { border-bottom: none; }
|
||||
.search-result-item:active { background: var(--surface2); }
|
||||
.search-result-item:hover { background: var(--surface2); }
|
||||
.sri-icon { color: var(--text3); flex-shrink: 0; }
|
||||
.sri-name { font-size: 14px; font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sri-sub { font-size: 12px; color: var(--text2); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* ── Map Control Buttons (right side) ─────────── */
|
||||
/* ── Map Control Buttons ──────────────────────── */
|
||||
#map-controls-r {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
position: fixed; right: 12px;
|
||||
bottom: calc(80px + env(safe-area-inset-bottom, 0px) + 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 200;
|
||||
display: flex; flex-direction: column; gap: 8px; z-index: 200;
|
||||
}
|
||||
.map-btn {
|
||||
width: 48px; height: 48px;
|
||||
@@ -164,565 +147,139 @@ html, body {
|
||||
border-radius: 12px;
|
||||
color: var(--text2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer; box-shadow: var(--shadow-sm);
|
||||
transition: all 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
.map-btn svg { width: 20px; height: 20px; }
|
||||
.map-btn:active { transform: scale(0.9); background: var(--surface2); }
|
||||
.map-btn:active { transform: scale(0.94); background: var(--surface2); }
|
||||
.map-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
|
||||
/* ── Bottom Toolbar ───────────────────────────── */
|
||||
#toolbar {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
position: fixed; bottom: 0; left: 0; right: 0;
|
||||
height: calc(68px + env(safe-area-inset-bottom, 0px));
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
display: flex; align-items: center; justify-content: space-around;
|
||||
z-index: 300;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.2);
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
.tb-btn {
|
||||
flex: 1;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: 3px;
|
||||
height: 56px;
|
||||
gap: 3px; height: 56px;
|
||||
border: none; background: none;
|
||||
color: var(--text3);
|
||||
font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.06em;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
border-radius: 10px; cursor: pointer;
|
||||
transition: color 0.15s, background 0.15s, transform 0.1s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tb-btn svg { width: 22px; height: 22px; margin-bottom: 1px; }
|
||||
.tb-btn:active { background: var(--surface2); }
|
||||
.tb-btn.active { color: var(--accent); }
|
||||
.tb-btn.active svg { stroke: var(--accent); }
|
||||
.tb-btn svg { width: 22px; height: 22px; margin-bottom: 1px; transition: transform 0.1s; }
|
||||
.tb-btn:active { background: var(--surface2); transform: scale(0.94); }
|
||||
.tb-btn.active {
|
||||
color: #fff; background: var(--accent); border-radius: 10px;
|
||||
}
|
||||
.tb-btn.active svg { stroke: #fff; }
|
||||
.tb-btn span { line-height: 1; }
|
||||
|
||||
/* ── Bottom Sheet ─────────────────────────────── */
|
||||
.bottom-sheet {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
position: fixed; bottom: 0; left: 0; right: 0;
|
||||
background: var(--surface);
|
||||
border-radius: 20px 20px 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
z-index: 400;
|
||||
max-height: 78vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
z-index: 400; max-height: 78vh;
|
||||
overflow-y: auto; overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.32, 0, 0.15, 1);
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.bottom-sheet.open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.bottom-sheet.open { transform: translateY(0); }
|
||||
.bottom-sheet.swiping { transition: none; }
|
||||
.sheet-handle {
|
||||
width: 36px; height: 4px;
|
||||
background: var(--border2);
|
||||
border-radius: 2px;
|
||||
margin: 12px auto 0;
|
||||
cursor: grab;
|
||||
border-radius: 2px; margin: 12px auto 0; cursor: grab;
|
||||
}
|
||||
.sheet-header {
|
||||
display: flex; align-items: center;
|
||||
padding: 14px 16px 12px;
|
||||
gap: 10px;
|
||||
padding: 14px 16px 12px; gap: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sheet-header svg { width: 20px; height: 20px; stroke: var(--accent); flex-shrink: 0; }
|
||||
.sheet-header h2 {
|
||||
flex: 1;
|
||||
font-size: 15px; font-weight: 700;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.sheet-header h2 { flex: 1; font-size: 15px; font-weight: 700; color: var(--text); letter-spacing: 0.02em; }
|
||||
.sheet-close {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
width: 32px; height: 32px; border-radius: 8px;
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer; flex-shrink: 0; transition: all 0.15s;
|
||||
}
|
||||
.sheet-close svg { width: 16px; height: 16px; }
|
||||
.sheet-close:active { background: var(--surface3); }
|
||||
.sheet-close:active { background: var(--surface3); color: var(--text); }
|
||||
.sheet-body { padding: 14px 16px; }
|
||||
.sheet-hint {
|
||||
font-size: 13px; color: var(--text2);
|
||||
text-align: center; padding: 16px 0 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.sheet-hint { font-size: 13px; color: var(--text2); text-align: center; padding: 16px 0 8px; line-height: 1.5; }
|
||||
|
||||
/* Sheet backdrop */
|
||||
#sheet-backdrop {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
z-index: 390;
|
||||
opacity: 0; pointer-events: none;
|
||||
background: var(--overlay);
|
||||
z-index: 390; opacity: 0; pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
#sheet-backdrop.visible {
|
||||
opacity: 1; pointer-events: auto;
|
||||
}
|
||||
#sheet-backdrop.visible { opacity: 1; pointer-events: auto; }
|
||||
|
||||
/* ── Section Label ────────────────────────────── */
|
||||
.section-label {
|
||||
font-size: 10px; font-weight: 800;
|
||||
color: var(--text3);
|
||||
text-transform: uppercase; letter-spacing: 0.12em;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.section-label { font-size: 10px; font-weight: 800; color: var(--text3); text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 8px; margin-top: 4px; }
|
||||
|
||||
/* ── Waypoints Row ────────────────────────────── */
|
||||
.waypoints-row {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
overflow-x: auto; padding: 0 0 4px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.waypoints-row { display: flex; align-items: center; gap: 4px; overflow-x: auto; padding: 0 0 4px; scrollbar-width: none; }
|
||||
.waypoints-row::-webkit-scrollbar { display: none; }
|
||||
.wp-chip {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 7px 10px;
|
||||
flex-shrink: 0;
|
||||
max-width: 140px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.wp-chip { display: flex; align-items: center; gap: 6px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 7px 10px; flex-shrink: 0; max-width: 140px; cursor: pointer; transition: border-color 0.15s; }
|
||||
.wp-chip:active { border-color: var(--accent); }
|
||||
.wp-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wp-label {
|
||||
font-size: 12px; font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.wp-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.wp-label { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wp-arrow { color: var(--text3); font-size: 18px; flex-shrink: 0; padding: 0 1px; }
|
||||
.wp-add {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
background: none;
|
||||
border: 1.5px dashed var(--border2);
|
||||
border-radius: 10px;
|
||||
padding: 7px 12px;
|
||||
font-size: 12px; font-weight: 600;
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.wp-add { display: flex; align-items: center; gap: 6px; background: none; border: 1.5px dashed var(--border2); border-radius: 10px; padding: 7px 12px; font-size: 12px; font-weight: 600; color: var(--text2); flex-shrink: 0; cursor: pointer; transition: border-color 0.15s, color 0.15s; }
|
||||
.wp-add:active { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* ── Waypoints List ───────────────────────────── */
|
||||
#waypoints-list {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.wl-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
#waypoints-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
|
||||
.wl-item { display: flex; align-items: center; gap: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }
|
||||
.wl-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.wl-label { flex: 1; font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-remove {
|
||||
width: 28px; height: 28px;
|
||||
border: none; background: none;
|
||||
color: var(--text3); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wl-remove { width: 28px; height: 28px; border: none; background: none; color: var(--text3); cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; flex-shrink: 0; }
|
||||
.wl-remove:active { background: var(--red-bg); color: var(--red); }
|
||||
.wl-remove svg { width: 14px; height: 14px; }
|
||||
|
||||
/* Route action buttons */
|
||||
.route-actions {
|
||||
display: flex; gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.btn-action {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text2);
|
||||
font-size: 12px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center; gap: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
/* Route actions */
|
||||
.route-actions { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.btn-action { flex: 1; height: 38px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; color: var(--text2); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 5px; cursor: pointer; transition: all 0.15s; }
|
||||
.btn-action svg { width: 14px; height: 14px; }
|
||||
.btn-action:active { background: var(--surface3); }
|
||||
.btn-action:active { background: var(--surface3); transform: scale(0.94); }
|
||||
.btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); }
|
||||
.btn-action.primary { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-action.primary:active { background: var(--accent-bg); }
|
||||
|
||||
/* ── Route Status ─────────────────────────────── */
|
||||
#route-status {
|
||||
font-size: 13px; color: var(--text2);
|
||||
padding: 8px 0;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
/* ── Route Cards ──────────────────────────────── */
|
||||
#route-cards, #link-cards, #scenic-cards {
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
#route-cards, #link-cards, #scenic-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
|
||||
.route-card {
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
background: var(--surface2); border: 1.5px solid var(--border);
|
||||
border-radius: 14px; padding: 12px 14px; cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: cardFadeIn 0.2s ease-out both;
|
||||
}
|
||||
.route-card.active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
.rc-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.rc-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.rc-title { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); }
|
||||
.rc-km { font-size: 14px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.rc-time { font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; }
|
||||
.rc-bar {
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: var(--surface3);
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
}
|
||||
.rc-bar-dirt { background: var(--gold); height: 100%; transition: width 0.4s; }
|
||||
.rc-bar-asphalt { background: var(--text3); height: 100%; flex: 1; }
|
||||
.rc-stats { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
|
||||
/* Stat pills */
|
||||
.stat-pill {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
border-radius: 20px;
|
||||
padding: 3px 9px;
|
||||
font-size: 11px; font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.stat-pill.dirt { background: var(--gold-bg); color: var(--gold); }
|
||||
.stat-pill.asphalt { background: var(--surface3); color: var(--text2); }
|
||||
.stat-pill.path { background: var(--red-bg); color: var(--red); }
|
||||
|
||||
/* ── Primary Button ───────────────────────────── */
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-size: 15px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
letter-spacing: 0.02em;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.btn-primary svg { width: 18px; height: 18px; }
|
||||
.btn-primary:active { background: var(--accent-h); transform: scale(0.98); }
|
||||
.btn-primary:disabled { opacity: 0.5; pointer-events: none; }
|
||||
|
||||
/* ── Radius / Dist Selector ───────────────────── */
|
||||
.seg-control {
|
||||
display: flex; gap: 4px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.seg-btn {
|
||||
flex: 1; height: 34px;
|
||||
background: none; border: none;
|
||||
border-radius: 9px;
|
||||
font-size: 13px; font-weight: 600;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.seg-btn.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(255,107,0,0.35);
|
||||
}
|
||||
.seg-btn:not(.active):active { background: var(--surface3); }
|
||||
.dist-custom {
|
||||
height: 34px; width: 70px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
color: var(--text);
|
||||
font-size: 13px; font-weight: 600;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dist-custom:focus { border-color: var(--accent); }
|
||||
|
||||
/* ── Recon Results ────────────────────────────── */
|
||||
.recon-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr;
|
||||
gap: 8px; margin-bottom: 14px;
|
||||
}
|
||||
.recon-stat {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.rs-value {
|
||||
font-size: 22px; font-weight: 800;
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.rs-value.gold { color: var(--gold); }
|
||||
.rs-value.red { color: var(--red); }
|
||||
.rs-label { font-size: 11px; color: var(--text2); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
|
||||
.poi-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.poi-row:last-child { border-bottom: none; }
|
||||
.poi-row-label { font-size: 13px; color: var(--text); display: flex; align-items: center; gap: 8px; }
|
||||
.poi-row-count {
|
||||
font-size: 16px; font-weight: 800;
|
||||
color: var(--accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.poi-icon { width: 28px; height: 28px; border-radius: 8px; background: var(--surface2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||
|
||||
/* ── Scenic POI items ─────────────────────────── */
|
||||
.scenic-poi-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 12px; color: var(--text2);
|
||||
padding: 3px 0;
|
||||
}
|
||||
.scenic-score-bar {
|
||||
height: 4px; border-radius: 2px;
|
||||
background: var(--surface3); overflow: hidden; margin: 6px 0;
|
||||
}
|
||||
.scenic-score-fill { height: 100%; background: var(--gold); border-radius: 2px; }
|
||||
|
||||
/* ── Link Points ──────────────────────────────── */
|
||||
.link-points { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.link-pt {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.link-pt-num {
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: #fff; font-size: 12px; font-weight: 800;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-pt-label { font-size: 13px; color: var(--text); flex: 1; }
|
||||
.link-pt.empty .link-pt-num { background: var(--surface3); color: var(--text3); }
|
||||
.link-pt.empty .link-pt-label { color: var(--text3); }
|
||||
#link-status { font-size: 13px; color: var(--text2); padding: 4px 0 10px; }
|
||||
|
||||
/* ── Scenic Config ───────────────────────────── */
|
||||
#scenic-status { font-size: 13px; color: var(--text2); padding: 6px 0; display: flex; align-items: center; gap: 6px; }
|
||||
.dist-row { display: flex; gap: 4px; align-items: center; margin-bottom: 4px; }
|
||||
|
||||
/* ── Marker Popup / Dialog ────────────────────── */
|
||||
#marker-dialog {
|
||||
position: fixed;
|
||||
inset: 0; z-index: 500;
|
||||
display: flex; align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
pointer-events: none; opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
#marker-dialog.open { pointer-events: auto; opacity: 1; }
|
||||
.marker-dialog-inner {
|
||||
background: var(--surface);
|
||||
border-radius: 20px 20px 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0 16px 20px;
|
||||
width: 100%;
|
||||
transform: translateY(30px);
|
||||
transition: transform 0.25s cubic-bezier(0.32, 0, 0.15, 1);
|
||||
}
|
||||
#marker-dialog.open .marker-dialog-inner { transform: translateY(0); }
|
||||
.marker-type-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px; padding: 12px 0;
|
||||
}
|
||||
.marker-type-btn {
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 5px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.marker-type-btn:active { border-color: var(--accent); background: var(--accent-bg); }
|
||||
.marker-type-btn .mt-icon { font-size: 24px; }
|
||||
.marker-type-btn .mt-label { font-size: 11px; font-weight: 600; color: var(--text2); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
|
||||
/* ── No Data Warning ─────────────────────────── */
|
||||
#no-data-warning {
|
||||
display: none;
|
||||
position: fixed; bottom: 80px; left: 12px; right: 12px;
|
||||
background: var(--red-bg);
|
||||
border: 1px solid var(--red);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px; color: var(--red);
|
||||
z-index: 200;
|
||||
}
|
||||
#no-data-warning.visible { display: block; }
|
||||
|
||||
/* ── Loading Skeleton ────────────────────────── */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--surface2) 0%, var(--surface3) 50%, var(--surface2) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-wave 1.4s infinite;
|
||||
border-radius: 8px;
|
||||
height: 14px;
|
||||
}
|
||||
@keyframes skeleton-wave {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.skeleton-card {
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ── Ruler ───────────────────────────────────── */
|
||||
#ruler-info {
|
||||
position: fixed;
|
||||
top: calc(max(env(safe-area-inset-top,0px),12px) + 58px);
|
||||
left: 12px; right: 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px; color: var(--text);
|
||||
font-weight: 600;
|
||||
z-index: 200;
|
||||
display: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
#ruler-info.visible { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* ── Waypoint Markers on Map ─────────────────── */
|
||||
.route-waypoint-marker {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; font-weight: 800;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
||||
border: 2px solid rgba(255,255,255,0.8);
|
||||
}
|
||||
.named-marker-el {
|
||||
font-size: 22px; cursor: pointer;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
|
||||
user-select: none; line-height: 1;
|
||||
}
|
||||
|
||||
/* ── Desktop Layout ──────────────────────────── */
|
||||
@media (min-width: 768px) {
|
||||
#toolbar {
|
||||
flex-direction: column;
|
||||
width: 68px; height: auto;
|
||||
right: auto; left: 0;
|
||||
top: 0; bottom: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
border-top: none;
|
||||
padding: 80px 0 20px;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
.tb-btn {
|
||||
width: 60px; height: 56px;
|
||||
flex: none;
|
||||
}
|
||||
.bottom-sheet {
|
||||
left: 68px; right: auto;
|
||||
width: 340px;
|
||||
max-height: 100vh;
|
||||
border-radius: 0 16px 16px 0;
|
||||
border-top: none;
|
||||
border-right: 1px solid var(--border);
|
||||
top: 0; bottom: 0;
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
.bottom-sheet.open { transform: translateX(0); }
|
||||
#search-bar {
|
||||
left: 80px; right: 12px; max-width: 400px;
|
||||
}
|
||||
#map-controls-r {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
#sheet-backdrop { display: none; }
|
||||
}
|
||||
|
||||
/* ── Misc ────────────────────────────────────── */
|
||||
.text-accent { color: var(--accent); }
|
||||
.text-gold { color: var(--gold); }
|
||||
.text-red { color: var(--red); }
|
||||
.text-muted { color: var(--text2); }
|
||||
.mt-8 { margin-top: 8px; }
|
||||
.mt-12 { margin-top: 12px; }
|
||||
.mb-8 { margin-bottom: 8px; }
|
||||
|
||||
/* cursor crosshair в режиме выбора точки */
|
||||
.cursor-crosshair .maplibregl-canvas { cursor: crosshair !important; }
|
||||
.route-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var
|
||||
@@ -25,6 +25,7 @@
|
||||
<button id="btn-theme" onclick="toggleTheme()" title="Переключить тему">
|
||||
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||||
<span id="theme-label">Авто</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-results"></div>
|
||||
@@ -230,6 +231,7 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.js"></script>
|
||||
<script src="https://unpkg.com/suncalc@1.9.0/suncalc.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Dev Report: Phase 5 Redesign
|
||||
Дата: 2026-05-05
|
||||
Статус: IN PROGRESS
|
||||
|
||||
## Задача
|
||||
Реализация Фазы 5 «Редизайн» — дизайн-система, две темы, SunCalc, skeleton, свайп, десктоп, микро-анимации.
|
||||
|
||||
## Сделано
|
||||
- [ ] Задача 1: Дизайн-система (CSS vars + SunCalc + переключатель)
|
||||
- [ ] Задача 2: Все компоненты на CSS vars
|
||||
- [ ] Задача 3: Skeleton loading
|
||||
- [ ] Задача 4: Свайп вниз для закрытия sheets
|
||||
- [ ] Задача 5: Десктоп-адаптив
|
||||
- [ ] Задача 6: Микро-анимации
|
||||
- [ ] Деплой + проверка
|
||||
|
||||
## Изменённые файлы
|
||||
- `prototype/static/index.html` — SunCalc CDN, theme label
|
||||
- `prototype/static/app.css` — CSS vars для двух тем, skeleton, свайп, десктоп, анимации
|
||||
- `prototype/static/app.js` — SunCalc, toggleTheme 3 режима, skeleton, свайп, анимации
|
||||
Reference in New Issue
Block a user