feat(admin-web): add @bytelyst/motion reveal/stagger on dashboard (UX-5)

- @bytelyst/motion added workspace:* (importer-only lockfile change;
  --frozen-lockfile clean).
- Dashboard overview only: KPI cards grid wrapped in StaggerList (from up,
  50ms stagger); the Model-Usage / Recent-Users table row wrapped in Reveal.
- Primitives honor prefers-reduced-motion and resolve to opacity 1, so no
  element is stranded transparent (no contrast/a11y regression); prefersReduced
  is SSR-safe. Motion is confined to the auth-gated dashboard, not the public
  e2e surfaces, per tracker-web's axe/opacity caution.
- vitest.config: inline @bytelyst/motion + react dedupe for the render test.

Tests: happy-dom asserts Reveal/StaggerList end visible and render all children.

Verify: typecheck+lint+build green (123 routes); vitest 21 files / 170 tests
(+2); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs
UX-1 baseline — environmental).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-29 14:19:28 -07:00
parent 89a56739bd
commit aa0e67d219
6 changed files with 87 additions and 7 deletions

View File

@ -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** `<SHA-UX5>` · `@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 ⬜
```

View File

@ -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:*",

View File

@ -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(
<Reveal disableMotion from="up">
<span>Revealed content</span>
</Reveal>
);
});
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(
<StaggerList disableMotion from="up">
<div>alpha</div>
<div>beta</div>
<div>gamma</div>
</StaggerList>
);
});
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');
});
});

View File

@ -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() {
))}
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<StaggerList
as="div"
from="up"
stagger={50}
className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"
>
{kpiCards.map(card => (
<Card key={card.title} className="transition-shadow hover:shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2">
@ -344,7 +350,7 @@ export default function DashboardPage() {
</CardContent>
</Card>
))}
</div>
</StaggerList>
)}
{/* Charts Row */}
@ -390,7 +396,7 @@ export default function DashboardPage() {
)}
{/* Bottom Row: Model Usage + Recent Users */}
<div className="grid gap-6 lg:grid-cols-2">
<Reveal from="up" className="grid gap-6 lg:grid-cols-2">
{/* Model Usage */}
<Card>
<CardHeader>
@ -476,7 +482,7 @@ export default function DashboardPage() {
)}
</CardContent>
</Card>
</div>
</Reveal>
</div>
);
}

View File

@ -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: {

3
pnpm-lock.yaml generated
View File

@ -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