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:
parent
b2539f21d0
commit
df72199cd1
@ -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)
|
||||
|
||||
|
||||
@ -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',
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user