From 4f5fb6d7c2af1b67f2627f7f8a1b858ee4d94bfc Mon Sep 17 00:00:00 2001 From: IK Date: Sun, 13 Sep 2026 15:52:03 +0700 Subject: [PATCH] feat: theme atlas and state components --- README.md | 2 +- .../web/src/components/AtlasBreadcrumbs.astro | 10 ++++---- apps/web/src/components/AtlasEntityLink.astro | 4 ++-- apps/web/src/styles/data-passport.css | 2 +- apps/web/src/styles/loading-states.css | 2 +- apps/web/src/styles/theme.css | 24 ++++++++++++++++++- docs/ROADMAP.md | 2 +- 7 files changed, 34 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a929447..91b008e 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. Семантические роли и ограничения дальнейшей миграции компонентов описаны в [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-артефакты. diff --git a/apps/web/src/components/AtlasBreadcrumbs.astro b/apps/web/src/components/AtlasBreadcrumbs.astro index a6a2061..7aeb7ff 100644 --- a/apps/web/src/components/AtlasBreadcrumbs.astro +++ b/apps/web/src/components/AtlasBreadcrumbs.astro @@ -19,10 +19,10 @@ const { items } = Astro.props as { items: Item[] }; ol::-webkit-scrollbar { display: none; } 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:hover { color: var(--deep); text-decoration: underline; text-underline-offset: 4px; } - li > span[aria-current] { color: var(--deep); font-weight: 750; } - .atlas-breadcrumbs__knot { width: 7px; height: 7px; flex: 0 0 auto; border: 1px solid #78918a; 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; } - .atlas-breadcrumbs__line { width: clamp(18px, 3vw, 42px); height: 1px; background: repeating-linear-gradient(90deg,#9aaca5 0 5px,transparent 5px 8px); } + a:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: 4px; } + 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 var(--border-strong); border-radius: 50%; background: var(--paper); } + 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,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; } } diff --git a/apps/web/src/components/AtlasEntityLink.astro b/apps/web/src/components/AtlasEntityLink.astro index d9fab7b..c6ec4bc 100644 --- a/apps/web/src/components/AtlasEntityLink.astro +++ b/apps/web/src/components/AtlasEntityLink.astro @@ -9,8 +9,8 @@ const { href, label, kind, identity } = Astro.props as { href: string; label: st diff --git a/apps/web/src/styles/data-passport.css b/apps/web/src/styles/data-passport.css index 333f0ea..77fb9bc 100644 --- a/apps/web/src/styles/data-passport.css +++ b/apps/web/src/styles/data-passport.css @@ -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 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)} .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}} diff --git a/apps/web/src/styles/loading-states.css b/apps/web/src/styles/loading-states.css index f026feb..d511e1f 100644 --- a/apps/web/src/styles/loading-states.css +++ b/apps/web/src/styles/loading-states.css @@ -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)}} diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css index 66b07db..6961fa6 100644 --- a/apps/web/src/styles/theme.css +++ b/apps/web/src/styles/theme.css @@ -29,6 +29,12 @@ --warning-soft: light-dark(#fff0c7, #46391d); --danger: light-dark(#842f25, #ffafa3); --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; } @@ -63,7 +69,7 @@ .alpha-banner strong { color: var(--text-primary); } .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, .moderation-actions textarea { background: var(--surface-control); color: var(--text-primary); border-color: var(--border-strong); } .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-slot strong { color: var(--text-primary); } .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 { :root { color-scheme: light; } } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0af58fe..7872c79 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 · Темизация компонентов и графики.** Перевести 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` ради инициализации темы. - [ ] **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 и краткий отчёт.