# admin-web × ByteLyst UX Integration Roadmap

> **Purpose:** Additively adopt the latest shared `@bytelyst/*` UX into the platform admin console —
> a token bridge, charts migration (`recharts` → `@bytelyst/charts`), command palette, page chrome,
> motion, and system banners — **without** ripping out the mature local shadcn `src/components/ui/*`
> layer (that wholesale replacement is explicitly out of scope for this phase).
> **Delegation target:** Devin CLI (`devin --prompt-file docs/roadmaps/UX_INTEGRATION_ADMIN.md`).
>
> **Repo:** `learning_ai_common_plat/dashboards/admin-web` (`@bytelyst/admin-web`)
> **Run dir:** this package · **Stack:** Next.js 16 · React 19 · TS 5 · Tailwind 4 · Vitest · Playwright
> **Showcase reference (read-only, do not edit):** `../../../copilot/learning_ai_uxui_web`.

---

## Current-state review (verified 2026-05-29)

admin-web is large (~45 dashboard surfaces) and has **two** UI layers: a mature local shadcn
`src/components/ui/*` (radix-based: button/dialog/select/tabs/…) **and** a `Primitives.tsx` adapter.
This roadmap is **additive** — it brings in shared capabilities the app lacks; it does **not**
rewrite the shadcn primitive layer.

| Area            | Today                                                                                                                                | Gap                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Token system    | shadcn OKLCH vars (`--background`/`--primary`/`--card`) + `.dark`                                                                    | `@bytelyst/ui` expects `--bl-*` → **need a bridge** (wave 1)       |
| UI-drift guard  | `eslint.config.mjs` has **no** `no-restricted-imports`                                                                               | add ratchet: direct `@bytelyst/ui` only via `Primitives.tsx`       |
| Charts          | **`recharts` directly in 5 files** (`(dashboard)/page`, `usage`, `users/[id]`, `ops/client-logs`, `docs`, `extraction/entity-chart`) | `@bytelyst/charts` + `@bytelyst/data-viz`                          |
| Command / nav   | none (45 surfaces!)                                                                                                                  | `@bytelyst/command-palette` (⌘K) — high value                      |
| Page chrome     | mixed                                                                                                                                | `@bytelyst/dashboard-components` (already a dep)                   |
| Motion          | none                                                                                                                                 | `@bytelyst/motion` (reduced-motion aware)                          |
| System messages | `broadcasts`/`maintenance`/`notifications` surfaces exist                                                                            | `@bytelyst/notifications-ui` banners (if a real feed is reachable) |

**Explicitly OUT of scope this phase:** replacing the local shadcn `src/components/ui/*` components
with `@bytelyst/ui` equivalents. Leave them as-is; only NEW UI and the waves below use shared packages.

---

## Baseline audit (verified by Devin run — 2026-05-29)

> Hard facts captured from the working tree **before** any wave changes. This corrects/confirms the
> hand-written current-state table above and pins the green/red gates each wave must hold.

**UI layers (confirmed).** Two coexisting layers: mature local shadcn `src/components/ui/*` (radix-based)

- a `src/components/ui/Primitives.tsx` adapter re-exporting `@bytelyst/ui` (`Button`/`Badge`/`Input`/
  `Select`/`Textarea`/`DataTable`/`Modal`/… ). `Primitives.tsx` already consumes `--bl-input` /
  `--bl-surface-muted`. **Additive contract holds:** UX-1→6 do not rewrite shadcn `ui/*`.

**Charts (corrected to 5 files, not 6).** `recharts@^3.7.0` is imported directly in exactly **5** files
(roadmap prose listed `docs` — verified it does **not** import recharts):

1. `src/app/(dashboard)/page.tsx` — `AreaChart` (DAU) + `BarChart` (revenue)
2. `src/app/(dashboard)/usage/page.tsx` — `AreaChart` + `BarChart` + `PieChart`
3. `src/app/(dashboard)/users/[id]/page.tsx` — `AreaChart` (per-user daily usage)
4. `src/app/(dashboard)/ops/client-logs/page.tsx` — `BarChart` (telemetry)
5. `src/components/extraction/entity-chart.tsx` — `BarChart` + `PieChart` (+ non-recharts `EntityTimeline`)

**Shared viz packages (built, NOT yet deps).** `@bytelyst/charts@0.1.1` (`LineChart`/`BarChart`/
`AreaChart`/`Donut`/`Gauge`/`RadarChart`, pure SVG, `--bl-*`-themed) and `@bytelyst/data-viz@0.1.0`
(`Sparkline`/`KpiCard`/`ProgressRing`/`BarSparkline`/`Heatmap`) are built (`dist/` present) but are
**not** in `admin-web` deps yet → UX-2 adds them as `workspace:*`. `@bytelyst/dashboard-components`
**is** already a dep (`PageHeader` re-exported via `Primitives.tsx`). `@bytelyst/command-palette`,
`@bytelyst/motion`, `@bytelyst/notifications-ui` are **not** yet deps.

**Tokens.** `--bl-*` tokens are already supplied by `@bytelyst/design-tokens/css` (imported in
`globals.css`), but they carry the _default_ design-token palette, not admin's shadcn OKLCH ramp →
UX-1 adds an in-`globals.css` **bridge** remapping the `--bl-*` shared contract onto admin's
`--background`/`--card`/`--primary`/… (light **and** `.dark`), so shared components theme correctly.

**ESLint.** `eslint.config.mjs` has **no** `no-restricted-imports` → UX-1 adds the ratchet
(forbid direct `@bytelyst/ui` outside `Primitives.tsx`).

**Green/red gates at baseline (run dir = this package):**

| Gate                                | Baseline result                                                               |
| ----------------------------------- | ----------------------------------------------------------------------------- |
| `typecheck` (`tsc --noEmit`)        | ✅ clean                                                                      |
| `lint` (`eslint`)                   | ✅ clean                                                                      |
| `test` (`vitest run`)               | ✅ **16 files / 111 tests** pass (all `.test.ts`, node env, logic-level)      |
| `format:check` (`prettier --check`) | ❌ **RED — 29 pre-existing files** (none in any wave's edit scope; see below) |
| `build` (`next build --webpack`)    | ✅ compiles, 123 routes (pre-existing multi-lockfile warning only)            |
| `test:e2e` (Playwright, 91 tests)   | captured in **E2e baseline** section below                                    |

**Pre-existing `format:check` debt (NOT caused by this run).** 29 files already fail Prettier at
baseline — none overlap the files UX-1→6 edit. Sweeping-reformatting 29 unrelated files would be
scope-creep in a shared monorepo, so this gate is treated like e2e: **no NEW format failures vs this
baseline**; every file this run creates/edits is kept Prettier-clean. (CSS is not in the prettier glob.)

---

## Ground rules (non-negotiable)

1. **Scope lock:** edit only files under `dashboards/admin-web/`. Never edit shared
   `packages/@bytelyst/*`, other dashboards, services, the showcase, or any sibling repo. Never
   `git push` outside `admin-web/`.
2. **Additive only:** do NOT rewrite the local shadcn `src/components/ui/*` layer. Route only NEW
   shared-`@bytelyst/ui` usage through `src/components/ui/Primitives.tsx`.
3. **Deps are `workspace:*`** (common-plat convention) — match that, NOT `"*"`. Keep lockfile
   changes minimal (importer link entries only); recover with `pnpm install --frozen-lockfile` if
   within-repo links corrupt. Do NOT commit a full monorepo lockfile re-normalization or relink
   `packages/ui/node_modules/@radix-ui/*` to a sibling store (this broke Turbopack/e2e on tracker-web).
4. **Tokens:** keep the shadcn OKLCH system; add a `--bl-*` → admin-token **bridge** (wave 1) so
   shared components theme correctly in light + dark. Zero new hardcoded color literals.
5. **Tests are sacred** — never weaken/skip/delete; add a test per wave.
6. **Commit cadence:** verify → commit → push **per item** (`feat(admin-web): … (UX-N)`); never
   leave >1 wave uncommitted; immediately tick the checkbox with **SHA + test counts**. Not done
   until committed **and** pushed.
7. **Deferrals explicit** (`[~]` + reason + table row); **no stray files**; env blockers recorded here.

### Verify (after EVERY wave + final sweep)

```bash
pnpm --filter @bytelyst/admin-web typecheck
pnpm --filter @bytelyst/admin-web lint
pnpm --filter @bytelyst/admin-web test
pnpm --filter @bytelyst/admin-web format:check
pnpm --filter @bytelyst/admin-web build
pnpm --filter @bytelyst/admin-web test:e2e   # Playwright + @axe-core (no new failures vs wave-1 baseline)
# size:check (bundlesize) where it runs in this env; else record gzipped sizes here
```

---

## Waves

- [x] **UX-1 — Token bridge + UI-drift ratchet (FIRST):** add a `--bl-*` → admin OKLCH bridge in
      `globals.css` (light + dark); add ESLint `no-restricted-imports` forbidding direct
      `@bytelyst/ui` imports outside `Primitives.tsx`; add a Primitives export-presence test. Capture
      the e2e baseline below. — **DONE** `df72199c` · test **17 files / 140 tests** (+1 file / +29 vs
      baseline 16 / 111); typecheck+lint+build green; e2e 11✅/80❌ (unchanged baseline); format:check
      no new failures (29 pre-existing).
- [x] **UX-2 — Charts:** migrate the `recharts` usages to `@bytelyst/charts` (+ `@bytelyst/data-viz`),
      lazy-loaded; render tests (no NaN in SVG). Drop `recharts` if fully unused afterward.
      — **DONE** `01f79afa` · Migrated all **5** recharts files (page, usage, users/[id],
      ops/client-logs, extraction/entity-chart) → `AreaChart`/`BarChart`/`Donut` via a lazy
      `src/components/charts` seam (`next/dynamic`, own chunk; dynamic-imports a local static
      re-export `primitives.tsx` because the packages declare only an `import` export condition).
      Added pure finite-safe mappers `src/lib/chart-data.ts`. **`recharts` dropped** from
      `package.json` + lockfile importer (now fully unused; 33 orphaned pkgs pruned). Stacked
      severity chart → single-series bars colored by dominant severity; pie charts → `Donut`;
      horizontal bars → vertical (charts are vertical/single-series; StackedBar deferred to charts
      0.2.x). Lockfile change = **+6/−3 importer lines only** (no re-normalization; `workspace:*`;
      `--frozen-lockfile` clean). Vitest config: inline + `dedupe` react for SSR render tests.
      test **18 files / 159 tests** (+1 file / +19); typecheck+lint+build green (123 routes);
      format:check no new failures; e2e unchanged (see below).
- [x] **UX-3 — Command palette:** add `@bytelyst/command-palette`; mount `CommandRegistryProvider` +
      `CommandPalette` (⌘K, lazy) in `(dashboard)/layout.tsx`; register navigate commands for the major
      surfaces (Users, Billing, Flags, Broadcasts, Audit, Experiments, Subscriptions, Licenses, Ops, …) + theme toggle + sign out; Vitest test palette opens on ⌘K.
      — **DONE** `b4e450d6` · `CommandRegistryProvider` wraps the dashboard; `CommandMenu`
      (`useCommandPalette` ⌘K/Ctrl-K hotkey + lazy `next/dynamic` dialog via local
      `command-palette-dialog.tsx` re-export) registers **21** navigate commands (`src/lib/admin-commands.ts`) + theme-toggle + sign-out actions; `onNavigate`→`router.push`. Dep added `workspace:*`
      (importer-only lockfile change, `--frozen-lockfile` clean). Vitest: pure command-set tests +
      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.
- [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** `94ef3f1c` · `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.
- [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** `aa0e67d2` · `@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**.
  — **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

- [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"`+`<title>`
      (`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 ⏭️ 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)

`pnpm --filter @bytelyst/admin-web test:e2e` (Playwright, chromium, 91 tests, dev server auto-booted on :3001):

```
11 passed
80 failed   (5.0m)
```

**The 80 failures are environmental, NOT regressions.** They all fail in a `loginAsAdmin` /
`beforeEach` step (`getByLabel('Email').fill(...)` times out, or post-login `waitForURL('**/dashboard')`
never resolves) because the `platform-service` backend (`:4003`) and emulator stack are not running in
this sandbox, so authenticated surfaces never render. The **11 passing** are the public/unauthenticated
specs (`login.spec.ts`, `navigation.spec.ts`, `smartauth-login.spec.ts` public-page assertions).

**Gate for every subsequent wave:** keep these **11 passing** and add **no new failures** — i.e. the
count must stay ≥ 11 passed / ≤ 80 failed. (A full green run requires the backend; out of scope here.)

## Deferrals (fill in as encountered)

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

---

## Phase 2 (FUTURE — do NOT start in this run)

> Tracked here so it isn't lost. **Out of scope for the phase-1 run above.** Do not begin any of
> this without an explicit, separate delegation — it is large and high-risk.

**Goal:** migrate the local shadcn `src/components/ui/*` layer (~16 radix-based components: `button`,
`badge`, `dialog`, `dropdown-menu`, `select`, `tabs`, `tooltip`, `switch`, `slider`, `progress`,
`avatar`, `label`, `separator`, `sheet`, …) onto shared `@bytelyst/ui` equivalents via the
`Primitives.tsx` adapter, retiring the bespoke radix wrappers.

**Why it's deferred:**

- Touches nearly all ~45 surfaces (every page imports shadcn primitives) → enormous blast radius.
- Behavioral parity risk: shadcn variants, `class-variance-authority` styling, and radix a11y
  semantics must be matched 1:1 or admin workflows regress.
- Should be done component-by-component behind the wave-1 token bridge, each with snapshot/interaction
  tests, not as a big-bang swap.

**Suggested sequencing (when scheduled):**

1. Bridge first (done in phase-1 UX-1) so shared components already theme correctly.
2. Migrate leaf primitives with no children first (`badge`, `label`, `separator`, `avatar`).
3. Then form controls (`input`, `select`, `switch`, `slider`) with submit/interaction tests.
4. Then overlays (`dialog`, `dropdown-menu`, `tooltip`, `sheet`) — verify focus-trap/Esc/scroll-lock.
5. Delete each shadcn wrapper only once all its importers are migrated and the drift ratchet passes.
6. One component per commit; keep `test:coverage` and `size:check` green throughout.

**Acceptance:** local `src/components/ui/*` shadcn wrappers removed (or reduced to thin re-exports of
the adapter), all surfaces visually + behaviorally unchanged, coverage and bundle budgets held.
