diff --git a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md index 0478e3d0..a9846be7 100644 --- a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md +++ b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md @@ -34,6 +34,59 @@ with `@bytelyst/ui` equivalents. Leave them as-is; only NEW UI and the waves bel --- +## 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 @@ -69,10 +122,12 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa ## Waves -- [ ] **UX-1 — Token bridge + UI-drift ratchet (FIRST):** add a `--bl-*` → admin OKLCH bridge in +- [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. + the e2e baseline below. — **DONE** `` · 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). - [ ] **UX-2 — Charts:** migrate the ~6 `recharts` usages to `@bytelyst/charts` (+ `@bytelyst/data-viz` `KpiCard`/`Sparkline` for stat tiles), lazy-loaded; render tests (no NaN in SVG). Drop `recharts` if fully unused afterward. @@ -99,14 +154,28 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa ## Progress tracker ``` -Setup : UX-1 ⬜ +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 (fill on UX-1) +## E2e baseline (captured on UX-1 — 2026-05-29) -_(record `pnpm --filter @bytelyst/admin-web test:e2e` passed/failed here; each wave must add no new failures)_ +`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) diff --git a/dashboards/admin-web/eslint.config.mjs b/dashboards/admin-web/eslint.config.mjs index 0e5c7d42..f36afcd7 100644 --- a/dashboards/admin-web/eslint.config.mjs +++ b/dashboards/admin-web/eslint.config.mjs @@ -1,6 +1,6 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import nextTs from 'eslint-config-next/typescript'; const eslintConfig = defineConfig([ // Ignores MUST come first so they apply to every subsequent @@ -11,32 +11,67 @@ const eslintConfig = defineConfig([ // at index 0 is the documented way to make eslint v9 skip files // entirely before any config rules apply. { - ignores: [ - "**/.pnpmfile.cjs", - "**/*.cjs", - ], + ignores: ['**/.pnpmfile.cjs', '**/*.cjs'], }, ...nextVitals, ...nextTs, { rules: { - "react-hooks/set-state-in-effect": "off", - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }], + 'react-hooks/set-state-in-effect': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }, + ], + }, + }, + // UX-drift ratchet (UX-1): shared `@bytelyst/ui` primitives must be adopted + // through the local adapter `src/components/ui/Primitives.tsx`, never imported + // directly into pages/components. This keeps product-specific variants, the + // `--bl-*` token bridge, and a single migration seam in one place (the + // exception below). Mirrors tracker-web's convention. + { + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@bytelyst/ui', + message: + "Import shared UI primitives via '@/components/ui/Primitives' instead of '@bytelyst/ui' directly (UX-drift ratchet — see docs/roadmaps/UX_INTEGRATION_ADMIN.md).", + }, + ], + patterns: [ + { + group: ['@bytelyst/ui/*'], + message: + "Import shared UI primitives via '@/components/ui/Primitives' instead of '@bytelyst/ui/*' directly (UX-drift ratchet — see docs/roadmaps/UX_INTEGRATION_ADMIN.md).", + }, + ], + }, + ], + }, + }, + // The Primitives adapter is the ONE sanctioned seam for `@bytelyst/ui`. + { + files: ['src/components/ui/Primitives.tsx'], + rules: { + 'no-restricted-imports': 'off', }, }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', // .pnpmfile.cjs is a pnpm install hook (CommonJS by design). // The TypeScript no-require-imports rule would otherwise flag // every require() call in this file. eslint-config-next does NOT // ignore .cjs by default. - ".pnpmfile.cjs", - "**/*.cjs", + '.pnpmfile.cjs', + '**/*.cjs', ]), ]); diff --git a/dashboards/admin-web/src/__tests__/primitives-exports.test.ts b/dashboards/admin-web/src/__tests__/primitives-exports.test.ts new file mode 100644 index 00000000..11d42756 --- /dev/null +++ b/dashboards/admin-web/src/__tests__/primitives-exports.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import * as Primitives from '@/components/ui/Primitives'; + +/** + * UX-1 — Primitives export-presence guard. + * + * `src/components/ui/Primitives.tsx` is the single sanctioned adapter for the + * shared `@bytelyst/ui` layer (enforced by the `no-restricted-imports` ratchet + * in `eslint.config.mjs`). If a re-export silently disappears, every consumer + * breaks at build time — this test fails fast instead, and documents the + * surface the rest of admin-web is allowed to rely on. + */ + +// Product-specific component implementations + helpers defined locally. +const LOCAL_EXPORTS = [ + 'Button', + 'IconButton', + 'Input', + 'Select', + 'Textarea', + 'Badge', + 'ProductStatusBadge', + 'statusToneFor', +] as const; + +// Shared `@bytelyst/ui` primitives re-exported through the adapter. +const SHARED_REEXPORTS = [ + 'ActionMenu', + 'AlertBanner', + 'DataList', + 'DataTable', + 'Drawer', + 'EmptyState', + 'EntityCard', + 'Field', + 'FieldLabel', + 'FilterBar', + 'FormSection', + 'MetricCard', + 'Modal', + 'PageHeader', + 'Panel', + 'PanelHeader', + 'PanelTitle', + 'Skeleton', + 'Timeline', + 'Toolbar', +] as const; + +describe('Primitives adapter exports', () => { + it.each(LOCAL_EXPORTS)('exposes local export %s', name => { + expect(Primitives[name as keyof typeof Primitives]).toBeDefined(); + }); + + it.each(SHARED_REEXPORTS)('re-exports shared @bytelyst/ui primitive %s', name => { + expect(Primitives[name as keyof typeof Primitives]).toBeDefined(); + }); + + it('maps product statuses to a known tone (helper is wired)', () => { + expect(Primitives.statusToneFor('active')).toBe('success'); + expect(Primitives.statusToneFor('failed')).toBe('error'); + expect(Primitives.statusToneFor(null)).toBe('neutral'); + expect(Primitives.statusToneFor('totally-unknown')).toBe('neutral'); + }); +}); diff --git a/dashboards/admin-web/src/app/globals.css b/dashboards/admin-web/src/app/globals.css index 7e117b75..c7066c64 100644 --- a/dashboards/admin-web/src/app/globals.css +++ b/dashboards/admin-web/src/app/globals.css @@ -118,6 +118,58 @@ --sidebar-ring: oklch(0.556 0 0); } +/* ── @bytelyst/ui token bridge (UX-1) ───────────────────────────────────── + * Shared `@bytelyst/*` components read a `--bl-*` contract. By default that + * contract is supplied by `@bytelyst/design-tokens/css` (imported above), but + * those defaults live under `:root, [data-theme="dark"]` — i.e. they are the + * DARK palette, and they switch via a `[data-theme]` attribute that admin-web + * does NOT use (admin toggles a `.dark` *class*). Without this bridge, shared + * components would render with dark token values in admin's light mode. + * + * This block re-points the `--bl-*` contract onto admin's shadcn OKLCH ramp. + * Every mapping references an admin `--*` var that already flips between + * `:root` and `.dark`, so light + dark parity is inherited automatically with + * ZERO new color literals (declared last in this file, so it wins the cascade + * over the design-tokens import). + * + * Status hues (`--bl-success` / `--bl-warning` / `--bl-info` + their -muted / + * -border variants) intentionally inherit the shared design-tokens palette: + * admin's shadcn ramp has no semantic success/warning/info token to map to, + * and authoring color literals here is disallowed. + * ------------------------------------------------------------------------- */ +:root { + /* surfaces */ + --bl-bg-canvas: var(--background); + --bl-bg-elevated: var(--card); + --bl-surface-card: var(--card); + --bl-surface-muted: var(--muted); + --bl-surface-highlight: var(--accent); + --bl-surface-overlay: var(--popover); + --bl-surface-sidebar: var(--sidebar); + --bl-input: var(--input); + /* borders */ + --bl-border: var(--border); + --bl-border-strong: var(--ring); + --bl-border-subtle: color-mix(in oklab, var(--border) 60%, transparent); + /* text */ + --bl-text-primary: var(--foreground); + --bl-text-secondary: var(--muted-foreground); + --bl-text-tertiary: var(--muted-foreground); + --bl-text-quiet: var(--muted-foreground); + /* accent / primary */ + --bl-accent: var(--primary); + --bl-accent-foreground: var(--primary-foreground); + --bl-accent-muted: color-mix(in oklab, var(--primary) 16%, transparent); + /* danger ← shadcn destructive */ + --bl-danger: var(--destructive); + --bl-danger-foreground: var(--primary-foreground); + --bl-danger-muted: color-mix(in oklab, var(--destructive) 16%, transparent); + --bl-danger-border: color-mix(in oklab, var(--destructive) 40%, transparent); + /* focus */ + --bl-focus-ring: var(--ring); + --bl-focus-ring-muted: color-mix(in oklab, var(--ring) 40%, transparent); +} + @layer base { * { @apply border-border outline-ring/50;