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)
|
## Ground rules (non-negotiable)
|
||||||
|
|
||||||
1. **Scope lock:** edit only files under `dashboards/admin-web/`. Never edit shared
|
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
|
## 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
|
`globals.css` (light + dark); add ESLint `no-restricted-imports` forbidding direct
|
||||||
`@bytelyst/ui` imports outside `Primitives.tsx`; add a Primitives export-presence test. Capture
|
`@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`
|
- [ ] **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`
|
`KpiCard`/`Sparkline` for stat tiles), lazy-loaded; render tests (no NaN in SVG). Drop `recharts`
|
||||||
if fully unused afterward.
|
if fully unused afterward.
|
||||||
@ -99,14 +154,28 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
|
|||||||
## Progress tracker
|
## Progress tracker
|
||||||
|
|
||||||
```
|
```
|
||||||
Setup : UX-1 ⬜
|
Setup : UX-1 ✅
|
||||||
Adopt : UX-2 ⬜ UX-3 ⬜ UX-4 ⬜ UX-5 ⬜ UX-6 ⬜(cond)
|
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 ⬜
|
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)
|
## Deferrals (fill in as encountered)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||||
import nextTs from "eslint-config-next/typescript";
|
import nextTs from 'eslint-config-next/typescript';
|
||||||
|
|
||||||
const eslintConfig = defineConfig([
|
const eslintConfig = defineConfig([
|
||||||
// Ignores MUST come first so they apply to every subsequent
|
// 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
|
// at index 0 is the documented way to make eslint v9 skip files
|
||||||
// entirely before any config rules apply.
|
// entirely before any config rules apply.
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: ['**/.pnpmfile.cjs', '**/*.cjs'],
|
||||||
"**/.pnpmfile.cjs",
|
|
||||||
"**/*.cjs",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
...nextVitals,
|
...nextVitals,
|
||||||
...nextTs,
|
...nextTs,
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
"react-hooks/set-state-in-effect": "off",
|
'react-hooks/set-state-in-effect': 'off',
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }],
|
'@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.
|
// Override default ignores of eslint-config-next.
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
// Default ignores of eslint-config-next:
|
// Default ignores of eslint-config-next:
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
// .pnpmfile.cjs is a pnpm install hook (CommonJS by design).
|
// .pnpmfile.cjs is a pnpm install hook (CommonJS by design).
|
||||||
// The TypeScript no-require-imports rule would otherwise flag
|
// The TypeScript no-require-imports rule would otherwise flag
|
||||||
// every require() call in this file. eslint-config-next does NOT
|
// every require() call in this file. eslint-config-next does NOT
|
||||||
// ignore .cjs by default.
|
// ignore .cjs by default.
|
||||||
".pnpmfile.cjs",
|
'.pnpmfile.cjs',
|
||||||
"**/*.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);
|
--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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user