feat(admin-web): --bl-* token bridge + UI-drift ratchet + baseline audit (UX-1)

Additive phase-1 foundation for ByteLyst UX integration:
- globals.css: bridge the shared --bl-* contract onto admin's shadcn OKLCH
  ramp (surfaces/borders/text/accent/danger/focus) so @bytelyst/* components
  theme correctly in light AND dark. Mappings reference admin --* vars that
  flip under .dark, so parity is inherited with zero new color literals.
  Status hues (success/warning/info) intentionally inherit design-tokens.
- eslint.config.mjs: no-restricted-imports ratchet forbidding direct
  @bytelyst/ui imports outside the Primitives.tsx adapter seam.
- primitives-exports.test.ts: export-presence guard for the adapter surface.
- roadmap: author verified baseline audit + green/red gate table + e2e baseline.

Verify: typecheck+lint+build green; vitest 17 files / 140 tests (+29);
format:check no new failures (29 pre-existing, out of scope); e2e baseline
11 passed / 80 failed (80 environmental — no backend).

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 13:25:00 -07:00
parent b2539f21d0
commit df72199cd1
4 changed files with 241 additions and 20 deletions

View File

@ -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** `<SHA-UX1>` · 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)

View File

@ -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',
]),
]);

View File

@ -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');
});
});

View File

@ -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;