From d8b0c08aaaa81e0145752bbbe81fd0ccb25e3e47 Mon Sep 17 00:00:00 2001 From: IK Date: Sun, 13 Sep 2026 16:09:56 +0700 Subject: [PATCH] feat: theme fishing data graphics --- README.md | 2 +- apps/web/src/styles/activity-timeline.css | 4 ++-- apps/web/src/styles/coordinate-radar.css | 2 +- apps/web/src/styles/fish-silhouette.css | 2 +- apps/web/src/styles/tackle-glyph.css | 2 +- apps/web/src/styles/theme.css | 24 +++++++++++++++++++++++ docs/ROADMAP.md | 2 +- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f634605..a1d9c6b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ FastAPI ─ PostgreSQL 17 Production CSP ограничивает browser-запросы текущим доменом и отдельным files-доменом для изображений, запрещает plugins, frames, inline handlers и attributes, eval, wildcard и HTTP. Page scripts и scoped styles принудительно выпускаются отдельными same-origin `_astro`-ассетами; единственное временное inline-исключение остаётся для динамического JSON-LD и описано в [CSP inventory](docs/csp-inventory.md). -Тема по умолчанию следует системному `prefers-color-scheme`, а переключатель в header позволяет выбрать системную, светлую или тёмную палитру. Выбор сохраняется в cookie и применяется Astro при SSR без localStorage-only flash и ослабления CSP. Базовые поверхности, каталог, формы, таблицы, moderation-карточки, provenance/status-плашки, навигационная леска, паспорта данных и состояния загрузки уже используют семантические light/dark-токены; ограничения дальнейшей миграции описаны в [dark-theme.md](docs/dark-theme.md). +Тема по умолчанию следует системному `prefers-color-scheme`, а переключатель в header позволяет выбрать системную, светлую или тёмную палитру. Выбор сохраняется в cookie и применяется Astro при SSR без localStorage-only flash и ослабления CSP. Публичные и административные поверхности, формы, таблицы, provenance/status-плашки и фирменная SVG/CSS-графика используют семантические light/dark-токены и базовый forced-colors layer; ограничения и оставшаяся визуальная приёмка описаны в [dark-theme.md](docs/dark-theme.md). Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты. diff --git a/apps/web/src/styles/activity-timeline.css b/apps/web/src/styles/activity-timeline.css index b5bbd19..90074da 100644 --- a/apps/web/src/styles/activity-timeline.css +++ b/apps/web/src/styles/activity-timeline.css @@ -1,3 +1,3 @@ -.activity-timeline{width:min(1180px,calc(100% - 64px));margin:20px auto 0;padding:25px 28px;border-radius:14px;background:#fff;border:1px solid #d9e1da}.activity-timeline header{display:flex;align-items:end;justify-content:space-between;gap:20px}.activity-timeline h2{font:400 29px Georgia,serif;margin:5px 0 0}.activity-timeline header p{margin:0;color:#71817e;font-size:10px} -.timeline-chart{height:145px;display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin-top:18px;padding-top:8px;border-bottom:1px solid #a9bbb1;background:repeating-linear-gradient(to top,transparent 0,transparent 35px,#dce5de 36px)}.timeline-slot{position:relative;display:grid;grid-template-rows:1fr auto;place-items:center}.timeline-line{position:relative;align-self:end;width:2px;height:var(--meter-level);min-height:10px;background:#3f7779}.timeline-line i{position:absolute;left:0;top:0;width:12px;height:18px;margin:-4px 0 0 -5px;border-radius:7px 7px 9px 9px;background:linear-gradient(#ff785a 0 35%,var(--lime) 35%);box-shadow:0 0 0 4px #c9f45b18}.timeline-slot strong{position:absolute;top:2px;font:400 14px Georgia,serif;color:#365c5e}.timeline-slot small{position:absolute;bottom:-20px;color:#71817e;font-size:8px;white-space:nowrap}.activity-timeline+*{margin-top:42px} +.activity-timeline{width:min(1180px,calc(100% - 64px));margin:20px auto 0;padding:25px 28px;border-radius:14px;background:var(--surface);border:1px solid var(--border-soft)}.activity-timeline header{display:flex;align-items:end;justify-content:space-between;gap:20px}.activity-timeline h2{font:400 29px Georgia,serif;margin:5px 0 0}.activity-timeline header p{margin:0;color:var(--text-muted);font-size:10px} +.timeline-chart{height:145px;display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin-top:18px;padding-top:8px;border-bottom:1px solid var(--border);background:repeating-linear-gradient(to top,transparent 0,transparent 35px,var(--graphic-grid) 36px)}.timeline-slot{position:relative;display:grid;grid-template-rows:1fr auto;place-items:center}.timeline-line{position:relative;align-self:end;width:2px;height:var(--meter-level);min-height:10px;background:var(--graphic-line)}.timeline-line i{position:absolute;left:0;top:0;width:12px;height:18px;margin:-4px 0 0 -5px;border-radius:7px 7px 9px 9px;background:linear-gradient(#ff785a 0 35%,var(--lime) 35%);box-shadow:0 0 0 4px color-mix(in srgb,var(--lime) 12%,transparent)}.timeline-slot strong{position:absolute;top:2px;font:400 14px Georgia,serif;color:var(--text)}.timeline-slot small{position:absolute;bottom:-20px;color:var(--text-muted);font-size:8px;white-space:nowrap}.activity-timeline+*{margin-top:42px} @media(max-width:720px){.activity-timeline{width:calc(100% - 28px);padding:20px 15px}.activity-timeline header{display:block}.activity-timeline header p{margin-top:6px}.timeline-slot small{font-size:7px}} diff --git a/apps/web/src/styles/coordinate-radar.css b/apps/web/src/styles/coordinate-radar.css index 368867f..f1bed57 100644 --- a/apps/web/src/styles/coordinate-radar.css +++ b/apps/web/src/styles/coordinate-radar.css @@ -1 +1 @@ -.coordinate-radar{position:relative;flex:0 0 180px;width:180px;height:180px;display:grid;place-items:center;color:var(--lime)}.coordinate-radar svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:#ffffff20;stroke-width:1}.coordinate-radar .radar-sweep{fill:#c9f45b0d;stroke:#c9f45b38}.coordinate-radar .radar-mark{fill:var(--lime);stroke:none}.coordinate-radar .radar-pulse{stroke:var(--lime);stroke-width:2;opacity:.55}.coordinate-radar strong{position:relative;display:grid;grid-template-columns:auto auto 1px auto auto;align-items:baseline;gap:4px;padding:7px 10px;border-radius:7px;background:#082226d9;font:400 20px Georgia,serif}.coordinate-radar strong small{font:700 8px Inter,sans-serif;color:#9eb0ad}.coordinate-radar strong i{width:1px;height:14px;background:#ffffff35}.radar-north{position:absolute;top:3px;font-size:8px;letter-spacing:.15em}.radar-caption{position:absolute;bottom:1px;color:#91a5a1;font-size:8px;text-transform:uppercase;letter-spacing:.12em}@media(max-width:720px){.coordinate-radar{display:none}} +.coordinate-radar{position:relative;flex:0 0 180px;width:180px;height:180px;display:grid;place-items:center;color:var(--lime)}.coordinate-radar svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:color-mix(in srgb,var(--text-on-dark) 13%,transparent);stroke-width:1}.coordinate-radar .radar-sweep{fill:color-mix(in srgb,var(--lime) 5%,transparent);stroke:color-mix(in srgb,var(--lime) 22%,transparent)}.coordinate-radar .radar-mark{fill:var(--lime);stroke:none}.coordinate-radar .radar-pulse{stroke:var(--lime);stroke-width:2;opacity:.55}.coordinate-radar strong{position:relative;display:grid;grid-template-columns:auto auto 1px auto auto;align-items:baseline;gap:4px;padding:7px 10px;border-radius:7px;background:color-mix(in srgb,var(--deep) 85%,transparent);font:400 20px Georgia,serif}.coordinate-radar strong small{font:700 8px Inter,sans-serif;color:color-mix(in srgb,var(--text-on-dark) 65%,transparent)}.coordinate-radar strong i{width:1px;height:14px;background:color-mix(in srgb,var(--text-on-dark) 22%,transparent)}.radar-north{position:absolute;top:3px;font-size:8px;letter-spacing:.15em}.radar-caption{position:absolute;bottom:1px;color:color-mix(in srgb,var(--text-on-dark) 60%,transparent);font-size:8px;text-transform:uppercase;letter-spacing:.12em}@media(max-width:720px){.coordinate-radar{display:none}} diff --git a/apps/web/src/styles/fish-silhouette.css b/apps/web/src/styles/fish-silhouette.css index d2a6df3..4791b8a 100644 --- a/apps/web/src/styles/fish-silhouette.css +++ b/apps/web/src/styles/fish-silhouette.css @@ -1 +1 @@ -.fish-silhouette{fill:currentColor;color:#315f63;pointer-events:none}.fish-silhouette .fish-eye{fill:var(--paper)}.fish-silhouette .fish-detail{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}.spot-main{position:relative}.spot-main>.fish-silhouette{position:absolute;right:2px;top:38px;opacity:.075}.catalog-grid>a{position:relative;overflow:hidden}.catalog-grid>a>.fish-silhouette{position:absolute;right:20px;bottom:12px;opacity:.12;transform:rotate(-4deg);transition:opacity var(--motion-base) var(--ease-out),transform var(--motion-base) var(--ease-out)}.catalog-grid>a:hover>.fish-silhouette{opacity:.2;transform:rotate(-2deg) translateX(-2px)}@media(max-width:720px){.spot-main>.fish-silhouette{width:68px;right:0;opacity:.055}} +.fish-silhouette{fill:currentColor;color:var(--decorative-water);pointer-events:none}.fish-silhouette .fish-eye{fill:var(--paper)}.fish-silhouette .fish-detail{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}.spot-main{position:relative}.spot-main>.fish-silhouette{position:absolute;right:2px;top:38px;opacity:.075}.catalog-grid>a{position:relative;overflow:hidden}.catalog-grid>a>.fish-silhouette{position:absolute;right:20px;bottom:12px;opacity:.12;transform:rotate(-4deg);transition:opacity var(--motion-base) var(--ease-out),transform var(--motion-base) var(--ease-out)}.catalog-grid>a:hover>.fish-silhouette{opacity:.2;transform:rotate(-2deg) translateX(-2px)}@media(max-width:720px){.spot-main>.fish-silhouette{width:68px;right:0;opacity:.055}} diff --git a/apps/web/src/styles/tackle-glyph.css b/apps/web/src/styles/tackle-glyph.css index d4b8efe..84d1728 100644 --- a/apps/web/src/styles/tackle-glyph.css +++ b/apps/web/src/styles/tackle-glyph.css @@ -1 +1 @@ -.tackle-glyph{--tackle-accent:#dd8b42;flex:none;color:var(--tackle-accent);stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.tackle-glyph[data-tone="1"]{--tackle-accent:#759d35}.tackle-glyph[data-tone="2"]{--tackle-accent:#3f8588}.tackle-glyph[data-tone="3"]{--tackle-accent:#9a6a9d}.tackle-glyph[data-tone="4"]{--tackle-accent:#bd6250}.tackle-glyph__fill{fill:color-mix(in srgb,currentColor 78%,var(--surface));stroke:currentColor}.tackle-glyph__stroke{stroke-width:3}.tackle-label{display:inline-flex;align-items:center;gap:8px;min-width:0}.tackle-label>span{min-width:0}.best-lure .tackle-glyph{color:var(--lime)}.bait-line>.tackle-glyph{width:25px;height:25px}.catch-list .tackle-label{margin-top:3px;color:var(--text-muted);font-size:12px}.detail-grid aside li .tackle-label{width:100%} +.tackle-glyph{--tackle-accent:var(--tackle-orange);flex:none;color:var(--tackle-accent);stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.tackle-glyph[data-tone="1"]{--tackle-accent:var(--tackle-green)}.tackle-glyph[data-tone="2"]{--tackle-accent:var(--tackle-teal)}.tackle-glyph[data-tone="3"]{--tackle-accent:var(--tackle-violet)}.tackle-glyph[data-tone="4"]{--tackle-accent:var(--tackle-red)}.tackle-glyph__fill{fill:color-mix(in srgb,currentColor 78%,var(--surface));stroke:currentColor}.tackle-glyph__stroke{stroke-width:3}.tackle-label{display:inline-flex;align-items:center;gap:8px;min-width:0}.tackle-label>span{min-width:0}.best-lure .tackle-glyph{color:var(--lime)}.bait-line>.tackle-glyph{width:25px;height:25px}.catch-list .tackle-label{margin-top:3px;color:var(--text-muted);font-size:12px}.detail-grid aside li .tackle-label{width:100%} diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css index 0f3f515..f50c0e8 100644 --- a/apps/web/src/styles/theme.css +++ b/apps/web/src/styles/theme.css @@ -44,6 +44,13 @@ --source-rf4map: light-dark(#2f704f, #79c79e); --source-rf4posts: light-dark(#984a33, #ee9a7e); --source-players: light-dark(#526b1c, #add066); + --graphic-line: light-dark(#3f7779, #79b5b8); + --graphic-grid: light-dark(#dce5de, #294244); + --tackle-orange: light-dark(#b96b27, #f0a55f); + --tackle-green: light-dark(#648b2b, #a8cf62); + --tackle-teal: light-dark(#34777a, #78bec1); + --tackle-violet: light-dark(#845787, #c795ca); + --tackle-red: light-dark(#a95041, #ed8e7c); } :root[data-theme="light"] { color-scheme: light; } @@ -158,6 +165,23 @@ .health-state i { background: var(--status-warning); } .source-health-grid [data-status="healthy"] .health-state i { background: var(--status-healthy); } .source-health-grid [data-status="source_changed"] .health-state i { background: var(--status-changed); } + .activity-timeline header p { color: var(--text-secondary); } + .timeline-line { background: var(--graphic-line); } + .timeline-line i { box-shadow: 0 0 0 4px color-mix(in srgb,var(--lime) 12%,transparent); } + .coordinate-radar svg { stroke: color-mix(in srgb,var(--text-on-dark) 13%,transparent); } + .coordinate-radar .radar-sweep { fill: color-mix(in srgb,var(--lime) 5%,transparent); stroke: color-mix(in srgb,var(--lime) 22%,transparent); } + .admin-dashboard-grid > section, .admin-kpis > a, .admin-kpis > article, + .source-health-grid article { background: var(--surface-elevated); border-color: var(--border-subtle); } + .admin-history li small, .source-health-grid p, .health-state { color: var(--text-secondary); } @media print { :root { color-scheme: light; } } + +@media (forced-colors: active) { + .theme-switcher, .theme-switcher button, .spot-card, .record-table, + .report-form, .moderation-card, .catalog-grid > a, .data-passport, + .source-chip, .notice, .state { border: 1px solid CanvasText; } + .theme-switcher button[aria-pressed="true"] { background: Highlight; color: HighlightText; } + .source-chip i, .health-state i, .quality-chip i, .activity-pill i { forced-color-adjust: none; } + .fish-silhouette, .waterbody-mark, .tackle-glyph, .coordinate-radar { color: CanvasText; } +} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 96b5807..5c7f471 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -44,7 +44,7 @@ - [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04. - [x] **D03 · Переключатель темы.** В header добавлен доступный трёхпозиционный выбор «Системная / Светлая / Тёмная»; на узких экранах он сохраняет три понятные иконки и доступные названия. Кнопки работают с клавиатуры, имеют видимый focus и синхронизируют `aria-pressed`. - [x] **D04 · Сохранение и SSR-согласование.** Выбор хранится год в allowlist-cookie `rf4-theme` с `SameSite=Lax` и `Secure` на HTTPS; Astro SSR выставляет `data-theme` до отрисовки, а системный режим оставляет выбор браузеру. Собранный Astro client script переключает тему без inline-кода, localStorage и ослабления CSP. -- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены уже переведены базовые поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния и часть декоративной графики. Следующий пакет: оставшиеся admin-поверхности, радар, timeline, силуэты и глифы; затем проверить фотографии, прозрачные PNG, screenshots, тени, градиенты и forced-colors. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. +- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены переведены базовые и admin-поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния, радар, timeline, силуэты и глифы; добавлен базовый forced-colors layer. Осталась визуальная проверка фотографий, прозрачных PNG, screenshots, теней и градиентов в обеих темах. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. - [ ] **D06 · Метаданные браузера и CSP.** Добавить парные `theme-color` для light/dark media queries, проверить manifest/PWA chrome и отсутствие новых inline script/style/attributes. Сохранить `inlinedScripts: []`, `style-src 'self'`, `style-src-attr 'none'` и не возвращать `unsafe-inline` ради инициализации темы. - [ ] **D07 · Визуальная и accessibility-приёмка.** Проверить light/dark/system на 320/390/768/1280 px для главной, каталогов, detail, records, report, status и всех admin-экранов; покрыть normal/hover/focus/disabled/error/loading/empty и длинные данные. Для обеих тем обеспечить WCAG AA, отсутствие горизонтального scroll и CLS, корректную печать, reduced motion и переключение без потери введённых данных; сохранить эталонные screenshots и краткий отчёт.