# 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 ``` (до `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`)