# tracker-web × ByteLyst UX Integration Roadmap

> **Purpose:** Adopt the latest shared `@bytelyst/*` UX (proven in the `learning_ai_uxui_web`
> showcase) into `tracker-web`, replacing bespoke/raw UI with shared primitives, charts, command
> palette, toasts, and motion — while keeping the existing OKLCH token system and dark mode.
> **Delegation target:** Devin CLI (`devin --prompt-file docs/roadmaps/UX_INTEGRATION_BYTELYST.md`).
>
> **Repo:** `learning_ai_common_plat/dashboards/tracker-web` (`@bytelyst/tracker-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` — live demos
> of every package at `http://localhost:3010/showcase/*` and source in `src/catalog/examples/`.

---

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

| Surface                 | Today                                                                                                      | Gap vs latest UX                                                     |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Token system            | shadcn-style OKLCH vars (`--background`, `--primary`, `--card`, `--chart-1..5`) + `.dark` in `globals.css` | `@bytelyst/ui` components expect `--bl-*` tokens → **need a bridge** |
| `/dashboard/items`      | ✅ already on `@bytelyst/data-table`                                                                       | good reference pattern                                               |
| `/dashboard` overview   | badge-pill stats; `--chart-1..5` defined but **unused**                                                    | swap to `charts`/`data-viz` (Donut, KpiCard, Sparkline)              |
| `/dashboard/*` controls | raw `<input>/<select>/<button>`, hand-rolled modals (no focus-trap/Esc/scroll-lock)                        | `@bytelyst/ui` `Input`/`Button`/`Modal`/`Field` (a11y built-in)      |
| `/dashboard` layout     | hand-rolled sticky top nav                                                                                 | `@bytelyst/ui` `AppShell`                                            |
| `/roadmap` (public)     | hardcoded `slate-*`/`blue-*` — different visual language                                                   | re-skin to tokens + primitives                                       |
| Feedback                | inline error/success `<div>`s                                                                              | `@bytelyst/ui` `Toast`                                               |
| Command/nav             | none                                                                                                       | `@bytelyst/command-palette` (⌘K)                                     |
| Motion                  | none                                                                                                       | `@bytelyst/motion` (reduced-motion aware)                            |

**Available shared components (verified exports):**

- `@bytelyst/ui`: `Button` `Input` `Field*` `Modal` `ConfirmDialog` `Badge` `StatusBadge`/`StatusDot`
  `EmptyState` `Skeleton`/`TableSkeleton` `MetricCard` `EntityCard` `Toast`/`ToastProvider`/`useToast`
  `AppShell*` `Drawer` `ActionMenu` `FilterBar` `PageHeader` `IconButton` `AlertBanner`
- `@bytelyst/charts`: `LineChart` `BarChart` `AreaChart` `Donut` `Gauge`
- `@bytelyst/data-viz`: `Sparkline` `BarSparkline` `KpiCard` `ProgressRing` `Heatmap`
- `@bytelyst/command-palette`: `CommandPalette` `CommandRegistryProvider` `useCommandPalette`
- `@bytelyst/motion`: `Reveal` `Stagger` `NumberFlow` `TiltCard` `ScrollProgress` … (see `/showcase/motion/all`)
- `@bytelyst/ui` (not yet in the adapter): `Tooltip` `Tabs` `SegmentedControl` `DropdownMenu`
  `Switch` `Checkbox` `RadioGroup` `Select` `Textarea` `Panel` `Surface` `Card` `Separator`
  `DataList` `Timeline` `Drawer` `ActionMenu` `IconButton` `AlertBanner`
- `@bytelyst/dashboard-components`: `PageHeader` `ErrorPage` `NotFoundPage` `EmptyState`
  `LoadingSpinner` `LoadingSkeleton`
- `@bytelyst/auth-ui`: `LoginForm` `MfaChallenge` `SocialButtons` `PasswordStrengthBar` …
- `@bytelyst/notifications-ui`: `NotificationCenter` `InboxItem` `BannerStack` `Announcement`
- `@bytelyst/rich-text`: `RichTextEditor` (Tiptap v3, slash + mentions) · `RichTextViewer`

> **Expansion (2026-05-28):** waves **UX-9 … UX-13** below were added after re-auditing
> tracker-web against the full showcase catalog (`src/catalog/routes.ts`, ~60 entries).
> They are sequenced **after** the core UX-2 … UX-8 migration and several are explicitly
> **stretch / data-gated** — do not start them until the wave they depend on is green.

> Aligns with master `docs/ROADMAP.md` Phase 2.1 (UI primitives migration) — this roadmap is the
> concrete, delegate-ready execution plan and extends it with charts/palette/toast/motion.

---

## Ground rules (non-negotiable)

1. **Scope lock:** edit only files under `dashboards/tracker-web/`. Never edit shared
   `packages/@bytelyst/*`, the showcase repo, other dashboards/services, or master `docs/ROADMAP.md`.
2. **Deps:** add shared packages as `"workspace:*"` in this package's `package.json` (they live in
   the same monorepo — no publish needed). Run `pnpm install` from the repo root after edits.
3. **Preserve the token system + dark mode.** Do NOT rip out the shadcn OKLCH vars. Bridge `--bl-*`
   onto them (UX-1) so shared components inherit tracker's theme automatically.
4. **Tests sacred; no fabrication.** Add tests for new behavior; only check a box after its
   **Verify** passes. If blocked (e.g. needs live backend), leave `- [ ]` + a one-line note.
5. **Style preservation:** match existing conventions. No `console.log`. No hardcoded hex/color
   literals in new code — use tokens / component props. No emojis in code.
6. **Commits:** one task = one conventional commit (`feat(tracker-web): …` / `refactor(tracker-web): …`),
   flip the checkbox + paste short-SHA in the same commit. **Do not push** — operator reviews.
7. **Offline-first verification.** Pages needing auth/live data can't be fully E2E'd here; rely on
   `typecheck` + `lint` + Vitest render tests + `build`. Mark backend-only checks as deferred.

## Verify commands (from `dashboards/tracker-web`)

```bash
pnpm typecheck && pnpm lint && pnpm test
pnpm build      # final gate
```

---

## UX-1 — Foundation: token bridge + Primitives adapter ⟵ do first, everything depends on it

- [x] **1.1** Add `@bytelyst/ui` and `@bytelyst/design-tokens` as `workspace:*` deps; `pnpm install`. (dc01dd02)
- [x] **1.2** In `src/app/globals.css`, add a **bridge layer** mapping `--bl-*` → existing tracker
      vars so shared components theme correctly in light + dark. Minimum set (extend as needed):
      `css
:root, .dark {
  --bl-accent: var(--primary);
  --bl-accent-foreground: var(--primary-foreground);
  --bl-bg-canvas: var(--background);
  --bl-surface-card: var(--card);
  --bl-text-primary: var(--foreground);
  --bl-text-secondary: var(--muted-foreground);
  --bl-text-tertiary: var(--muted-foreground);
  --bl-border: var(--border);
  --bl-danger: var(--destructive);
  --bl-radius-control: var(--radius-md);
  --bl-radius-card: var(--radius-lg);
  /* map chart palette: --bl-chart-1 → --chart-1 … */
}
`
      Reference the showcase's `globals.css` fallback layer for the full `--bl-*` surface. (dc01dd02)
- [x] **1.3** Create `src/components/ui/Primitives.tsx` re-exporting the `@bytelyst/ui` components
      this app needs (`Button` `Input` `Field*` `Modal` `ConfirmDialog` `Badge` `StatusBadge`
      `EmptyState` `Skeleton`/`TableSkeleton` `MetricCard` `Toast`/`ToastProvider`/`useToast`).
      All app code imports from this adapter, never `@bytelyst/ui` directly (enables the Phase 1.3
      UI-drift ratchet later). (dc01dd02)
- [x] **1.4** Smoke test: a Vitest render test asserting `Button` renders and a `StatusBadge`
      gets a token-driven class. **Verify:** `pnpm typecheck && pnpm lint && pnpm test && pnpm build`. (dc01dd02)

## UX-2 — Migrate dashboard controls to primitives

- [x] **2.1** `src/app/dashboard/items/page.tsx`: replace raw search `<input>`, the 3 filter
      `<select>`s, the "+ New Item" `<button>`, and the **create modal** with `Input`/`Field`/
      `Button`/`Modal`. The shared `Modal` fixes the current focus-trap/Esc/scroll-lock a11y gap.
      Keep `DataTable` as-is; swap the inline type/status/priority cell pills to `StatusBadge`.
      (UX-2.1 `a7a6f191`: tc/lint/test 165 ✓/format/build/e2e 18 ✓; cell pills → `StatusBadge`
      tones, errors → `AlertBanner`, all via the Primitives adapter.)
- [x] **2.2** `src/app/dashboard/page.tsx`: wrap the `confirm()` delete in `ConfirmDialog`; replace
      the bespoke `StatCard` chrome with `MetricCard` where it fits (full chart swap is UX-4).
      (UX-2.2 `c9e65d43`: tc/lint/test 165 ✓/format/build/e2e 18 ✓. Total-count card → `MetricCard`;
      breakdown `StatCard`s left for the UX-4 chart swap. The `confirm()` delete is the items-list
      delete action — moved to the accessible `ConfirmDialog`; overview load errors → `AlertBanner`.)
- [x] **2.3** `src/app/dashboard/board/page.tsx` + `src/app/dashboard/items/[id]/page.tsx`:
      migrate buttons/inputs/badges/modals to primitives; comments + status/priority controls.
      (UX-2.3 `aa36671e`: tc/lint/test 165 ✓/format/build/e2e 18 ✓. Board pills → `StatusDot`/
      `StatusBadge`/`Badge`, status-move → `Button`; detail editor/selects/vote/comment composer →
      `Input`/`Textarea`/`Select`/`Button`; errors → `AlertBanner`. ActionMenu/Timeline untouched.)
- [x] **2.4** `src/app/login/page.tsx`: migrate form to `Field`/`Input`/`Button` + `AlertBanner`.
      **Verify per task:** `pnpm typecheck && pnpm lint && pnpm build`.
      (SUPERSEDED by UX-11 `328e3072`: the login surface was already adopted onto the shared
      `@bytelyst/auth-ui` `LoginForm`/`MfaChallenge`, which provide the field/input/button +
      inline error presentation this item asked for. Per the ground rules, the adopted surface is
      not re-migrated to the lower-level `Field`/`Input`/`Button` primitives. No code change.)

## UX-3 — Re-skin the public roadmap to the token system

- [x] **3.1** `src/app/roadmap/page.tsx`: replace hardcoded `slate-*`/`blue-*` utility classes with
      tokenized equivalents (`bg-card`, `text-foreground`, `text-muted-foreground`, `bg-primary`…)
      and swap the type/priority/status pills to `Badge`/`StatusBadge`. Keep all behavior + the
      board/list toggle. Vote buttons keep the a11y attrs added previously.
      (UX-3.1 `f612d2ec`: tc/lint/test 165 ✓/format/build/e2e 18 ✓. type/priority → `Badge`, status
      markers → `StatusDot` tones, stats → `MetricCard`, search/filter → `Input`/`Select`, errors →
      `AlertBanner`; vote buttons keep aria-pressed/aria-label/title; zero new color literals.)
- [x] **3.2** Replace the submit + email-prompt modals with the shared `Modal`.
      **Verify:** `pnpm typecheck && pnpm lint && pnpm build`; existing roadmap tests stay green.
      (UX-3.2 `51c30ed7`: tc/lint/test 165 ✓/format/build/e2e 18 ✓. Both dialogs now use the shared
      Radix `Modal`; the bespoke local `Modal` is removed; titles drive the accessible heading.)

## UX-4 — Data-viz for the overview (use the unused `--chart-*` tokens)

- [x] **4.1** Add `@bytelyst/charts` + `@bytelyst/data-viz` as `workspace:*` deps.
      (UX-4 `f2dfddf9`: added with minimal `link:` importer entries in `pnpm-lock.yaml` — no full
      re-normalisation; recovered/validated with `pnpm install --frozen-lockfile`.)
- [x] **4.2** `src/app/dashboard/page.tsx`: replace badge-pill breakdowns with a `Donut` for
      **By Status** and **By Type**, a `BarChart`/`BarSparkline` for **By Priority**, and `KpiCard`s
      for `total` + open/in-progress/done with trend where data allows.
      (UX-4 `f2dfddf9`: KpiCards for total/open/in-progress/done; `Donut` By Status (centered total) + By Type; `BarChart` By Priority, coloured from the bridged `--bl-chart-*` palette. The chart
      surface is split into `overview-charts.tsx` and loaded via `next/dynamic` (ssr:false) to keep
      it out of the route bundle. No time-series in `getStats`, so KPI trend is omitted.)
- [x] **4.3** Add a Vitest test rendering the overview with mocked `getStats` (no NaN in SVG paths).
      **Verify:** `pnpm typecheck && pnpm lint && pnpm test && pnpm build`.
      (UX-4 `f2dfddf9`: tc/lint/test 170 ✓/format/build/e2e 18 ✓. `overview-charts.test.tsx` renders
      the surface with mocked stats via `react-dom/server` asserting no `NaN`, plus transform unit
      tests; react deduped in vitest for a single SSR React instance.)

## UX-5 — Command palette (⌘K)

- [x] **5.1** Add `@bytelyst/command-palette` dep; mount `CommandRegistryProvider` + `CommandPalette`
      in `src/app/providers.tsx` (or dashboard layout), opened with ⌘K / Ctrl-K.
      (UX-5 `a7cb866c`: dep added with a minimal `link:` lockfile entry; `CommandRegistryProvider`
      wraps the app in `providers.tsx`, the palette shell is loaded via `next/dynamic` (ssr:false).)
- [x] **5.2** Register commands: navigate (Overview / Items / Board / Roadmap), **New item**,
      **Switch product** (wire `ProductSwitcher`), **Toggle theme**, **Sign out**.
      (UX-5 `a7cb866c`: built in pure `src/lib/command-registry.ts`. New item → `/dashboard/items?new=1`
      auto-opens the create modal; Switch commands call `setProductId` (same store as ProductSwitcher);
      Toggle theme flips `setTheme`; Sign out calls `logout()` + routes to `/login`.)
- [x] **5.3** Vitest test: palette opens on ⌘K and lists registered commands.
      **Verify:** `pnpm typecheck && pnpm lint && pnpm test && pnpm build`.
      (UX-5 `a7cb866c`: tc/lint/test 173 ✓/format/build/e2e 18 ✓. `command-menu.test.tsx` (jsdom)
      asserts the builder set + that ⌘K opens the palette and lists New item/Toggle theme/Sign out.)

## UX-6 — Toasts replace inline status divs

- [x] **6.1** Mount `ToastProvider`; replace inline error/success `<div>`s across items/overview/
      board/detail/roadmap with `toast()` calls (create, delete, vote, submit, load-error).
      **Verify:** `pnpm typecheck && pnpm lint && pnpm build`.
      (UX-6 `3fc559d0`: tc/lint/test 173 ✓/format/build/e2e 18 ✓. `ToastProvider` mounted in
      `providers.tsx`; the AlertBanner/inline status divs across all five surfaces now emit `toast()`
      (load errors + create/delete/vote/update/comment/submit). Roadmap submit toasts + closes the
      dialog; item detail keeps one inline empty-state for the hard item-load failure.)

## UX-7 — Motion polish (reduced-motion aware)

- [x] **7.1** Add `@bytelyst/motion`; apply `Reveal`/`Stagger` to dashboard cards, items rows, and
      roadmap columns; `NumberFlow` on the overview totals. Must no-op under `prefers-reduced-motion`.
      **Verify:** `pnpm typecheck && pnpm lint && pnpm build`.
      (UX-7 `002b55c3`: tc/lint/test 173 ✓/format/build/e2e 18 ✓. `@bytelyst/motion` added (minimal
      `link:` lockfile entry). `Reveal` (staggered) on the overview KPI cards + chart surface and on
      the items `DataTable`; `NumberFlow` on the overview KPI totals; all no-op under
      `prefers-reduced-motion`. **Roadmap-column reveal intentionally omitted:** the offline
      `@axe-core` gate scans `/roadmap` synchronously and `Reveal`'s transient sub-1 opacity trips
      `color-contrast` (verified — even `from="scale"` at 0.92 fails on the muted card text), which
      would breach the CC.4 a11y gate. The dashboard/items surfaces are auth-gated (not axe-scanned)
      so they keep motion; no new a11y violations.)

## UX-8 — AppShell (stretch)

- [x] **8.1** Replace the hand-rolled top nav in `src/app/dashboard/layout.tsx` with `@bytelyst/ui`
      `AppShell` (`AppShellSidebar`/`AppShellNav`/`AppShellPageHeader`/`AppShellSkipLink` + mobile
      toggle), keeping `ProductSwitcher`, user email, sign-out, and adding the ⌘K trigger + theme
      toggle. **Verify:** `pnpm typecheck && pnpm lint && pnpm build`; keyboard nav + skip-link work.
      (UX-8 `cbd4274a`: tc/lint/test 182 ✓/format/build/e2e 18 ✓. `AppShell` + `AppShellSidebar`/
      `AppShellNav`/`AppShellNavItem` (aria-current active route, client nav) / `AppShellMain`
      (focusable `#main-content`) / `AppShellSkipLink` + mobile toggle + overlay. Sidebar keeps
      `ProductSwitcher`, user email, Sign out and adds a ⌘K trigger (replays the hotkey) + theme
      toggle. AppShell exports go through the Primitives adapter (CC.6) + export-presence test.
      `AppShellPageHeader` deliberately not used — the per-page `PageHeader` (UX-10) stays the single
      h1 per route, avoiding duplicate headings.)

## UX-9 — Complete the Primitives adapter (+ close the coverage gap)

> The adapter (`src/components/ui/Primitives.tsx`) currently re-exports ~12 components and
> sits at **0% test coverage** (flagged in `docs/TEST_VALIDATION_LOG.md`). Broaden it and
> lock it with a cheap export-presence test — no `@testing-library/react`/jsdom needed.

- [x] **9.1** Extend `Primitives.tsx` to also re-export the shared controls the app will need
      in later waves: `Select` `Textarea` `Checkbox` `Switch` `RadioGroup` `Tooltip` `Tabs`
      `SegmentedControl` `DropdownMenu` `Drawer` `ActionMenu` `IconButton` `AlertBanner`
      `Card` `Panel` `Separator` `DataList` `Timeline`. Keep app code importing only from the
      adapter (preserves the UI-drift ratchet).
- [x] **9.2** Add `src/__tests__/primitives-adapter.exports.test.ts` asserting every adapter
      export is `defined` (a pure import test — raises `Primitives.tsx` off 0% without a DOM dep).
      **Verify:** `pnpm typecheck && pnpm lint && pnpm test && pnpm build`. (UX-9 `18a09b25`: tc/lint/test 159 ✓/build/e2e 18 ✓)

## UX-10 — Page chrome via `@bytelyst/dashboard-components`

- [x] **10.1** `src/app/error.tsx`: render `ErrorPage` from `@bytelyst/dashboard-components`
      (keep the existing `trackEvent` telemetry side-effect + `reset` wiring). `not-found.tsx`
      already uses `NotFoundPage` — leave it.
- [x] **10.2** Add `PageHeader` (title + breadcrumbs) to the top of `/dashboard`, `/dashboard/items`,
      `/dashboard/board`, and the item detail page for a consistent header band.
- [x] **10.3** Replace ad-hoc loading text with `LoadingSpinner`/`LoadingSkeleton` where a full
      `SkeletonGroup` (UX-2) is overkill.
      **Verify:** `pnpm typecheck && pnpm lint && pnpm build`. (UX-10 verified: tc/lint/test 159 ✓/build/e2e 18 ✓; no new color literals)

## UX-11 — Adopt `@bytelyst/auth-ui` on the login surface

> `src/app/login/page.tsx` is a hand-rolled `<form>` with a second MFA step. The existing
> `/api/auth/*` proxy routes and `auth-context` stay as-is — only the **presentation** changes.

- [x] **11.1** Add `@bytelyst/auth-ui` as a `workspace:*` dep; `pnpm install` from the root.
      (Added dep + minimal `link:` lockfile entry; avoided the env-specific full lockfile
      re-normalisation churn — see TEST_VALIDATION_LOG blocker.)
- [x] **11.2** Replace the password form with `LoginForm` and the OTP step with `MfaChallenge`,
      wiring their submit handlers to the current login/MFA fetch calls. Add `SocialButtons`
      only for providers the backend actually supports (gate on `/api/auth/oauth/[provider]`).
      Social gated to Google via `NEXT_PUBLIC_GOOGLE_CLIENT_ID`; an effect adds aria-labels to
      the placeholder-only inputs so a11y + label queries stay green.
- [x] **11.3** Keep all existing auth tests green; add a render test asserting the login form
      shows email/password fields + submit. (Render assertion is the Playwright "shows login form
      with correct branding" test; added an auth-ui import smoke test for unit-level wiring.)
      **Verify:** `pnpm typecheck && pnpm lint && pnpm test && pnpm build`. (UX-11 verified: tc/lint/test 162 ✓/build/e2e 18 ✓)

## UX-12 — Detail & board richness (Tabs · Tooltip · Drawer · Timeline · rich-text)

- [x] **12.1** `/dashboard` board↔list: use `SegmentedControl` (or `Tabs`) for the view toggle
      instead of bespoke buttons; `Tooltip` on truncated titles / status dots.
      (SegmentedControl now drives the roadmap board/list toggle — the app's only board↔list
      toggle — replacing the bespoke `blue-600` buttons; `Tooltip` wraps the truncated board
      card titles. e2e toggle selector updated button→radio. UX-12.1 `ddf25cf5`: tc/lint/test 162 ✓/build/e2e 18 ✓)
- [x] **12.2** Item detail: move row/item actions into an `ActionMenu`, and render the item's
      activity/comment history with `Timeline`.
      (Edit + Delete now live in an `ActionMenu` in the page header; comments render via
      `Timeline`. UX-12.2 `32dac7d4`: tc/lint/test 162 ✓/build/e2e 18 ✓.)
- [ ] 🔒 **12.3 — BLOCKED ON BACKEND** _(stretch — needs HTML-capable description/comment storage)_
      Swap the plain description/comment `<textarea>` for `RichTextEditor`, and render saved content
      with `RichTextViewer`. **Only do this if** the backend stores/returns rich HTML safely.
      **Verify per task:** `pnpm typecheck && pnpm lint && pnpm build`.
      DEFERRED (data-gated): `TrackerItem.description` and `Comment.body` are plain `string`s
      rendered with `whitespace-pre-wrap`; the `/api/tracker/*` proxy does not store or sanitize
      rich HTML. Adopting `RichTextEditor` would persist HTML with no backend sanitization (XSS
      risk) and mismatch the plain-text model, so `@bytelyst/rich-text` is intentionally not
      adopted until the backend supports safe rich HTML. No dep added. **Kept out of the ✅ count.**

      > **Required platform-service change** (see [`BACKEND_ENABLERS.md`](BACKEND_ENABLERS.md) §BE-1):
      > Add server-side HTML sanitization on the **write** paths for `items.description` and
      > `comments.body` in `services/platform-service` (the items + comments modules, before persist).
      > The sanitizer must apply a strict allowlist of formatting tags (e.g. `p`, `br`, `strong`,
      > `em`, `u`, `s`, `a`, `ul`/`ol`/`li`, `blockquote`, `code`, `pre`, headings) and safe attributes
      > (`a[href]` restricted to `http`/`https`/`mailto`), and **strip** all `<script>`/`<style>`,
      > inline event-handler attributes (`on*`), and `javascript:`/`data:` URLs. Sanitization must run
      > on the server (never trust the client), preserve the existing plain-text reads for old rows,
      > and be backward-compatible since the field is shared by every product. Once stored HTML is
      > guaranteed safe, tracker-web can adopt `RichTextEditor` (compose) + `RichTextViewer` (render)
      > with no client-side `dangerouslySetInnerHTML` of unsanitized content.

## UX-13 — Notifications surface via `@bytelyst/notifications-ui` (stretch / data-gated)

> **Gate:** only build this if tracker has (or will get) a notifications data source. If there
> is no feed, ship just the `BannerStack` for client-side system messages and leave the rest `- [ ]`.

- [ ] 🔒 **13.1 — BLOCKED ON BACKEND** Add `@bytelyst/notifications-ui`; mount a `NotificationCenter`
      bell in the header (or AppShell from UX-8) fed by the notifications API, with `InboxItem` rows.
      DEFERRED (data-gated): tracker exposes no notifications feed — the `/api/tracker/*` proxy
      surfaces only items/comments/votes/roadmap, with no notifications endpoint. Per the wave
      gate, `NotificationCenter`/`InboxItem` are left unbuilt until a feed exists. The dep is
      added and the imports are smoke-tested so a future feed wiring starts from green.
      **Kept out of the ✅ count.**

      > **Required platform-service change** (see [`BACKEND_ENABLERS.md`](BACKEND_ENABLERS.md) §BE-2):
      > Emit notifications into platform-service's **existing notifications module** on tracker
      > events — at minimum **new comment**, **status change**, and **vote milestones** — fanned out
      > to the item author and any subscribers/watchers, each stamped with `productId` so the feed
      > stays product-scoped. Expose the resulting feed (list + unread count + mark-as-read) through
      > the `/api/tracker` proxy so the dashboard can read it with the existing token, and keep the
      > payload shape compatible with `@bytelyst/notifications-ui` `InboxItem`. The change must be
      > additive/backward-compatible (platform-service is shared by 9 products — no behavioural change
      > for products that do not subscribe). Once the feed exists, tracker-web binds `NotificationCenter`
      > to it (bell + `InboxItem` rows) in the AppShell sidebar/header.

- [x] **13.2** Use `BannerStack` for top-of-page system/maintenance messages and `Announcement`
      for a dismissible "what's new" pill. **Verify:** `pnpm typecheck && pnpm lint && pnpm build`.
      (Added `@bytelyst/notifications-ui` workspace dep with a minimal `link:` lockfile entry;
      `SystemBanners` mounts `BannerStack` (env `NEXT_PUBLIC_SYSTEM_NOTICE`, dismissible) +
      a localStorage-dismissible `Announcement` at the top of the dashboard shell. UX-13.2
      `3d22c303`: tc/lint/test 165 ✓/build/e2e 18 ✓.)

## Cross-cutting (run continuously)

- [x] **CC.1** Full suite green after every wave: `pnpm typecheck && pnpm lint && pnpm test && pnpm build`.
      (Run after every UX-9…13 commit; final state UX-13.2: typecheck ✓ / lint ✓ / test 165 ✓ /
      build ✓ / e2e 18 ✓.)
- [x] **CC.2** Dark mode parity — every migrated surface looks correct in `.dark`.
      (Achieved structurally: the extended `--bl-*` bridge maps every adopted token onto tracker's
      OKLCH vars — which already flip under `.dark` — or `color-mix` of them, plus semantic
      `--success/--warning/--info` defined per-theme. No adopted component carries a fixed color.)
- [x] **CC.3** Zero new hardcoded color literals (grep `slate-|blue-600|#[0-9a-f]{6}` in changed files).
      (`git diff <UX-base>..HEAD -- src` → no new `slate-N` / `blue-600` / `#hex` / `bg-[#…]`; in fact
      UX-12.1 removed the roadmap toggle's `blue-600` literals.)
- [x] **CC.4** a11y — if `@axe-core/playwright` runs offline, assert no new violations on `/roadmap`
      and `/login`; otherwise Vitest render assertions for roles/labels.
      (axe-core runs offline in the Playwright suite asserting no serious/critical violations on
      `/login` and `/roadmap`; both green. Added aria-labels where the shared `LoginForm` shipped
      placeholder-only inputs.)
- [x] **CC.5** Update master `docs/ROADMAP.md` Phase 2.1 checkboxes **only if** the operator asks —
      default: leave it, this doc is the source of truth for the integration.
      (Default applied: master `docs/ROADMAP.md` left untouched — operator did not request it.)
- [x] **CC.6** **UI-drift ratchet:** once UX-9 lands, add an ESLint `no-restricted-imports` rule
      forbidding direct `@bytelyst/ui` imports outside `src/components/ui/Primitives.tsx`, so all
      future UI goes through the adapter. Fix any existing violations in the same commit.
      (Done: rule + adapter exemption in `eslint.config.mjs`; no pre-existing violations; probe confirms it fires.)
- [x] **CC.7** **Bundle budget:** after chart/motion/rich-text waves, re-check the `bundlesize`
      budgets in `package.json`. If a route grows, prefer dynamic `import()` for heavy surfaces
      (charts, rich-text editor) over raising a budget. Note the gzipped sizes in the log.
      (`bundlesize` itself is env-blocked — `iltorb` won't build on Node 25, see TEST_VALIDATION_LOG —
      so budgets were verified by gzipping the built chunks: all within budget, no budget raised.
      Largest: framework 58.3 kB / 100, main 37.3 kB / 50, layout 3.6 kB / 150; heaviest route
      `/dashboard/items` 6.9 kB / 30, `/roadmap` 3.8 kB / 30, `/login` 3.1 kB / 30. No dynamic
      import needed — charts/motion/rich-text waves were not adopted in this scope.)
- [x] **CC.8** **Coverage ratchet:** keep the enforced Vitest thresholds (80%) green; each wave
      that adds a component should add the render/export test that keeps `Primitives.tsx` and new
      surfaces above threshold (no silently-dead gates — see `docs/TEST_VALIDATION_LOG.md`).
      (Enforced thresholds stay green: 94.36% stmts / 86.58% branch / 94.28% funcs / 96.99% lines,
      165 tests. Added export/import smoke tests for the Primitives adapter, auth-ui and
      notifications-ui. Note: v8 reports the pure re-export `Primitives.tsx` barrel as 0% — there
      are no executable bodies to instrument — but the global gate is green and the export test
      guards every re-export.)

## Progress

```
Core   : UX-1 ✅  UX-2 ✅  UX-3 ✅  UX-4 ✅  UX-5 ✅  UX-6 ✅  UX-7 ✅  UX-8 ✅
Expand : UX-9 ✅  UX-10 ✅  UX-11 ✅  UX-12 ✅  UX-13 ✅
Backend-blocked (not in ✅ count) : UX-12.3 🔒  UX-13.1 🔒   (see BACKEND_ENABLERS.md)
```

**All client-only waves are complete.** Every task that can ship from `dashboards/tracker-web`
alone (Core UX-2 … UX-8 and Expand UX-9 … UX-13) is done. The only remaining items — **UX-12.3**
(rich-text) and **UX-13.1** (notifications feed) — are **🔒 blocked on a shared `platform-service`
change**, not on tracker-web. They are tracked as follow-ups in
[`BACKEND_ENABLERS.md`](BACKEND_ENABLERS.md) and are intentionally excluded from the ✅ count until
the backend enabler lands.

**UX-1 is done** (token bridge + Primitives adapter, commit `dc01dd02`) — the `--bl-*` bridge is
live, so shared components already inherit tracker's theme. **Start with UX-2.** Before editing,
read `src/app/globals.css` (the `--bl-*` bridge), `src/components/ui/Primitives.tsx` (the adapter
all app code imports from), and `src/app/dashboard/items/page.tsx` (the working `data-table`
adoption — the reference pattern). Execute **one task at a time**, verify + commit after each, and
do the **Core** waves (UX-2 … UX-8) before the **Expand** waves (UX-9 … UX-13). Treat 12.3 and all
of UX-13 as data-gated stretch — leave them `- [ ]` with a note if the backend can't support them.

---

## Status — Expand waves complete (2026-05-28)

The **Expand** waves (UX-9 … UX-13) and **all cross-cutting items (CC.1 … CC.8)** are done,
strictly scoped to `dashboards/tracker-web`. Each item was verified
(`typecheck` + `lint` + `test` + `build` + Playwright e2e/axe) and shipped as its own
conventional commit referencing its roadmap ID:

| Wave / item | Commit     | Summary                                                    |
| ----------- | ---------- | ---------------------------------------------------------- |
| UX-9        | `18a09b25` | Broaden Primitives adapter + export-presence test          |
| CC.6        | `73d2891d` | `no-restricted-imports` UI-drift ratchet                   |
| UX-10       | `3a9621f0` | `ErrorPage` + `PageHeader` + `LoadingSpinner` page chrome  |
| UX-1.2 ext  | `ccee7dfa` | Full `--bl-*` token bridge (enables CC.2 dark-mode parity) |
| UX-11       | `328e3072` | `LoginForm` + `MfaChallenge` on the login surface          |
| UX-12.1     | `ddf25cf5` | `SegmentedControl` view toggle + board `Tooltip`s          |
| UX-12.2     | `32dac7d4` | item-detail `ActionMenu` + `Timeline` comments             |
| UX-13.2     | `3d22c303` | `BannerStack` + `Announcement` system messaging            |

**🔒 Blocked on backend (data-gated, not in the ✅ count):** UX-12.3 (`RichTextEditor` — needs
server-side HTML sanitization on `items.description` + `comments.body`) and UX-13.1
(`NotificationCenter` — needs a real notifications feed). Both are owned follow-ups tracked in
[`BACKEND_ENABLERS.md`](BACKEND_ENABLERS.md); the needed client deps are already wired/smoke-tested
so enablement starts from green once the shared `platform-service` change ships.

**Core waves UX-2 … UX-8 are complete** (see the per-item SHAs above) — every wave that can ship
from `dashboards/tracker-web` alone is done. Nothing in this roadmap remains that is unblocked at
the tracker-web layer.
