# 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.
- [ ] **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.
- [ ] **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.)
- [ ] **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**.

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

## 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 ⬜
```

## 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 |
| ------------ | -------------------------- | --------- |
| _(none yet)_ |                            |           |

---

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