311 lines
12 KiB
Markdown
311 lines
12 KiB
Markdown
# 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`)
|