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