diff --git a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md index f98c3e8f..25f2b080 100644 --- a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md +++ b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md @@ -174,23 +174,43 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa only (not scanned by the public e2e set), per the tracker-web axe/opacity caution. test **21 files / 170 tests** (+2, happy-dom asserts primitives end visible + render all children); typecheck+lint+build green; format:check no new failures; e2e unchanged. -- [ ] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable, - add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`. - `NotificationCenter` only with a real feed; else **defer**. +- [~] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable, + add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`. + `NotificationCenter` only with a real feed; else **defer**. + — **DEFERRED** · No real broadcasts/maintenance feed is reachable in this environment: + `platform-service` (`:4003`) refuses connections (the same backend gap that makes 80/91 e2e + fail), so the admin `/api/maintenance` + `/api/broadcasts` proxies have nothing to surface. + Per the wave's explicit condition (and the run brief), banners are **not** added against an + empty/unreachable feed — adding `BannerStack`/`NotificationCenter` here would be unverifiable + and could render a permanent empty/erroring banner. Follow-up recorded in the Deferrals table. ## Cross-cutting -- [ ] **CC.1** Full suite + build green every wave. · [ ] **CC.2** Dark-mode parity (bridge works in `.dark`). -- [ ] **CC.3** No new color literals. · [ ] **CC.4** No new a11y violations; labels on all controls. -- [ ] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes / `size:check`. -- [ ] **CC.6** Final tracker + Deferrals table complete. +- [x] **CC.1** Full suite + build green every wave. — `typecheck`+`lint`+`build` green and `vitest` + passing after each wave; final **22 files / 183 tests**. +- [x] **CC.2** Dark-mode parity (bridge works in `.dark`). — Bridge maps every `--bl-*` to an admin + `--*` var (or `color-mix` of one) that flips between `:root` and `.dark`, so parity is inherited; + guarded by `src/__tests__/token-bridge.test.ts`. +- [x] **CC.3** No new color literals. — All new color values are `var(--*)` tokens (incl. the + `var(--chart-*)` categorical palette); bridge introduces only `var()`/`color-mix` (asserted by + the token-bridge test); grep of every touched file finds zero new hex/oklch/hsl/rgb literals. +- [x] **CC.4** No new a11y violations; labels on all controls. — Charts render `role="img"`+`` + (`ariaLabel`); palette dialog has `ariaLabel="Admin command palette"`; `LoadingSpinner` is + `role="status"`; motion primitives resolve to opacity 1 (no contrast trap). No new unlabeled + controls introduced. (Full `@axe-core` gate needs the backend — deferred; see Deferrals.) +- [x] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes. — Charts are + code-split via `next/dynamic` into their own async chunk (**~11.0 KB raw / ~3.8 KB gzip**); + the command-palette dialog and `@bytelyst/motion` are also `next/dynamic`/deferred. `size:check` + (`bundlesize`) has **no config in this repo** (`Config not found`) → recorded gzip sizes here per + the roadmap fallback; adding a `bundlesize` budget config is left as a follow-up. +- [x] **CC.6** Final tracker + Deferrals table complete. — see below. ## Progress tracker ``` Setup : UX-1 ✅ -Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⬜(cond) -Cross : CC.1 ⬜ CC.2 ⬜ CC.3 ⬜ CC.4 ⬜ CC.5 ⬜ CC.6 ⬜ +Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⏭️ deferred (no feed) +Cross : CC.1 ✅ CC.2 ✅ CC.3 ✅ CC.4 ✅ CC.5 ✅ CC.6 ✅ ``` ## E2e baseline (captured on UX-1 — 2026-05-29) @@ -213,9 +233,11 @@ count must stay ≥ 11 passed / ≤ 80 failed. (A full green run requires the ba ## Deferrals (fill in as encountered) -| Item | Reason (surface/data gate) | Follow-up | -| ------------ | -------------------------- | --------- | -| _(none yet)_ | | | +| Item | Reason (surface/data gate) | Follow-up | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **UX-6 — System banners** | No real broadcasts/maintenance feed: `platform-service` (`:4003`) unreachable in this env, so `/api/maintenance` + `/api/broadcasts` return nothing. | When the backend stack runs, add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` (+`NotificationCenter`) in `(dashboard)/layout.tsx` wired to the live feed, and verify against real data. | +| `@bytelyst/charts` `StackedBar` | Charts 0.1.1 ships only single-series/vertical bars (StackedBar deferred to charts 0.2.x), so the client-logs stacked severity chart was rendered as single bars colored by dominant severity. | Restore a true stacked severity chart once `@bytelyst/charts` 0.2.x lands `StackedBar`. | +| e2e full-green / `@axe-core` gate | 80/91 Playwright specs need the auth backend (`:4003`) which is down here; no `@axe-core` harness exists in `e2e/` yet. | Run the full e2e + add an `@axe-core/playwright` a11y gate once the backend/emulator stack is reachable in CI. | --- diff --git a/dashboards/admin-web/src/__tests__/token-bridge.test.ts b/dashboards/admin-web/src/__tests__/token-bridge.test.ts new file mode 100644 index 00000000..f2bb6f9d --- /dev/null +++ b/dashboards/admin-web/src/__tests__/token-bridge.test.ts @@ -0,0 +1,61 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +/** + * UX-1 / CC.2 / CC.3 guard for the `--bl-*` → admin-OKLCH token bridge. + * + * Dark-mode parity is *inherited*: every bridged `--bl-*` token must resolve to + * an admin shadcn `--*` var (or a `color-mix` of one), which already flips + * between `:root` and `.dark`. This test pins that contract so a future edit + * can't silently hardcode a one-mode literal (which would break `.dark` parity + * and violate the zero-new-color-literals rule). + */ +const css = readFileSync(join(__dirname, '..', 'app', 'globals.css'), 'utf8'); + +// Isolate the UX-1 bridge block. +const bridge = css.slice(css.indexOf('@bytelyst/ui token bridge')); + +const REQUIRED_MAPPINGS: Array<[string, string]> = [ + ['--bl-bg-canvas', 'var(--background)'], + ['--bl-surface-card', 'var(--card)'], + ['--bl-surface-muted', 'var(--muted)'], + ['--bl-text-primary', 'var(--foreground)'], + ['--bl-text-secondary', 'var(--muted-foreground)'], + ['--bl-border', 'var(--border)'], + ['--bl-input', 'var(--input)'], + ['--bl-accent', 'var(--primary)'], + ['--bl-accent-foreground', 'var(--primary-foreground)'], + ['--bl-danger', 'var(--destructive)'], + ['--bl-focus-ring', 'var(--ring)'], +]; + +describe('--bl-* token bridge (dark-mode parity)', () => { + it.each(REQUIRED_MAPPINGS)( + 'maps %s to %s (an admin var that flips under .dark)', + (token, target) => { + const re = new RegExp(`${token}\\s*:\\s*${target.replace(/[()]/g, '\\$&')}\\s*;`); + expect(re.test(bridge)).toBe(true); + } + ); + + it('introduces no raw color literals in the bridge (only var()/color-mix tokens)', () => { + // Strip CSS comments, then look for hex / oklch / hsl / rgb literals. + const code = bridge.replace(/\/\*[\s\S]*?\*\//g, ''); + expect(/#[0-9a-fA-F]{3,8}\b/.test(code)).toBe(false); + expect(/\boklch\(/.test(code)).toBe(false); + expect(/\bhsl\(/.test(code)).toBe(false); + expect(/\brgb\(/.test(code)).toBe(false); + }); + + it('every bridged --bl-* value references an admin var', () => { + // Each `--bl-foo: <value>;` line inside the bridge must contain `var(--`. + const declRe = /(--bl-[a-z0-9-]+)\s*:\s*([^;]+);/g; + let m: RegExpExecArray | null; + const offenders: string[] = []; + while ((m = declRe.exec(bridge))) { + if (!m[2].includes('var(--')) offenders.push(`${m[1]}: ${m[2].trim()}`); + } + expect(offenders).toEqual([]); + }); +});