diff --git a/frontend/index.html b/frontend/index.html index c08ab9f..013a1a6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,12 @@ Адресная программа + @@ -24,6 +30,7 @@ + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index d20db05..1b4fe51 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -34,8 +34,26 @@ const el = { mapEl: document.getElementById('map') as HTMLElement, mapEmpty: document.getElementById('map-empty') as HTMLElement, mapCounter: document.getElementById('map-counter') as HTMLElement, + themeToggle: document.getElementById('theme-toggle') as HTMLButtonElement, }; +// ---- theme toggle (data-theme is set pre-paint by the inline head script) ---- +function currentTheme(): 'light' | 'dark' { + return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; +} +function applyThemeIcon(): void { + const dark = currentTheme() === 'dark'; + el.themeToggle.textContent = dark ? '☀' : '☾'; + el.themeToggle.title = dark ? 'Светлая тема' : 'Тёмная тема'; +} +el.themeToggle.addEventListener('click', () => { + const next = currentTheme() === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + try { localStorage.setItem('mapdash.theme', next); } catch { /* ignore */ } + applyThemeIcon(); +}); +applyThemeIcon(); + const YANDEX_KEY = ((import.meta as any).env?.VITE_YANDEX_KEY as string) || ''; const appearance = loadAppearance(); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 3574659..56a1136 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,52 +1,99 @@ :root { - --bar-color: #4169E1; - --grid-line: #e2e2e2; - --label-w: 340px; - --row-even: #f0f0f0; - --row-odd: #FFFFFF; - --row-hover: #FFECB3; - --month-hover: rgba(65, 105, 225, 0.10); /* appearance (overridden at runtime by appearance.ts) */ --row-h: 30px; --bar-h: 18px; --bar-font: 11px; --label-font: 12px; + --label-w: 340px; + + /* ---- design tokens (light) ---- */ + --bg: #ffffff; + --surface: #f7f8fa; + --surface-2: #eef1f5; + --border: #e5e7eb; + --border-strong: #d1d6de; + --text: #1f2430; + --text-muted: #6b7280; + --accent: #2e66d6; + --accent-weak: rgba(46, 102, 214, .12); + --accent-ring: rgba(46, 102, 214, .20); + --radius: 8px; + --radius-sm: 6px; + --shadow-sm: 0 1px 2px rgba(16, 24, 40, .06); + --shadow-md: 0 6px 18px rgba(16, 24, 40, .12); + --transition: .13s ease; + + /* timeline / map surfaces (built on the tokens above) */ + --grid-line: var(--border); + --row-even: #f5f6f8; + --row-odd: #ffffff; + --row-hover: #fff3cd; + --month-hover: rgba(46, 102, 214, .08); + --tip-bg: #1f2430; + --tip-text: #ffffff; + --tip-sub: #cfd3d6; + --today: #ef5350; } + +/* Dark theme — single source of truth is data-theme on , set by the + no-flash inline script in index.html (defaults to prefers-color-scheme). */ +:root[data-theme="dark"] { + --bg: #0f1420; + --surface: #161c2b; + --surface-2: #1e2637; + --border: #273043; + --border-strong: #33405a; + --text: #e6e9ef; + --text-muted: #93a0b5; + --accent: #5b8def; + --accent-weak: rgba(91, 141, 239, .16); + --accent-ring: rgba(91, 141, 239, .30); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4); + --shadow-md: 0 6px 18px rgba(0, 0, 0, .5); + --row-even: #141a27; + --row-odd: #0f1420; + --row-hover: #33361f; + --month-hover: rgba(91, 141, 239, .12); + --tip-bg: #0b0f18; +} + * { box-sizing: border-box; } html, body { height: 100%; margin: 0; overflow: hidden; } body { display: flex; flex-direction: column; font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; - background: #fff; - color: #222; + background: var(--bg); + color: var(--text); } header { flex: 0 0 auto; - padding: 12px 16px; - border-bottom: 1px solid #e2e2e2; + padding: 12px 18px; + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow-sm); display: flex; - gap: 14px; + gap: 18px; align-items: stretch; /* Responsive: the fields group compresses (and wraps its own fields) first; - if the window is still too narrow, whole gray blocks drop to the next row + if the window is still too narrow, whole blocks drop to the next row instead of overlapping. */ flex-wrap: wrap; align-content: flex-start; - background: #fff; - z-index: 20; + background: var(--bg); + /* position:relative makes the z-index actually take effect, so the header + (and its open dropdown panels) forms a stacking context above the timeline + — otherwise the column-resizer line bleeds over the filter dropdown lists. */ + position: relative; + z-index: 40; } -/* Header groups (boxed blocks). By default they keep their natural width. */ +/* Header groups. Flat now (no boxes) — the header itself is the panel. */ .hgroup { display: flex; align-items: center; gap: 16px; - padding: 10px 14px; - background: #f2f2f2; - border: 1px solid #e2e2e2; - border-radius: 8px; + padding: 4px 0; flex-shrink: 1; min-width: 0; } @@ -55,7 +102,7 @@ header { flex-shrink: 4; min-width: 0; display: grid; grid-template-columns: repeat(4, minmax(130px, 1fr)); - gap: 10px 14px; + gap: 12px 14px; align-items: start; align-content: start; } @@ -66,132 +113,199 @@ header { .hgroup-fields input[type=text], .hgroup-fields input[type=date] { min-width: 0; width: 100%; } @media (max-width: 760px) { .hgroup-fields { grid-template-columns: repeat(2, minmax(120px, 1fr)); } } -.hgroup-brand { background: #f2f2f2; } /* Fields group: top-align so the date input sits UNDER Город/Бренд. */ .hgroup-fields { align-items: flex-start; } .fcol { display: flex; flex-direction: column; gap: 8px; } -input[type=date] { - border: 1px solid #ccc; - border-radius: 4px; - padding: 6px 8px; + +input[type=date], +input[type=text] { + height: 36px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0 10px; font-size: 13px; min-width: 0; width: 100%; - box-sizing: border-box; + background: var(--bg); + color: var(--text); + transition: border-color var(--transition), box-shadow var(--transition); } -/* Compact single-column layout: the three checkboxes stack in three rows so the - block takes as little horizontal space as possible. */ +input[type=date]:hover, +input[type=text]:hover { border-color: var(--border-strong); } +input[type=date]:focus, +input[type=text]:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} +input::placeholder { color: var(--text-muted); } +/* Search field: magnifier icon inside the input. */ +#search { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 10px center; + background-size: 15px; + padding-left: 32px; +} +:root[data-theme="dark"] input[type=date]::-webkit-calendar-picker-indicator { + filter: invert(1) opacity(.65); +} + +/* Compact single-column layout: the three checkboxes stack in three rows. */ .hgroup-checks { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; - gap: 6px; + gap: 8px; } .hgroup-checks .checkbox-field { align-self: flex-start; } -.brand { display: flex; flex-direction: column; justify-content: center; line-height: 1.2; } +/* Let the brand block fill the header height so the counters can sit at the + bottom, level with the date fields / mode switch. */ +.hgroup-brand { align-items: stretch; } +.brand { display: flex; flex-direction: column; justify-content: flex-start; line-height: 1.2; } .brand-name { font-size: 18px; font-weight: 700; white-space: nowrap; } -.brand-sub { font-size: 15px; font-weight: 400; color: #666; white-space: nowrap; } +.brand-sub { font-size: 15px; font-weight: 400; color: var(--text-muted); white-space: nowrap; } -.field { display: flex; flex-direction: column; gap: 4px; } -.field label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: .03em; } -input[type=text] { - border: 1px solid #ccc; - border-radius: 4px; - padding: 6px 8px; - font-size: 13px; - min-width: 0; - width: 100%; +.field { display: flex; flex-direction: column; gap: 5px; } +.field label { + font-size: 11px; color: var(--text-muted); + text-transform: uppercase; letter-spacing: .05em; font-weight: 500; +} + +/* Counters — compact inset inside the brand block. */ +.brand #status { + margin-top: auto; padding: 6px 10px; + background: var(--surface); border: 1px solid var(--border); + border-radius: var(--radius-sm); font-size: 12px; color: var(--text-muted); + line-height: 1.5; white-space: nowrap; } -/* Counters as a compact gray block, pushed to the end of the header row so it - sits in line with the other boxed groups instead of floating below. */ -/* Counters now live inside the brand block as a compact inset. */ -.brand #status { margin-top: 10px; padding: 6px 10px; background: #fff; border: 1px solid #e2e2e2; border-radius: 6px; font-size: 12px; color: #666; line-height: 1.5; white-space: nowrap; } /* Scale group: stack the "Оформление" button under the zoom slider. */ -.hgroup-scale { flex-direction: column; align-items: flex-start; gap: 10px; } +.hgroup-scale { flex-direction: column; align-items: flex-start; gap: 12px; } .hgroup-scale .decor-wrap { align-self: flex-start; } .checkbox-field { flex-direction: row; align-items: center; gap: 6px; align-self: center; } .checkbox-field label { display: flex; align-items: center; gap: 6px; - font-size: 13px; color: #222; text-transform: none; letter-spacing: normal; - cursor: pointer; white-space: nowrap; + font-size: 13px; color: var(--text); text-transform: none; letter-spacing: normal; + font-weight: 400; cursor: pointer; white-space: nowrap; } -.checkbox-field input[type=checkbox] { cursor: pointer; } +.checkbox-field input[type=checkbox] { cursor: pointer; accent-color: var(--accent); } -.zoom-field input[type=range] { width: 150px; cursor: pointer; } +.zoom-field input[type=range] { width: 160px; cursor: pointer; accent-color: var(--accent); } .zoom-field label { white-space: nowrap; } /* checkbox dropdown filter */ .dropdown { position: relative; } .dropdown-btn { - border: 1px solid #ccc; border-radius: 4px; background: #fff; - padding: 6px 10px; font-size: 13px; min-width: 180px; text-align: left; + height: 36px; + border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); + color: var(--text); padding: 0 12px; font-size: 13px; min-width: 180px; text-align: left; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 8px; + transition: border-color var(--transition), box-shadow var(--transition); } -.dropdown-btn:hover { border-color: #999; } -.dropdown-btn .caret { font-size: 10px; color: #888; } +.dropdown-btn:hover { border-color: var(--border-strong); } +.dropdown.open .dropdown-btn { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); } +.dropdown-btn .caret { font-size: 10px; color: var(--text-muted); } .dropdown-panel { display: none; position: absolute; top: calc(100% + 4px); left: 0; - background: #fff; border: 1px solid #ccc; border-radius: 4px; - box-shadow: 0 4px 14px rgba(0,0,0,.12); padding: 6px 0; + background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); + box-shadow: var(--shadow-md); padding: 6px 0; min-width: 220px; max-height: 280px; overflow-y: auto; z-index: 30; } .dropdown.open .dropdown-panel { display: block; } .dropdown-panel label { - display: flex; align-items: center; gap: 8px; padding: 5px 12px; - font-size: 13px; cursor: pointer; white-space: nowrap; + display: flex; align-items: center; gap: 8px; padding: 6px 12px; + font-size: 13px; cursor: pointer; white-space: nowrap; color: var(--text); + /* Option labels are