diff --git a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md index 9049d3fa..58bb81af 100644 --- a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md +++ b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md @@ -163,9 +163,17 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa regress them (additive rule). dashboard-components reads `--color-*` (admin `@theme inline`), so themes correctly. test **20 files / 168 tests** (+3, happy-dom render of error/not-found/ loading chrome); typecheck+lint+build green; format:check no new failures; e2e unchanged. -- [ ] **UX-5 — Motion:** add `@bytelyst/motion`; subtle `Reveal`/`Stagger` on the overview dashboard +- [x] **UX-5 — Motion:** add `@bytelyst/motion`; subtle `Reveal`/`Stagger` on the overview dashboard cards + key tables; respect `prefers-reduced-motion`. (Note tracker-web's lesson: do NOT apply motion to surfaces an offline axe gate scans synchronously if transient opacity trips contrast.) + — **DONE** `` · `@bytelyst/motion` added `workspace:*` (importer-only lockfile change, + `--frozen-lockfile` clean). Dashboard overview only: KPI cards grid → `StaggerList` + (from="up", 50ms), bottom Model-Usage/Recent-Users tables → `Reveal`. Primitives honor + `prefers-reduced-motion` and resolve to **opacity 1** (no element stranded transparent → no + contrast/a11y regression; SSR-safe `prefersReducedMotion`). Applied to the auth-gated dashboard + 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**. @@ -181,7 +189,7 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa ``` Setup : UX-1 ✅ -Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ⬜ UX-6 ⬜(cond) +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 ⬜ ``` diff --git a/dashboards/admin-web/package.json b/dashboards/admin-web/package.json index ddf9218d..272ae1dc 100644 --- a/dashboards/admin-web/package.json +++ b/dashboards/admin-web/package.json @@ -38,6 +38,7 @@ "@bytelyst/errors": "workspace:*", "@bytelyst/extraction": "workspace:*", "@bytelyst/logger": "workspace:*", + "@bytelyst/motion": "workspace:*", "@bytelyst/react-auth": "workspace:*", "@bytelyst/telemetry-client": "workspace:*", "@bytelyst/ui": "workspace:*", diff --git a/dashboards/admin-web/src/__tests__/motion.test.tsx b/dashboards/admin-web/src/__tests__/motion.test.tsx new file mode 100644 index 00000000..b62c35d8 --- /dev/null +++ b/dashboards/admin-web/src/__tests__/motion.test.tsx @@ -0,0 +1,62 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { Reveal, StaggerList } from '@bytelyst/motion'; + +/** + * UX-5 — motion guard. The shared motion primitives must end fully visible + * (no element stranded at opacity:0, which would trip contrast / a11y) and + * must render all their content. We assert the reduced-motion / disabled path + * (what `prefers-reduced-motion` users and SSR-stable snapshots get). + */ +describe('motion primitives stay visible + render content', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = + true; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + it('Reveal renders its child and resolves to visible (opacity 1)', () => { + act(() => { + root.render( + + Revealed content + + ); + }); + const el = container.querySelector('[data-testid="bl-reveal"]') as HTMLElement | null; + expect(el).toBeTruthy(); + expect(el!.getAttribute('data-visible')).toBe('true'); + expect(el!.style.opacity).toBe('1'); + expect(container.textContent).toContain('Revealed content'); + }); + + it('StaggerList renders every child, each visible', () => { + act(() => { + root.render( + +
alpha
+
beta
+
gamma
+
+ ); + }); + const reveals = container.querySelectorAll('[data-testid="bl-reveal"]'); + expect(reveals.length).toBe(3); + reveals.forEach(r => expect(r.getAttribute('data-visible')).toBe('true')); + expect(container.textContent).toContain('alpha'); + expect(container.textContent).toContain('beta'); + expect(container.textContent).toContain('gamma'); + }); +}); diff --git a/dashboards/admin-web/src/app/(dashboard)/page.tsx b/dashboards/admin-web/src/app/(dashboard)/page.tsx index da56a67a..9211b795 100644 --- a/dashboards/admin-web/src/app/(dashboard)/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/page.tsx @@ -35,6 +35,7 @@ import { type RevenueAnalytics, } from '@/lib/api'; import { PageHeader } from '@bytelyst/dashboard-components'; +import { Reveal, StaggerList } from '@bytelyst/motion'; import { AreaChart, BarChart } from '@/components/charts'; import { seriesValues, dateBars } from '@/lib/chart-data'; @@ -310,7 +311,12 @@ export default function DashboardPage() { ))} ) : ( -
+ {kpiCards.map(card => ( @@ -344,7 +350,7 @@ export default function DashboardPage() { ))} -
+ )} {/* Charts Row */} @@ -390,7 +396,7 @@ export default function DashboardPage() { )} {/* Bottom Row: Model Usage + Recent Users */} -
+ {/* Model Usage */} @@ -476,7 +482,7 @@ export default function DashboardPage() { )} -
+ ); } diff --git a/dashboards/admin-web/vitest.config.ts b/dashboards/admin-web/vitest.config.ts index 734eb6b6..2c1e73f0 100644 --- a/dashboards/admin-web/vitest.config.ts +++ b/dashboards/admin-web/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ // The real Next/webpack build already dedupes these to admin-web's React. server: { deps: { - inline: [/@bytelyst\/(charts|data-viz|command-palette|dashboard-components)/], + inline: [/@bytelyst\/(charts|data-viz|command-palette|dashboard-components|motion)/], }, }, coverage: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b58758..7564f029 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: '@bytelyst/logger': specifier: workspace:* version: link:../../packages/logger + '@bytelyst/motion': + specifier: workspace:* + version: link:../../packages/motion '@bytelyst/react-auth': specifier: workspace:* version: link:../../packages/react-auth