diff --git a/README.md b/README.md index e2b12ab..c3345bc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ 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` без JavaScript, сохранённого состояния и вспышки неверной палитры. Семантические роли и ограничения миграции описаны в [dark-theme.md](docs/dark-theme.md); ручной выбор и SSR-cookie запланированы следующими этапами D03–D04. + Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты. Актуальная инвентаризация источников и правила подключения адаптеров находятся в [`docs/data-source-audit.md`](docs/data-source-audit.md). Разрешённый технический пилот RF4DB/RF4-STAT описан в [`docs/community-source-pilot.md`](docs/community-source-pilot.md), а статус разрешений и лимитов — в [`docs/data-permissions.md`](docs/data-permissions.md). Все наблюдения сначала попадают в staging. Полные записи с уже подтверждёнными алиасами могут публиковаться автоматически; новые соответствия требуют модерации, а неполные записи показываются отдельно и не влияют на индекс. diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro index c910741..35c80ea 100644 --- a/apps/web/src/layouts/Layout.astro +++ b/apps/web/src/layouts/Layout.astro @@ -13,6 +13,7 @@ import "../styles/alpha-banner.css"; import "../styles/signal-pagination.css"; import "../styles/dashboard-polish.css"; import "../styles/loading-states.css"; +import "../styles/theme.css"; import FishingIcon from "../components/FishingIcon.astro"; import AlphaBanner from "../components/AlphaBanner.astro"; const { diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css new file mode 100644 index 0000000..3ef778d --- /dev/null +++ b/apps/web/src/styles/theme.css @@ -0,0 +1,108 @@ +:root { + color-scheme: light dark; + --canvas: #f2f5ee; + --surface-elevated: #ffffff; + --surface-raised: #fbfcf9; + --surface-control: #f3f6f1; + --text-primary: #092226; + --text-secondary: #526662; + --text-tertiary: #647572; + --text-on-dark: #f5f8f3; + --border-strong: #cbd6ce; + --border-subtle: #d6dfd7; + --shadow-color: #14333812; +} + +@media (prefers-color-scheme: dark) { + :root { + --canvas: #071719; + --surface-elevated: #0d2325; + --surface-raised: #10282a; + --surface-control: #0a1d1f; + --text-primary: #edf5ef; + --text-secondary: #b8c7c3; + --text-tertiary: #93a7a2; + --text-on-dark: #f4f8f5; + --border-strong: #3a5658; + --border-subtle: #294244; + --shadow-color: #00000052; + --paper: var(--canvas); + --surface: var(--surface-elevated); + --surface-soft: var(--surface-raised); + --surface-field: var(--surface-control); + --ink: var(--text-primary); + --text: var(--text-primary); + --text-muted: var(--text-secondary); + --text-subtle: var(--text-tertiary); + --line: var(--border-strong); + --border: var(--border-strong); + --border-soft: var(--border-subtle); + --focus: #c9f45b; + --success: #c9ec8a; + --success-soft: #243e25; + --warning: #f3ce7a; + --warning-soft: #46391d; + --danger: #ffafa3; + --danger-soft: #4b2928; + } + + body { background: var(--canvas); color: var(--text-primary); } + .brand-name span, .topbar nav a, .live-badge, .eyebrow, .overline, + .intro-copy > p, .active-filters a, .result-count, .spot-rank, + .spot-topline, .spot-meta, .bait-line div span, .spot-stats span, + .data-note, .principles p, .principles article > span, .official-note, + .privacy, .field-help, .record-row > span, .record-row > time, + .report-form label, .screenshot-retry label, .back, .note, + .moderation-summary > p, .moderation-summary dt, + .moderation-provenance summary, .legal-page p, .legal-page li { + color: var(--text-secondary); + } + + .topbar nav a.active, .eyebrow b, .state h2, .admin-kpis strong, + .data-passport, .data-passport dd, .legal-page h1, .legal-page h2 { + color: var(--text-primary); + } + + .active-filters span, .spot-card, .record-table, .report-form, + .periods, .detail-grid aside, .screenshot-retry, .moderation-card, + .admin-kpis > a, .admin-kpis > article, .admin-dashboard-grid > section, + .source-health-grid article, .state, .load-more, .signal-more { + background: var(--surface-elevated); + border-color: var(--border-subtle); + } + + .alpha-banner { background: linear-gradient(90deg, #132a26, #102426); color: var(--text-secondary); border-color: var(--border-strong); } + .alpha-banner strong { color: var(--text-primary); } + .alpha-banner__cta { background: var(--lime); color: #082226; } + + .spot-card:hover { background: var(--surface-raised); border-color: #6f8988; box-shadow: 0 18px 40px var(--shadow-color); } + .report-form input, .report-form textarea, .report-form select, + .moderation-actions textarea { background: var(--surface-control); color: var(--text-primary); border-color: var(--border-strong); } + .record-head, .no-proof { background: var(--surface-raised); } + .record-row, .periods > div, .catch-list article, .detail-grid aside li, + .bait-line, .spot-stats, .moderation-summary dl div, + .moderation-actions, .how-it-works, .signal-heading { + border-color: var(--border-subtle); + } + + .signal-card { background: linear-gradient(145deg, #20291f, #151f20); border-color: #756b3d; } + .signal-card > p, .signal-card dd, .signal-heading p { color: var(--text-secondary); } + .signal-card dt { color: #b9ae82; } + .signal-card .missing-note { background: #453c20; color: #f0d889; } + .signal-card .data-passport { background: #172628; border-color: #59605a; } + .source-chip { background: color-mix(in srgb, var(--chip) 24%, var(--surface-elevated)); } + .notice.success { background: var(--success-soft); color: var(--success); } + .notice.warning, .data-quality { background: var(--warning-soft); color: var(--warning); } + .notice.error { background: var(--danger-soft); color: var(--danger); } + .moderation-summary blockquote { background: var(--surface-raised); color: var(--text-secondary); } + .moderation-actions .reject { background: var(--danger-soft); color: var(--danger); } + .moderation-actions .delete { color: var(--danger); border-color: #82504c; } + .activity-timeline { background: var(--surface-elevated); border-color: var(--border-subtle); } + .timeline-chart { border-color: var(--border-strong); background: repeating-linear-gradient(to top, transparent 0, transparent 35px, #294244 36px); } + .timeline-slot strong { color: var(--text-primary); } + .timeline-slot small, .activity-legend { color: var(--text-tertiary); } +} + +@media print { + :root { color-scheme: light; } +} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index bf19f62..88fb1b8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -40,8 +40,8 @@ Реализовывать последовательно: сначала семантическая палитра и системный режим, затем ручное управление и полировка компонентов. Тёмная тема должна сохранять полевую эстетику RF4 Spotter, а не быть механической инверсией светлой. -- [ ] **D01 · Семантическая палитра и color-scheme.** Провести инвентаризацию жёстко заданных цветов во всех CSS-файлах и SVG-компонентах; дополнить токены ролями canvas/surface/elevated/text/border/interactive/status/source для light и dark. Задать `color-scheme: light dark`, проверить нативные form controls и сохранить лаймовый акцент без свечения и кислотного контраста. -- [ ] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme`; серверный HTML и CSS сразу отдают правильную тему без inline bootstrap script. До появления ручного выбора не хранить лишнее состояние и не ослаблять CSP. +- [x] **D01 · Семантическая палитра и color-scheme.** Жёсткие цвета и конфликтующая роль исторического `--deep` проинвентаризированы; добавлены независимые canvas/surface/elevated/control, text, border, shadow и контрастные status-роли для light/dark. Базовые public/admin поверхности получили первый dark-layer, нативные controls используют `color-scheme: light dark`; стратегия миграции зафиксирована в [dark-theme.md](dark-theme.md). +- [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04. - [ ] **D03 · Переключатель темы.** Добавить доступный трёхпозиционный выбор «Системная / Светлая / Тёмная» в header и компактный вариант для мобильной навигации. Управление должно иметь текстовое accessible name, видимый focus, корректные `aria-pressed`/состояние и работать с клавиатуры. - [ ] **D04 · Сохранение и SSR-согласование.** Хранить явный выбор в cookie с `SameSite=Lax`, безопасным сроком жизни и значениями allowlist; Astro SSR читает cookie и выставляет `data-theme` на ``. Внешний client module мгновенно переключает тему и синхронизирует изменение системной схемы только в режиме «Системная», без localStorage-only flash и без inline-кода. - [ ] **D05 · Темизация компонентов и графики.** Перевести header/footer, hero, карточки активности, таблицы, формы, provenance/quality badges, состояния, admin-панель, радар, timeline, силуэты, глифы и декоративные водные мотивы на семантические токены. Отдельно проверить фотографии, прозрачные PNG, screenshots, тени, градиенты и режим forced-colors; источники и статусы не должны различаться только цветом. diff --git a/docs/dark-theme.md b/docs/dark-theme.md new file mode 100644 index 0000000..ca905ae --- /dev/null +++ b/docs/dark-theme.md @@ -0,0 +1,17 @@ +# Тёмная тема: палитра и ограничения + +Базовая ревизия выполнена 13 сентября 2026 года. Старый CSS содержит много жёстких цветов, а исторический `--deep` используется одновременно как фирменный тёмный фон и как текст на светлых карточках. Поэтому менять его значение для dark mode нельзя: роли разделяются постепенно без визуальной инверсии бренда. + +## Семантические роли + +- `--canvas` — фон страницы; +- `--surface-elevated`, `--surface-raised`, `--surface-control` — карточки, hover и поля; +- `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-on-dark` — иерархия текста; +- `--border-strong`, `--border-subtle`, `--shadow-color` — разделители и глубина; +- существующие status/source-токены сохраняют смысл, но получают контрастные dark-значения там, где используются как текст. + +Первый этап следует `prefers-color-scheme` только через CSS и поэтому не создаёт вспышку неверной темы, не записывает состояние и не требует inline script. Явный выбор появится на этапе D03–D04 и будет согласован с SSR через allowlist-cookie. + +## Дальнейшая миграция + +Файл `theme.css` временно содержит точечные dark overrides для основных публичных и административных поверхностей. На этапе D05 жёсткие цвета последовательно заменяются семантическими токенами в исходных component styles; source/status различия сохраняют текст или форму и никогда не полагаются только на оттенок.