From b4289a7e8edf20f016a31f7c23d19debecd4eb66 Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 02:00:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 02:00:01 --- tasks/enduro-trails/BRD_PHASE5.md | 44 +- tasks/enduro-trails/prototype/static/app.css | 1169 ++++++++++------- tasks/enduro-trails/prototype/static/app.js | 26 +- .../enduro-trails/prototype/static/index.html | 405 +++--- 4 files changed, 960 insertions(+), 684 deletions(-) diff --git a/tasks/enduro-trails/BRD_PHASE5.md b/tasks/enduro-trails/BRD_PHASE5.md index a53fca8..2fef575 100644 --- a/tasks/enduro-trails/BRD_PHASE5.md +++ b/tasks/enduro-trails/BRD_PHASE5.md @@ -22,23 +22,41 @@ ## 3. Дизайн-система -### 3.1 Цветовая палитра +### 3.1 Цветовая палитра — две темы +Тема переключается кнопкой ☀️/🌙 в search bar. Автодетект: если время 07:00–20:00 → светлая, иначе → тёмная. + +**Тёмная (ночная езда):** ``` -Фон приложения: #0D1117 (почти чёрный, как грязь ночью) -Поверхности: #161B22 (тёмно-серый) -Поверхности 2: #21262D (чуть светлее) -Акцент (оранж): #FF6B00 (цвет эндуро-резины, огня) -Акцент hover: #FF8C2A -Золото (Lev1-2): #FFD700 -Красный (Lev3-5): #FF3B1F -Текст primary: #E6EDF3 -Текст secondary: #8B949E -Текст muted: #484F58 -Успех: #2EA043 -Граница: #30363D +--bg: #0D1117 +--surface: #161B22 +--surface2: #21262D +--border: #30363D +--text: #E6EDF3 +--text2: #8B949E +--accent: #FF6B00 +--gold: #FFD700 +--red: #FF3B1F +--success: #2EA043 ``` +**Светлая (дневная езда):** +``` +--bg: #F5F5F0 (бежевый, не слепит на солнце) +--surface: #FFFFFF +--surface2: #F0F0EA +--border: #D0CFC8 +--text: #1A1A1A +--text2: #6B6B6B +--accent: #E55A00 (оранжевый чуть темнее — виден на белом) +--gold: #C89B00 +--red: #CC2200 +--success: #1A7A30 +``` + +Реализация через CSS custom properties на `:root` + класс `body.theme-dark` / `body.theme-light`. +Стиль карты MapLibre меняется соответственно (тёмный/светлый style.json — уже существуют). + ### 3.2 Типографика ``` diff --git a/tasks/enduro-trails/prototype/static/app.css b/tasks/enduro-trails/prototype/static/app.css index 36581b9..df290c9 100644 --- a/tasks/enduro-trails/prototype/static/app.css +++ b/tasks/enduro-trails/prototype/static/app.css @@ -1,499 +1,728 @@ -* { box-sizing: border-box; margin: 0; padding: 0; } +/* ═══════════════════════════════════════════════════════════════════ + Enduro Trails — Design System v5.0 + Dark (night ride) + Light (day ride) themes + Mobile-first, thumb-friendly, adventure style + ═══════════════════════════════════════════════════════════════════ */ -body { - background: #f5f3ee; - color: #333333; - font-family: 'Segoe UI', system-ui, sans-serif; - height: 100vh; +/* ── Reset ────────────────────────────────────── */ +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} + +/* ── CSS Variables — Dark Theme (default) ───── */ +body.theme-dark { + --bg: #0D1117; + --surface: #161B22; + --surface2: #21262D; + --surface3: #2D333B; + --border: #30363D; + --border2: #444C56; + --text: #E6EDF3; + --text2: #8B949E; + --text3: #484F58; + --accent: #FF6B00; + --accent-h: #FF8C2A; + --accent-bg: rgba(255,107,0,0.12); + --gold: #FFD700; + --gold-bg: rgba(255,215,0,0.12); + --red: #FF3B1F; + --red-bg: rgba(255,59,31,0.12); + --success: #2EA043; + --shadow: 0 4px 24px rgba(0,0,0,0.6); + --shadow-sm: 0 2px 8px rgba(0,0,0,0.4); +} + +/* ── CSS Variables — Light Theme ────────────── */ +body.theme-light { + --bg: #F0EFE8; + --surface: #FFFFFF; + --surface2: #F5F4EE; + --surface3: #ECEAE2; + --border: #D4D0C8; + --border2: #B8B4AA; + --text: #1C1C1A; + --text2: #6B6760; + --text3: #9C9890; + --accent: #D95200; + --accent-h: #BF4800; + --accent-bg: rgba(217,82,0,0.1); + --gold: #A07800; + --gold-bg: rgba(160,120,0,0.1); + --red: #B82200; + --red-bg: rgba(184,34,0,0.1); + --success: #1A6B2A; + --shadow: 0 4px 24px rgba(0,0,0,0.15); + --shadow-sm: 0 2px 8px rgba(0,0,0,0.1); +} + +/* ── Base ─────────────────────────────────────── */ +html, body { + height: 100%; + font-family: -apple-system, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + background: var(--bg); + color: var(--text); + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +/* ── Map Container ────────────────────────────── */ +#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; + height: 50px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + display: flex; + align-items: center; + padding: 0 6px 0 14px; + 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; +} +#search-input { + flex: 1; + background: none; + border: none; + color: var(--text); + font-size: 15px; + outline: none; + min-width: 0; +} +#search-input::placeholder { color: var(--text3); } +#btn-theme { + 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; +} +#btn-theme:active { transform: scale(0.9); background: var(--surface3); } +#btn-theme svg { width: 16px; height: 16px; } + +/* Search results */ +#search-results { + 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: 14px; + z-index: 300; + box-shadow: var(--shadow); + overflow: hidden; + display: none; +} +.search-result-item { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + cursor: pointer; + display: flex; align-items: center; gap: 10px; + transition: background 0.1s; +} +.search-result-item:last-child { border-bottom: none; } +.search-result-item:active { 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-controls-r { + position: fixed; + right: 12px; + bottom: calc(80px + env(safe-area-inset-bottom, 0px) + 12px); display: flex; flex-direction: column; -} - -#header { - background: #ffffff; - border-bottom: 1px solid #ddd; - padding: 10px 16px; - display: flex; - align-items: center; - gap: 16px; - flex-shrink: 0; - z-index: 10; - box-shadow: 0 1px 4px rgba(0,0,0,0.1); -} - -#header h1 { - font-size: 18px; - font-weight: 600; - color: #e07b00; - letter-spacing: 0.5px; -} - -#header .subtitle { - font-size: 12px; - color: #888; -} - -#controls { - display: flex; gap: 8px; - margin-left: auto; - align-items: center; - flex-wrap: wrap; + z-index: 200; } - -.toggle-btn { - background: #f0f0f0; - border: 1px solid #ccc; - color: #444; - padding: 5px 12px; - border-radius: 4px; +.map-btn { + width: 48px; height: 48px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + color: var(--text2); + display: flex; align-items: center; justify-content: center; cursor: pointer; - font-size: 13px; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 6px; -} - -.toggle-btn:hover { background: #e0e0e0; } -.toggle-btn.active { background: #ff6600; border-color: #ff6600; color: #fff; font-weight: 600; } - -.dot { - width: 10px; height: 10px; - border-radius: 50%; - display: inline-block; -} - -#map-container { - flex: 1; + 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 { background: var(--accent); color: #fff; border-color: var(--accent); } -#map { width: 100%; height: 100%; } +/* ── Bottom Toolbar ───────────────────────────── */ +#toolbar { + 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; + z-index: 300; + box-shadow: 0 -4px 20px rgba(0,0,0,0.2); +} +.tb-btn { + flex: 1; + display: flex; flex-direction: column; + align-items: center; justify-content: center; + 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; + -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 span { line-height: 1; } -/* Legend */ -#legend { - position: absolute; - bottom: 30px; - left: 12px; - background: rgba(255,255,255,0.95); - border: 1px solid #ddd; - border-radius: 6px; - padding: 10px 14px; - font-size: 12px; - z-index: 5; - min-width: 160px; - box-shadow: 0 2px 8px rgba(0,0,0,0.12); +/* ── Bottom Sheet ─────────────────────────────── */ +.bottom-sheet { + 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; + -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)); +} +.bottom-sheet.open { + transform: translateY(0); +} +.sheet-handle { + width: 36px; height: 4px; + background: var(--border2); + border-radius: 2px; + margin: 12px auto 0; + cursor: grab; +} +.sheet-header { + display: flex; align-items: center; + 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-close { + 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; +} +.sheet-close svg { width: 16px; height: 16px; } +.sheet-close:active { background: var(--surface3); } +.sheet-body { padding: 14px 16px; } +.sheet-hint { + font-size: 13px; color: var(--text2); + text-align: center; padding: 16px 0 8px; + line-height: 1.5; } -#legend h3 { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 1px; - color: #888; +/* Sheet backdrop */ +#sheet-backdrop { + position: fixed; inset: 0; + background: rgba(0,0,0,0.45); + z-index: 390; + opacity: 0; pointer-events: none; + transition: opacity 0.3s; +} +#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; +} + +/* ── Waypoints Row ────────────────────────────── */ +.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: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-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: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; +} +.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: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; +} +.btn-action svg { width: 14px; height: 14px; } +.btn-action:active { background: var(--surface3); } +.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 Cards ──────────────────────────────── */ +#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; + -webkit-tap-highlight-color: transparent; +} +.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; } -.legend-item { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 5px; -} - -.legend-line { - width: 28px; - height: 3px; - border-radius: 2px; -} - -.legend-dashed { - width: 28px; - height: 0; - border-top: 2px dashed #cc9900; -} - -/* Popup */ -.maplibregl-popup-content { - background: #ffffff !important; - color: #333 !important; - border: 1px solid #ddd !important; - border-radius: 6px !important; - padding: 12px 14px !important; - font-size: 13px !important; - min-width: 180px; - box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important; -} - -.popup-title { +/* ── 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; - font-size: 14px; - color: #e07b00; - margin-bottom: 6px; -} - -.popup-row { - display: flex; - justify-content: space-between; - gap: 12px; - margin-bottom: 3px; -} - -.popup-key { color: #888; } -.popup-val { color: #333; font-weight: 500; } - -/* Stats bar */ -#stats { - position: absolute; - top: 10px; - right: 12px; - background: rgba(255,255,255,0.9); - border: 1px solid #ddd; - border-radius: 4px; - padding: 6px 10px; - font-size: 11px; - color: #666; - z-index: 5; - box-shadow: 0 1px 4px rgba(0,0,0,0.1); -} - -/* Loading indicator */ -#loading { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(255,255,255,0.97); - border: 1px solid #ff6600; - border-radius: 8px; - padding: 20px 30px; - text-align: center; - z-index: 100; + z-index: 200; display: none; - color: #333; + box-shadow: var(--shadow-sm); } +#ruler-info.visible { display: flex; align-items: center; gap: 8px; } -#loading.visible { display: block; } -#loading .spinner { - width: 32px; height: 32px; - border: 3px solid #eee; - border-top-color: #ff6600; - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin: 0 auto 10px; -} - -@keyframes spin { to { transform: rotate(360deg); } } - -/* No-data warning */ -#no-data-warning { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(255,255,255,0.97); - border: 1px solid #ff6600; - border-radius: 8px; - padding: 24px 32px; - text-align: center; - z-index: 100; - max-width: 400px; - display: none; -} - -#no-data-warning.visible { display: block; } -#no-data-warning h2 { color: #ff6600; margin-bottom: 10px; } -#no-data-warning p { color: #666; font-size: 13px; line-height: 1.5; } -#no-data-warning code { - background: #f5f5f5; - padding: 2px 6px; - border-radius: 3px; - font-family: monospace; - color: #333; -} - -/* ─── Кастомные кнопки управления ────────────────────────────────────────── */ -.custom-map-ctrl { - position: absolute; - right: 10px; - display: flex; - flex-direction: column; - gap: 6px; - z-index: 5; -} - -#map-controls-br { - bottom: 40px; -} - -/* ─── Панель маршрута — не перекрывать кнопки управления ─────────────────── */ -#route-panel { - right: 56px !important; /* кнопки 36px + gap 10px + отступ 10px */ -} - -@media (max-width: 768px) { - #route-panel { - right: 8px !important; - left: 8px !important; - width: auto !important; - bottom: 100px !important; /* выше кнопок на мобиле */ - max-height: 50vh; - } -} - -.map-ctrl-btn { - width: 36px; - height: 36px; - background: #fff; - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - font-size: 18px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 1px 4px rgba(0,0,0,0.15); - transition: background 0.15s; - user-select: none; -} - -.map-ctrl-btn:hover { background: #f5f5f5; } -.map-ctrl-btn.active { background: #ff6600; border-color: #ff6600; } - -/* ─── Маркер текущего местоположения ─────────────────────────────────────── */ -.my-location-marker { - position: relative; - width: 20px; - height: 20px; -} - -.my-location-dot { - position: absolute; - top: 50%; left: 50%; - transform: translate(-50%, -50%); - width: 14px; height: 14px; - background: #ff6600; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 0 4px rgba(0,0,0,0.3); - z-index: 2; -} - -.my-location-pulse { - position: absolute; - top: 50%; left: 50%; - transform: translate(-50%, -50%); - width: 30px; height: 30px; - background: rgba(255, 102, 0, 0.25); - border-radius: 50%; - animation: location-pulse 1.5s ease-out infinite; -} - -/* ─── Поиск (Nominatim) ─────────────────────────────────────────────── */ -#search-box { - position: relative; - margin-left: 12px; -} - -#search-input { - padding: 6px 12px; - border: 1px solid #ccc; - border-radius: 20px; - font-size: 13px; - width: 220px; - outline: none; - background: rgba(255,255,255,0.95); -} - -#search-input:focus { - border-color: #e07b00; - box-shadow: 0 0 0 2px rgba(224,123,0,0.15); -} - -#search-results { - display: none; - position: absolute; - top: calc(100% + 4px); - left: 0; - width: 300px; - background: white; - border: 1px solid #ddd; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 100; - max-height: 280px; - overflow-y: auto; -} - -.search-result-item { - padding: 8px 12px; - cursor: pointer; - font-size: 13px; - border-bottom: 1px solid #f0f0f0; - line-height: 1.4; -} - -.search-result-item:last-child { - border-bottom: none; -} - -.search-result-item:hover { - background: #fff8f0; -} - -.search-result-name { - font-weight: 600; - color: #333; -} - -.search-result-detail { - font-size: 11px; - color: #888; - margin-top: 2px; -} - -@keyframes location-pulse { - 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 1; } - 100% { transform: translate(-50%, -50%) scale(1.5); opacity: 0; } -} - -/* ─── Фаза 3: Карточки маршрутов ─────────────────────────────────────────── */ -.route-card { - border: 2px solid #eee; - border-radius: 6px; - padding: 8px 10px; - margin-bottom: 6px; - cursor: pointer; - transition: border-color 0.15s, background 0.15s; -} -.route-card:hover { background: #fff8f0; } -.route-card.active { border-color: #ff6600; background: #fff8f0; } - -.route-card-header { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 6px; -} - -.route-color-dot { - width: 10px; height: 10px; - border-radius: 50%; - flex-shrink: 0; -} - -.route-card-title { font-weight: 600; flex: 1; font-size: 13px; } -.route-card-dist { color: #333; font-weight: 600; font-size: 13px; } -.route-card-time { color: #666; font-size: 12px; } - -.route-coverage-bar { - display: flex; - height: 6px; - border-radius: 3px; - overflow: hidden; - margin-bottom: 4px; - background: #eee; -} -.route-coverage-bar div { height: 100%; min-width: 3px; } - -.route-card-summary { font-size: 11px; color: #666; margin-bottom: 4px; } - -.route-card-details { margin-top: 6px; } -.route-stat-row { font-size: 12px; padding: 2px 0; color: #444; } - -.route-details-toggle { - font-size: 11px; color: #888; background: none; border: none; - cursor: pointer; padding: 2px 0; width: 100%; text-align: right; -} -.route-details-toggle:hover { color: #ff6600; } - -/* ─── Фаза 3: Панель точек маршрута ──────────────────────────────────────── */ -.waypoint-row { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 0; - border-bottom: 1px solid #f0f0f0; - font-size: 12px; - cursor: grab; -} -.waypoint-row:last-child { border-bottom: none; } -.waypoint-row.drag-over { background: #fff3e0; border-radius: 4px; } - -.waypoint-label { - width: 20px; height: 20px; - border-radius: 50%; - display: flex; align-items: center; justify-content: center; - font-size: 10px; font-weight: 700; - flex-shrink: 0; - color: #fff; -} -.waypoint-label.start { background: #00aa44; } -.waypoint-label.end { background: #cc0000; } -.waypoint-label.mid { background: #fff; color: #0066ff; border: 2px solid #0066ff; } - -.waypoint-coords { - flex: 1; - color: #555; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.waypoint-remove { - background: none; border: none; cursor: pointer; - color: #aaa; font-size: 14px; padding: 0 2px; - line-height: 1; -} -.waypoint-remove:hover { color: #cc0000; } - -/* ─── Фаза 3: Маркеры на карте ───────────────────────────────────────────── */ +/* ── Waypoint Markers on Map ─────────────────── */ .route-waypoint-marker { - display: flex; align-items: center; justify-content: center; + width: 28px; height: 28px; border-radius: 50%; - font-size: 10px; font-weight: 700; + display: flex; align-items: center; justify-content: center; + font-size: 11px; font-weight: 800; + color: #fff; cursor: pointer; - box-shadow: 0 1px 4px rgba(0,0,0,0.3); + 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: 20px; - cursor: pointer; - filter: drop-shadow(0 1px 3px rgba(0,0,0,0.4)); - user-select: none; - line-height: 1; + font-size: 22px; cursor: pointer; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); + user-select: none; line-height: 1; } -/* ─── Фаза 4: Разведка ───────────────────────────────────────────────────── */ -.recon-radius-btn { - flex: 1; - padding: 4px 0; - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - font-size: 11px; - background: #f0f0f0; - transition: background 0.15s; +/* ── 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; } } -.recon-radius-btn:hover { background: #e0e0e0; } -.recon-radius-btn.active { background: #ff6600; color: #fff; border-color: #ff6600; font-weight: 600; } -/* ─── Фаза 4: Красивый маршрут ─────────────────────────────────────────── */ -.scenic-km-btn { - flex: 1; - padding: 4px 0; - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - background: #f0f0f0; - transition: background 0.15s; -} -.scenic-km-btn:hover { background: #e0e0e0; } -.scenic-km-btn.active { background: #ff6600; color: #fff; border-color: #ff6600; font-weight: 600; } +/* ── 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; } -.scenic-poi-item { - display: flex; align-items: center; gap: 6px; - font-size: 12px; color: #444; padding: 2px 0; -} +/* cursor crosshair в режиме выбора точки */ +.cursor-crosshair .maplibregl-canvas { cursor: crosshair !important; } diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index 6251311..fe9450a 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -1,7 +1,7 @@ // ─── Общее: деактивация всех режимов ──────────────────────────────────────────── function deactivateAllModes() { - if (routeMode) { routeMode = false; document.getElementById('btn-route').classList.remove('active'); document.getElementById('route-panel').style.display = 'none'; clearRoute(); } + if (routeMode) { routeMode = false; document.getElementById('tb-route').classList.remove('active'); closeSheet('sheet-route'); clearRoute(); } if (rulerMode) toggleRuler(); if (markerMode) toggleMarkerMode(); if (typeof reconMode !== 'undefined' && reconMode) toggleReconMode(); @@ -65,7 +65,7 @@ function locateMe() { alert('Геолокация недоступна в этом браузере'); return; } - const btn = document.getElementById('btn-locate'); + const btn = document.getElementById('btn-locate-x'); btn.textContent = '⏳'; navigator.geolocation.getCurrentPosition( (pos) => { @@ -138,7 +138,7 @@ function getBasePath() { // ─── Режим маршрута ─────────────────────────────────────────────────────────── function toggleRouteMode() { routeMode = !routeMode; - const btn = document.getElementById('btn-route'); + const btn = document.getElementById('tb-route'); const panel = document.getElementById('route-panel'); if (routeMode) { deactivateAllModes(); @@ -618,7 +618,7 @@ function saveMarkers(markers) { function toggleMarkerMode() { markerMode = !markerMode; - const btn = document.getElementById('btn-markers'); + const btn = document.getElementById('tb-marker'); if (markerMode) { deactivateAllModes(); markerMode = true; @@ -1030,7 +1030,7 @@ let rulerTotal = 0; function toggleRuler() { rulerMode = !rulerMode; - const btn = document.getElementById('btn-ruler'); + const btn = document.getElementById('tb-ruler'); if (rulerMode) { deactivateAllModes(); rulerMode = true; @@ -1136,7 +1136,7 @@ let reconRadius = 20; function toggleReconMode() { reconMode = !reconMode; - const btn = document.getElementById('btn-recon'); + const btn = document.getElementById('tb-recon'); if (reconMode) { deactivateAllModes(); reconMode = true; @@ -1222,7 +1222,7 @@ function clearRecon() { if (map.getLayer('recon-circle-fill')) map.removeLayer('recon-circle-fill'); if (map.getLayer('recon-circle-stroke')) map.removeLayer('recon-circle-stroke'); if (map.getSource('recon-circle')) map.removeSource('recon-circle'); - document.getElementById('recon-panel').style.display = 'none'; + closeSheet('sheet-recon'); reconCenter = null; } @@ -1233,13 +1233,13 @@ let linkMarkers = []; function toggleLinkMode() { linkMode = !linkMode; - const btn = document.getElementById('btn-link'); + const btn = document.getElementById('tb-link'); if (linkMode) { deactivateAllModes(); linkMode = true; btn.classList.add('active'); window._map.getCanvas().style.cursor = 'crosshair'; - document.getElementById('link-panel').style.display = 'block'; + openSheet('sheet-link'); document.getElementById('link-status').textContent = '1️⃣ Кликни конец первого трека'; document.getElementById('link-cards').innerHTML = ''; } else { @@ -1356,7 +1356,7 @@ function clearLink() { const sid = `link-src-${i}`; if (map.getSource(sid)) map.removeSource(sid); } - document.getElementById('link-panel').style.display = 'none'; + closeSheet('sheet-link'); document.getElementById('link-cards').innerHTML = ''; } @@ -1370,13 +1370,13 @@ let activeScenicIdx = 0; function toggleScenicMode() { scenicMode = !scenicMode; - const btn = document.getElementById('btn-scenic'); + const btn = document.getElementById('tb-scenic'); if (scenicMode) { deactivateAllModes(); scenicMode = true; btn.classList.add('active'); window._map.getCanvas().style.cursor = 'crosshair'; - document.getElementById('scenic-panel').style.display = 'block'; + openSheet('sheet-scenic'); document.getElementById('scenic-status').textContent = 'Кликни точку старта на карте'; } else { btn.classList.remove('active'); @@ -1499,7 +1499,7 @@ function clearScenic() { if (scenicStartMarker) { scenicStartMarker.remove(); scenicStartMarker = null; } scenicStart = null; scenicRoutes = []; - document.getElementById('scenic-panel').style.display = 'none'; + closeSheet('sheet-scenic'); const cardsEl = document.getElementById('scenic-cards'); if (cardsEl) cardsEl.innerHTML = ''; } diff --git a/tasks/enduro-trails/prototype/static/index.html b/tasks/enduro-trails/prototype/static/index.html index 4ae6c8b..d991a46 100644 --- a/tasks/enduro-trails/prototype/static/index.html +++ b/tasks/enduro-trails/prototype/static/index.html @@ -1,206 +1,235 @@ - - - Enduro Trails — ЦФО + Чувашия - - - - - - + + + + + + Enduro Trails + + - + -