feat(admin-web): adopt @bytelyst/dashboard-components page chrome (UX-4)

- error.tsx -> ErrorPage (keep telemetry on mount; retry wired to Next reset).
- (dashboard)/loading.tsx -> LoadingSpinner inside the existing skeleton.
- not-found.tsx already used NotFoundPage (confirmed, unchanged).
- dashboard overview page.tsx header -> PageHeader (Refresh as actions; the
  subtitle/last-updated line preserved directly below).

Rich detail headers (e.g. users/[id] back-button + plan/status badges) left
bespoke on purpose: PageHeader has no subtitle/badge slot, so forcing it would
regress them (additive-only rule). dashboard-components reads --color-* which
admin maps via @theme inline, so it themes in light + dark.

Verify: typecheck+lint+build green (123 routes); vitest 20 files / 168 tests
(+3 happy-dom chrome render tests); 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:09:40 -07:00
parent 997002e913
commit 94ef3f1c20
6 changed files with 110 additions and 34 deletions

View File

@ -152,9 +152,17 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
happy-dom ⌘K/Ctrl-K interaction test (`react-dom/client` + `act`, no new deps; react deduped).
test **19 files / 165 tests** (+6); typecheck+lint+build green (123 routes); format:check no new
failures; e2e unchanged.
- [ ] **UX-4 — Page chrome:** use `@bytelyst/dashboard-components` (`PageHeader`/`ErrorPage`/
- [x] **UX-4 — Page chrome:** use `@bytelyst/dashboard-components` (`PageHeader`/`ErrorPage`/
`NotFoundPage`/`LoadingSpinner`) on `error.tsx`/`not-found.tsx`/`loading.tsx` + a few high-traffic
surfaces where chrome is bespoke. Keep it additive.
**DONE** `<SHA-UX4>` · `error.tsx`→`ErrorPage` (telemetry kept; retry→`reset`);
`loading.tsx`→`LoadingSpinner` inside the existing skeleton; `not-found.tsx` already used
`NotFoundPage` (confirmed); dashboard overview `page.tsx` header→`PageHeader` (Refresh as
`actions`, subtitle preserved below). Rich detail headers (users/[id] back-button + badges)
intentionally left bespoke — `PageHeader` has no subtitle/badge slot, so forcing it would
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
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.)
@ -173,7 +181,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

@ -0,0 +1,66 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
// error.tsx fires telemetry on mount — stub it so the chrome can render in isolation.
vi.mock('@/lib/telemetry', () => ({ trackEvent: vi.fn() }));
import GlobalError from '@/app/error';
import NotFound from '@/app/not-found';
import DashboardLoading from '@/app/(dashboard)/loading';
/**
* UX-4 page-chrome adoption guard. Verifies admin's error / not-found /
* loading surfaces render the shared `@bytelyst/dashboard-components` chrome
* and that the error retry handler is wired to Next's `reset`.
*/
describe('page chrome (dashboard-components)', () => {
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('error.tsx renders ErrorPage with the message and a working retry', () => {
const reset = vi.fn();
act(() => {
root.render(
<GlobalError error={Object.assign(new Error('kaboom'), { digest: 'd1' })} reset={reset} />
);
});
expect(container.textContent).toContain('Something went wrong');
expect(container.textContent).toContain('kaboom');
const retry = Array.from(container.querySelectorAll('button')).find(b =>
/try again|retry/i.test(b.textContent ?? '')
);
expect(retry).toBeTruthy();
act(() => retry!.dispatchEvent(new MouseEvent('click', { bubbles: true })));
expect(reset).toHaveBeenCalledTimes(1);
});
it('not-found.tsx renders the NotFoundPage chrome', () => {
act(() => {
root.render(<NotFound />);
});
expect(container.querySelector('a[href="/"]')).toBeTruthy();
});
it('loading.tsx renders an accessible LoadingSpinner', () => {
act(() => {
root.render(<DashboardLoading />);
});
expect(container.querySelector('[role="status"]')).toBeTruthy();
});
});

View File

@ -1,4 +1,4 @@
import { Loader2 } from 'lucide-react';
import { LoadingSpinner } from '@bytelyst/dashboard-components';
export default function DashboardLoading() {
return (
@ -29,7 +29,7 @@ export default function DashboardLoading() {
<div key={i} className="rounded-xl border bg-card p-6">
<div className="mb-4 h-5 w-36 animate-pulse rounded bg-muted" />
<div className="flex items-center justify-center h-64">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<LoadingSpinner size="md" />
</div>
</div>
))}

View File

@ -34,6 +34,7 @@ import {
type ApiUsageRecord,
type RevenueAnalytics,
} from '@/lib/api';
import { PageHeader } from '@bytelyst/dashboard-components';
import { AreaChart, BarChart } from '@/components/charts';
import { seriesValues, dateBars } from '@/lib/chart-data';
@ -275,22 +276,30 @@ export default function DashboardPage() {
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">
Platform overview and key metrics
{lastUpdated && (
<span className="ml-2 text-xs">
&middot; Updated {lastUpdated.toLocaleTimeString()}
</span>
)}
</p>
</div>
<Button variant="outline" size="sm" onClick={() => fetchData(true)} disabled={refreshing}>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
</Button>
<div>
<PageHeader
title="Dashboard"
className="!mb-2"
actions={
<Button
variant="outline"
size="sm"
onClick={() => fetchData(true)}
disabled={refreshing}
>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
</Button>
}
/>
<p className="text-muted-foreground">
Platform overview and key metrics
{lastUpdated && (
<span className="ml-2 text-xs">
&middot; Updated {lastUpdated.toLocaleTimeString()}
</span>
)}
</p>
</div>
{/* KPI Cards */}

View File

@ -1,6 +1,7 @@
'use client';
import { useEffect } from 'react';
import { ErrorPage } from '@bytelyst/dashboard-components';
import { trackEvent } from '@/lib/telemetry';
export default function GlobalError({
@ -19,19 +20,11 @@ export default function GlobalError({
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="mx-auto max-w-md text-center">
<div className="mb-4 text-5xl"></div>
<h2 className="mb-2 text-xl font-semibold">Something went wrong</h2>
<p className="mb-6 text-sm text-muted-foreground">
{error.message || 'An unexpected error occurred.'}
</p>
<button
onClick={reset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Try again
</button>
</div>
<ErrorPage
title="Something went wrong"
message={error.message || 'An unexpected error occurred.'}
onRetry={reset}
/>
</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)/],
inline: [/@bytelyst\/(charts|data-viz|command-palette|dashboard-components)/],
},
},
coverage: {