feat: theme atlas and state components
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-13 15:52:03 +07:00
parent 703a0ba32f
commit 4f5fb6d7c2
7 changed files with 34 additions and 12 deletions
+1 -1
View File
@@ -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). 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. Семантические роли и ограничения дальнейшей миграции компонентов описаны в [dark-theme.md](docs/dark-theme.md). Тема по умолчанию следует системному `prefers-color-scheme`, а переключатель в header позволяет выбрать системную, светлую или тёмную палитру. Выбор сохраняется в cookie и применяется Astro при SSR без localStorage-only flash и ослабления CSP. Базовые поверхности, каталог, навигационная леска, паспорта данных и состояния загрузки уже используют семантические light/dark-токены; ограничения дальнейшей миграции описаны в [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-артефакты. Gitea Actions workflow `.gitea/workflows/ci.yml` на каждый push и pull request проверяет Python, миграции на чистой PostgreSQL, Astro build и полный Compose/Playwright-сценарий. При падении E2E сохраняются логи контейнеров и Playwright-артефакты.
@@ -19,10 +19,10 @@ const { items } = Astro.props as { items: Item[] };
ol::-webkit-scrollbar { display: none; } ol::-webkit-scrollbar { display: none; }
li { display: flex; align-items: center; flex: 0 0 auto; color: var(--text-muted); font-size: 12px; } li { display: flex; align-items: center; flex: 0 0 auto; color: var(--text-muted); font-size: 12px; }
a, li > span[aria-current] { padding: 8px 9px; color: inherit; text-decoration: none; white-space: nowrap; } a, li > span[aria-current] { padding: 8px 9px; color: inherit; text-decoration: none; white-space: nowrap; }
a:hover { color: var(--deep); text-decoration: underline; text-underline-offset: 4px; } a:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: 4px; }
li > span[aria-current] { color: var(--deep); font-weight: 750; } li > span[aria-current] { color: var(--text-primary); font-weight: 750; }
.atlas-breadcrumbs__knot { width: 7px; height: 7px; flex: 0 0 auto; border: 1px solid #78918a; border-radius: 50%; background: var(--paper); } .atlas-breadcrumbs__knot { width: 7px; height: 7px; flex: 0 0 auto; border: 1px solid var(--border-strong); border-radius: 50%; background: var(--paper); }
li:last-child .atlas-breadcrumbs__knot { border-color: #6f922d; background: var(--lime); box-shadow: 0 0 0 4px #c9f45b26; } li:last-child .atlas-breadcrumbs__knot { border-color: var(--accent-muted); background: var(--lime); box-shadow: 0 0 0 4px color-mix(in srgb,var(--lime) 18%,transparent); }
.atlas-breadcrumbs__line { width: clamp(18px, 3vw, 42px); height: 1px; background: repeating-linear-gradient(90deg,#9aaca5 0 5px,transparent 5px 8px); } .atlas-breadcrumbs__line { width: clamp(18px, 3vw, 42px); height: 1px; background: repeating-linear-gradient(90deg,var(--border-strong) 0 5px,transparent 5px 8px); }
@media (max-width: 720px) { .atlas-breadcrumbs { width: calc(100% - 28px); padding-top: 22px; } a, li > span[aria-current] { padding-inline: 7px; } } @media (max-width: 720px) { .atlas-breadcrumbs { width: calc(100% - 28px); padding-top: 22px; } a, li > span[aria-current] { padding-inline: 7px; } }
</style> </style>
@@ -9,8 +9,8 @@ const { href, label, kind, identity } = Astro.props as { href: string; label: st
</a> </a>
<style> <style>
.atlas-entity-link { display:grid;grid-template-columns:52px minmax(0,1fr) auto;align-items:center;gap:9px;min-height:54px;padding:5px 2px;border-bottom:1px solid var(--border-soft);color:var(--text-muted);font-size:13px;text-decoration:none } .atlas-entity-link { display:grid;grid-template-columns:52px minmax(0,1fr) auto;align-items:center;gap:9px;min-height:54px;padding:5px 2px;border-bottom:1px solid var(--border-soft);color:var(--text-muted);font-size:13px;text-decoration:none }
.atlas-entity-link__mark { width:48px;height:38px;display:grid;place-items:center;color:#315f63 } .atlas-entity-link__mark { width:48px;height:38px;display:grid;place-items:center;color:var(--decorative-water) }
.atlas-entity-link__mark :global(.fish-silhouette),.atlas-entity-link__mark :global(.waterbody-mark) { position:static;opacity:.68 } .atlas-entity-link__mark :global(.fish-silhouette),.atlas-entity-link__mark :global(.waterbody-mark) { position:static;opacity:.68 }
.atlas-entity-link__mark :global(.waterbody-mark) { stroke:currentColor }.atlas-entity-link__mark :global(.waterbody-mark text) { fill:currentColor } .atlas-entity-link__mark :global(.waterbody-mark) { stroke:currentColor }.atlas-entity-link__mark :global(.waterbody-mark text) { fill:currentColor }
b { color:#6d8d2a;font-size:15px;transition:transform var(--motion-fast) var(--ease-out) }.atlas-entity-link:hover { color:var(--deep) }.atlas-entity-link:hover b { transform:translateX(2px) } b { color:var(--accent-muted);font-size:15px;transition:transform var(--motion-fast) var(--ease-out) }.atlas-entity-link:hover { color:var(--text-primary) }.atlas-entity-link:hover b { transform:translateX(2px) }
</style> </style>
+1 -1
View File
@@ -1,5 +1,5 @@
.data-passport{margin-top:18px;padding:14px;border:1px solid var(--border);border-radius:13px;background:var(--surface-soft);color:var(--deep)} .data-passport{margin-top:18px;padding:14px;border:1px solid var(--border);border-radius:13px;background:var(--surface-soft);color:var(--deep)}
.data-passport header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}.data-passport header>span{font:italic 15px Georgia,serif}.data-passport header strong{padding:4px 8px;border-radius:999px;background:var(--success-soft);color:var(--success);font-size:9px;text-transform:uppercase;letter-spacing:.06em}.data-passport header strong[data-passport-status="unverified"]{background:#e8e2f4;color:#59438d}.data-passport header strong[data-passport-status="incomplete"]{background:var(--warning-soft);color:var(--warning)} .data-passport header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}.data-passport header>span{font:italic 15px Georgia,serif}.data-passport header strong{padding:4px 8px;border-radius:999px;background:var(--success-soft);color:var(--success);font-size:9px;text-transform:uppercase;letter-spacing:.06em}.data-passport header strong[data-passport-status="unverified"]{background:var(--info-soft);color:var(--info)}.data-passport header strong[data-passport-status="incomplete"]{background:var(--warning-soft);color:var(--warning)}
.data-passport__sources{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}.data-passport dl{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:0}.data-passport dl div{padding-top:8px;border-top:1px solid var(--border-soft)}.data-passport dt{font-size:8px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-subtle)}.data-passport dd{margin:3px 0 0;font:400 12px Georgia,serif;color:var(--deep)} .data-passport__sources{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}.data-passport dl{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:0}.data-passport dl div{padding-top:8px;border-top:1px solid var(--border-soft)}.data-passport dt{font-size:8px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-subtle)}.data-passport dd{margin:3px 0 0;font:400 12px Georgia,serif;color:var(--deep)}
.detail-card .data-passport,.detail-grid aside .data-passport{border-color:#ffffff20;background:#ffffff08;color:#fff}.detail-card .data-passport dd,.detail-grid aside .data-passport dd{color:#fff}.detail-card .data-passport dl div,.detail-grid aside .data-passport dl div{border-color:#ffffff1c}.detail-card .data-passport dt,.detail-grid aside .data-passport dt{color:#a9b8b5}.signal-card .data-passport{position:relative;z-index:1;margin-top:15px;background:#fffdf7;border-color:#ddd3af} .detail-card .data-passport,.detail-grid aside .data-passport{border-color:#ffffff20;background:#ffffff08;color:#fff}.detail-card .data-passport dd,.detail-grid aside .data-passport dd{color:#fff}.detail-card .data-passport dl div,.detail-grid aside .data-passport dl div{border-color:#ffffff1c}.detail-card .data-passport dt,.detail-grid aside .data-passport dt{color:#a9b8b5}.signal-card .data-passport{position:relative;z-index:1;margin-top:15px;background:#fffdf7;border-color:#ddd3af}
@media(max-width:420px){.data-passport dl{grid-template-columns:1fr}.data-passport dl div{display:flex;justify-content:space-between;gap:10px}} @media(max-width:420px){.data-passport dl{grid-template-columns:1fr}.data-passport dl div{display:flex;justify-content:space-between;gap:10px}}
+1 -1
View File
@@ -1 +1 @@
.loading-grid{display:grid;gap:18px}.loading-card{position:relative;overflow:hidden;min-height:210px;padding:25px;border:1px solid var(--border-soft);border-radius:16px;background:var(--surface)}.loading-card:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(100deg,transparent 20%,var(--surface-soft) 48%,transparent 76%);animation:loading-sweep calc(var(--motion-signal) - 1000ms) ease-in-out infinite}.loading-line{display:block;width:100%;height:13px;margin-top:18px;border-radius:999px;background:#e7ede7}.loading-line--label{width:22%;height:9px;margin-top:0;background:#d8e2d8}.loading-line--title{width:48%;height:30px;margin-top:24px}.loading-line--short{width:64%}.moderation-list[aria-busy="true"]{cursor:progress}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}@keyframes loading-sweep{to{transform:translateX(100%)}}@media(prefers-reduced-motion:reduce){.loading-card:after{animation:none;background:#f7faf533}} .loading-grid{display:grid;gap:18px}.loading-card{position:relative;overflow:hidden;min-height:210px;padding:25px;border:1px solid var(--border-soft);border-radius:16px;background:var(--surface)}.loading-card:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(100deg,transparent 20%,var(--surface-soft) 48%,transparent 76%);animation:loading-sweep calc(var(--motion-signal) - 1000ms) ease-in-out infinite}.loading-line{display:block;width:100%;height:13px;margin-top:18px;border-radius:999px;background:var(--skeleton)}.loading-line--label{width:22%;height:9px;margin-top:0;background:var(--skeleton-strong)}.loading-line--title{width:48%;height:30px;margin-top:24px}.loading-line--short{width:64%}.moderation-list[aria-busy="true"]{cursor:progress}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}@keyframes loading-sweep{to{transform:translateX(100%)}}@media(prefers-reduced-motion:reduce){.loading-card:after{animation:none;background:color-mix(in srgb,var(--surface-soft) 35%,transparent)}}
+23 -1
View File
@@ -29,6 +29,12 @@
--warning-soft: light-dark(#fff0c7, #46391d); --warning-soft: light-dark(#fff0c7, #46391d);
--danger: light-dark(#842f25, #ffafa3); --danger: light-dark(#842f25, #ffafa3);
--danger-soft: light-dark(#f5d8d2, #4b2928); --danger-soft: light-dark(#f5d8d2, #4b2928);
--info: light-dark(#59438d, #c9baf2);
--info-soft: light-dark(#e8e2f4, #302844);
--accent-muted: light-dark(#6d8d2a, #c0e562);
--decorative-water: light-dark(#315f63, #72a9ad);
--skeleton: light-dark(#e7ede7, #1c3537);
--skeleton-strong: light-dark(#d8e2d8, #294548);
} }
:root[data-theme="light"] { color-scheme: light; } :root[data-theme="light"] { color-scheme: light; }
@@ -63,7 +69,7 @@
.alpha-banner strong { color: var(--text-primary); } .alpha-banner strong { color: var(--text-primary); }
.alpha-banner__cta { background: light-dark(var(--deep), var(--lime)); color: light-dark(#fff, #082226); } .alpha-banner__cta { background: light-dark(var(--deep), var(--lime)); color: light-dark(#fff, #082226); }
.spot-card:hover { background: var(--surface-raised); border-color: #6f8988; box-shadow: 0 18px 40px var(--shadow-color); } .spot-card:hover { background: var(--surface-raised); border-color: var(--border-strong); box-shadow: 0 18px 40px var(--shadow-color); }
.report-form input, .report-form textarea, .report-form select, .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); } .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-head, .no-proof { background: var(--surface-raised); }
@@ -98,6 +104,22 @@
.timeline-chart { border-color: var(--border-strong); background: repeating-linear-gradient(to top, transparent 0, transparent 35px, var(--border-subtle) 36px); } .timeline-chart { border-color: var(--border-strong); background: repeating-linear-gradient(to top, transparent 0, transparent 35px, var(--border-subtle) 36px); }
.timeline-slot strong { color: var(--text-primary); } .timeline-slot strong { color: var(--text-primary); }
.timeline-slot small, .activity-legend { color: var(--text-tertiary); } .timeline-slot small, .activity-legend { color: var(--text-tertiary); }
.catalog-hero { border-color: var(--border-strong); }
.catalog-hero p, .catalog-results aside a, .catalog-grid span { color: var(--text-secondary); }
.catalog-results aside { background: var(--surface-elevated); border-color: var(--border-subtle); }
.catalog-results aside a { border-color: var(--border-subtle); }
.catalog-grid > a { background: linear-gradient(145deg,var(--surface-elevated),var(--surface-raised)); border-color: var(--border-subtle); }
.catalog-grid > a:hover { border-color: var(--border-strong); box-shadow: 0 14px 30px var(--shadow-color); }
.catalog-grid i, .catalog-grid > a > i b { color: var(--accent-muted); }
.waterbody-mark, .fish-silhouette { color: var(--decorative-water); stroke: currentColor; }
.signal-more, .load-more { color: var(--text-secondary); background: var(--surface-elevated); border-color: var(--border-strong); box-shadow: 0 8px 22px var(--shadow-color); }
.signal-more span, .load-more span { color: var(--text-secondary); }
.signal-more:hover, .load-more:hover { background: var(--surface-raised); border-color: var(--border-strong); }
.state { color: var(--text-secondary); border-color: var(--border-strong); }
.state::before { border-color: var(--border-strong); }
.state::after { border-color: var(--decorative-water); box-shadow: 0 -7px 0 -6px var(--decorative-water),0 7px 0 -6px var(--decorative-water); }
.data-passport header strong[data-passport-status="unverified"] { background: var(--info-soft); color: var(--info); }
@media print { @media print {
:root { color-scheme: light; } :root { color-scheme: light; }
} }
+1 -1
View File
@@ -44,7 +44,7 @@
- [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04. - [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04.
- [x] **D03 · Переключатель темы.** В header добавлен доступный трёхпозиционный выбор «Системная / Светлая / Тёмная»; на узких экранах он сохраняет три понятные иконки и доступные названия. Кнопки работают с клавиатуры, имеют видимый focus и синхронизируют `aria-pressed`. - [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. - [x] **D04 · Сохранение и SSR-согласование.** Выбор хранится год в allowlist-cookie `rf4-theme` с `SameSite=Lax` и `Secure` на HTTPS; Astro SSR выставляет `data-theme` до отрисовки, а системный режим оставляет выбор браузеру. Собранный Astro client script переключает тему без inline-кода, localStorage и ослабления CSP.
- [ ] **D05 · Темизация компонентов и графики.** Перевести header/footer, hero, карточки активности, таблицы, формы, provenance/quality badges, состояния, admin-панель, радар, timeline, силуэты, глифы и декоративные водные мотивы на семантические токены. Отдельно проверить фотографии, прозрачные PNG, screenshots, тени, градиенты и режим forced-colors; источники и статусы не должны различаться только цветом. - [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены уже переведены базовые поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, паспорта данных, pagination, empty/loading-состояния и часть декоративной графики. Следующий пакет: таблицы и формы, provenance/quality badges, admin-панель, радар, timeline, силуэты и глифы; затем проверить фотографии, прозрачные PNG, screenshots, тени, градиенты и forced-colors. Источники и статусы не должны различаться только цветом.
- [ ] **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` ради инициализации темы. - [ ] **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 и краткий отчёт. - [ ] **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 и краткий отчёт.