mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
feat(dashboard): unified visual identity — grid, primitives, tables, form controls (design phases 1-4) (#4122)
Unified visual identity (design phases 1-4): grid, primitives, tables, form controls. Integrado em release/v3.8.29: clsx + tailwind-merge adicionados à dependency-allowlist (já declarados no package.json, consumidos por src/shared/utils/cn.ts) para o check:deps anti-slopsquatting passar.
This commit is contained in:
committed by
GitHub
parent
d828e49b9d
commit
541ff3bfa7
@@ -38,6 +38,7 @@
|
||||
"better-sqlite3",
|
||||
"bottleneck",
|
||||
"c8",
|
||||
"clsx",
|
||||
"commander",
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
@@ -106,6 +107,7 @@
|
||||
"socks",
|
||||
"sql.js",
|
||||
"sqlite-vec",
|
||||
"tailwind-merge",
|
||||
"tailwindcss",
|
||||
"tls-client-node",
|
||||
"tsup",
|
||||
|
||||
218
design.md
Normal file
218
design.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# OmniRoute — Design System & Visual Identity
|
||||
|
||||
> **Status:** standardization plan. **Phases 1–3 are implemented in this PR** (grid, primitives, status-color centralization, mono token, and the DataTable token migration). The DataTable migration is **faithful** — dark stays byte-identical (the new `--table-*` dark values equal the old hardcoded rgba); light is fixed (it was buggy always-dark via dead `var()` fallbacks). **⚠️ Wants a visual pass before merge** (light-theme tables + the secondary-text shift `#888`→`--color-text-muted`). Phase 4 still pending; note several remaining "hardcoded" hex are _intentional_ (always-dark console terminal, ReactFlow SVG strokes) and must NOT be swept.
|
||||
> **Date:** 2026-06-16 · **Scope:** unify the OmniRoute dashboard (`src/`) with the marketing site (`_mono_repo/omnirouteSite/`) into **one visual identity** — same graph-paper grid background, same color tokens, standardized components.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
The marketing site (`viral.omniroute.online`, `why.omniroute.online`, `omniroute.online`) and the product dashboard should look like **one product**. The site already borrowed its palette from the dashboard — its `css/tokens.css` even says _"Palette mirrors the OmniRoute dashboard (src/app/globals.css)"_. So the two are already ~80% aligned at the color level. What's missing on the dashboard:
|
||||
|
||||
1. The **graph-paper grid wallpaper** the site uses on every page.
|
||||
2. A handful of **shared design tokens** the site has but the dashboard lacks (radius scale, brand gradient, `surface-2`, mono font).
|
||||
3. **Component-level consistency** — a number of dashboard components bypass the theme tokens with hardcoded hex/rgba.
|
||||
|
||||
This document is the analysis and the plan.
|
||||
|
||||
---
|
||||
|
||||
## 2. Principles
|
||||
|
||||
- **Single source of truth = `src/app/globals.css`.** The site mirrors the dashboard, never the other way around. New tokens land in `globals.css` first.
|
||||
- **Tokens, never literals.** Components consume semantic tokens (`bg-surface`, `text-primary`, `border-border`), never raw `#hex`.
|
||||
- **Subtle, not loud.** The grid is a faint wallpaper that sits behind content — it must never reduce text contrast or fight the UI.
|
||||
- **Theme-aware.** Everything works in both `.dark` (the product's signature look) and light.
|
||||
- **Surgical rollout.** Ship the grid + tokens first (low risk, high visibility), then component cleanups in waves.
|
||||
|
||||
---
|
||||
|
||||
## 3. Current state — what's already aligned vs. what's not
|
||||
|
||||
### 3.1 Colors — already unified ✅
|
||||
|
||||
Every brand color and surface already matches the site **by value** (only the names differ — dashboard prefixes with `--color-`). Verified in `src/app/globals.css:30-128`:
|
||||
|
||||
| Concept | Site token (`tokens.css`) | Dashboard token (`globals.css`) | Match |
|
||||
| -------------------------- | ------------------------------------------- | ------------------------------- | ------------ |
|
||||
| primary | `--primary #e54d5e` | `--color-primary #e54d5e` | ✅ |
|
||||
| primary-hover | `--primary-hover #c93d4e` | `--color-primary-hover #c93d4e` | ✅ |
|
||||
| accent | `--accent #6366f1` | `--color-accent #6366f1` | ✅ |
|
||||
| accent-2 | `--accent-2 #8b5cf6` | `--color-accent-hover #8b5cf6` | ✅ (renamed) |
|
||||
| accent-3 | `--accent-3 #a855f7` | `--color-accent-light #a855f7` | ✅ (renamed) |
|
||||
| success / warning / error | `#22c55e / #f59e0b / #ef4444` | identical | ✅ |
|
||||
| traffic lights | `#ff5f56 / #ffbd2e / #27c93f` | identical | ✅ |
|
||||
| dark bg / surface / border | `#0b0e14 / #161b22 / rgba(255,255,255,.08)` | identical | ✅ |
|
||||
| light bg / surface / text | `#f9f9fb / #fff / #1a1a2e` | identical | ✅ |
|
||||
|
||||
**Conclusion:** there is no color migration to do. The identity is already shared; we are _finishing_ it, not rebuilding it.
|
||||
|
||||
### 3.2 Gaps — what the dashboard is missing
|
||||
|
||||
| Gap | Site has | Dashboard | Action |
|
||||
| ----------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | ---------------------- |
|
||||
| **Grid wallpaper** | `body::before` graph-paper, `--grid-line`, `--grid-size 46px`, `--section-alt` | **✅ added (Phase 1)** | **Part A** |
|
||||
| **Radius scale** | `--radius 14px`, `--radius-sm 9px` | `--radius 14px` added; `-sm` + component repoint pending | **Part B / Phase 2** |
|
||||
| **Brand gradient** | `--grad-brand 135deg primary→accent-3` | **✅ token added (Phase 1)**; consumed in Phase 2 | **Part B** |
|
||||
| **Nested surface** | `--surface-2 #1c2230` | **✅ added (Phase 1)** | **Part B** |
|
||||
| **Mono font** | `--font-mono` (ui-monospace stack) | pending (Phase 4, with consumers) | **Part B** |
|
||||
| **`text-muted` (dark)** | `#8b8b9e` | `#a1a1aa` (zinc-400) | reconcile — **Part B** |
|
||||
|
||||
### 3.3 Theming mechanics (so we don't break anything)
|
||||
|
||||
- **Tailwind v4, CSS-first** (no `tailwind.config.*`). Tokens are defined in `:root`/`.dark` and exposed to utilities via `@theme inline` (`globals.css:130-179`).
|
||||
- **Dark via `.dark` class** on `<html>` (`@custom-variant dark` at `globals.css:22`), toggled by a custom Zustand store (`src/store/themeStore.ts`), default theme = `system` (`src/shared/constants/appConfig.ts:11`). The site uses `html[data-theme="light"]` instead — **the mechanisms differ but never meet** (separate origins), so no conflict. We keep the dashboard's `.dark` mechanism.
|
||||
- **Runtime primary override** exists (`themeStore.ts:85-97`, presets in `COLOR_THEMES`) — users can swap `--color-primary`. Any new token (gradient, etc.) that references `--color-primary` inherits those overrides for free. ✅
|
||||
- **Tailwind v4 reserved radius names:** `--radius-sm/md/lg/...` back the `rounded-*` utilities. Redefining them retroactively changes every existing `rounded-*` (e.g. `rounded-sm` is used in 12 files). So the small-radius value and component repoint are deliberately deferred to Phase 2, where consumers change together.
|
||||
|
||||
---
|
||||
|
||||
## 4. Part A — The graph-paper grid background (headline ask) — IMPLEMENTED (Phase 1)
|
||||
|
||||
### 4.1 What it is
|
||||
|
||||
The exact recipe from the site (`_mono_repo/omnirouteSite/css/base.css`): a **fixed, full-viewport pseudo-element** painting two 1px line gradients, sitting at `z-index:-1` behind all content.
|
||||
|
||||
```css
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
|
||||
background-size: var(--grid-size) var(--grid-size);
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works even though `body` has an opaque `background-color`:** a `::before` with `z-index:-1` paints _above_ the element's own background but _below_ its in-flow content. So `--color-bg` is the base fill, the grid is layered on top of it, and the app renders above the grid.
|
||||
|
||||
### 4.2 Precedent already in the codebase
|
||||
|
||||
`src/app/landing/page.tsx:16-26` **already implements this same grid per-page** — but with **red** lines (`#E54D5E`, opacity `0.06`) at **50px**, plus animated orbs. So the pattern is proven in the product; this work promotes it to a **global, theme-aware** wallpaper.
|
||||
|
||||
### 4.3 Tokens added (in `globals.css`)
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* light */
|
||||
--grid-line: rgba(0, 0, 0, 0.045);
|
||||
--grid-size: 46px;
|
||||
--section-alt: rgba(0, 0, 0, 0.022);
|
||||
}
|
||||
.dark {
|
||||
/* dark */
|
||||
--grid-line: rgba(255, 255, 255, 0.035);
|
||||
--section-alt: rgba(255, 255, 255, 0.018);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 The single blocker — removed
|
||||
|
||||
The grid is global by construction (it covers the panel, `auth`/`login`, error pages — every route — at once). Exactly **one** element hid it inside the panel:
|
||||
|
||||
- `src/shared/components/layouts/DashboardLayout.tsx` — the outer wrapper painted an opaque `bg-bg`. Everything below it is already transparent (`<main>`, the scroll container, the `max-w-7xl` inner), so **removing `bg-bg`** lets the body grid show through the content area (the body's `--color-bg` remains the base fill).
|
||||
|
||||
```diff
|
||||
- <div className="flex h-dvh min-h-0 w-full overflow-hidden bg-bg">
|
||||
+ <div className="flex h-dvh min-h-0 w-full overflow-hidden">
|
||||
```
|
||||
|
||||
### 4.5 Chrome interaction (sidebar / header)
|
||||
|
||||
- `Header` (`Header.tsx:207`, `bg-bg`) and `Sidebar` (`Sidebar.tsx:430`, `bg-sidebar`) stay **opaque** → the grid shows in the **content area only**, with solid chrome framing it. Calm default, matches how the site separates chrome from canvas (decision D3 = solid).
|
||||
|
||||
### 4.6 Login / auth / error pages
|
||||
|
||||
These render directly under `<body>` (no panel chrome) and their wrappers are mostly transparent — the global grid appears behind them automatically. Exception: `login/page.tsx:124,139` uses opaque `bg-bg` wrappers; softening them (D4) is a small Phase 2 follow-up.
|
||||
|
||||
### 4.7 Landing page
|
||||
|
||||
`landing/page.tsx` keeps its richer animated background (orbs + vignette) — its own marketing splash (decision D5 = leave as-is).
|
||||
|
||||
---
|
||||
|
||||
## 5. Part B — Token unification
|
||||
|
||||
Phase 1 adds the inert, collision-free identity tokens (`--surface-2`/`--color-surface-2`, `--grad-brand`, `--radius`). Phase 2 wires the radius scale into Tailwind and repoints components; Phase 4 adds `--font-mono` with its consumers.
|
||||
|
||||
| Token | Why | Phase |
|
||||
| -------------------------- | --------------------------------------------------------------- | ------------------------------ |
|
||||
| `--radius` / `--radius-sm` | One radius scale (14/9) instead of 6/8/12 ad-hoc | 1 (value) / 2 (wire + repoint) |
|
||||
| `--grad-brand` | Brand gradient for primary CTAs (red→violet), matching the site | 1 (token) / 2 (Button) |
|
||||
| `--surface-2` | Nested panels / table headers / inset rows | 1 |
|
||||
| `--font-mono` | Code blocks, terminal, IDs, endpoints | 4 |
|
||||
| `--text-muted` reconcile | Pick one value site↔panel (`#a1a1aa` recommended) | 2 |
|
||||
|
||||
**D2 (text-muted):** site `#8b8b9e` vs dashboard `#a1a1aa`. Recommend keeping the **dashboard's `#a1a1aa`** and updating the _site_ to match. Cosmetic.
|
||||
|
||||
---
|
||||
|
||||
## 6. Part C — Component standardization (Phases 2–4)
|
||||
|
||||
Custom components (no shadcn/Radix), Tailwind v4, semantic tokens **mostly** adopted (195 files import the shared barrel). The work is removing the **bypasses**. Home: `src/shared/components/`.
|
||||
|
||||
| # | Item | File(s) | Problem → Target | Phase |
|
||||
| --- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||
| C1 | **Radius alignment** | `Button.tsx:14-18`, `Card.tsx:39`, `Modal.tsx`, `Input.tsx`, `Select.tsx` | mixed 6/8/12px → `--radius`/`--radius-sm` (14/9) | 2 |
|
||||
| C2 | **Button gradient + `accent` variant** | `Button.tsx:5-12` | primary is flat red→red; align to `--grad-brand`; add missing `accent` variant. ~195 importers — highest visibility | 2 |
|
||||
| C3 | **Tables** | `DataTable.tsx:122-176`, `logTableStyles.ts`, `globals.css:405-414` | 100% inline hardcoded rgba + non-existent vars; migrate to tokens, retire divergent styles | 3 |
|
||||
| C4 | **Centralize status colors** | `flow/edgeStyles.ts`, `TokenHealthBadge.tsx`, `DegradationBadge.tsx`, `ProviderCascadeNode.tsx`, `Badge.tsx` + 5 helpers | 6+ copies of the same hex → one module off `--color-success/warning/error` | 3 |
|
||||
| C5 | **Card border** | `Card.tsx:39` | `border-white/5` → brand `/8` | 2 |
|
||||
| C6 | **Focus ring reconcile** | `globals.css:183` vs component `ring-primary/30` | global indigo vs component red → pick one | 4 |
|
||||
| C7 | **Add `Checkbox` + `Textarea`** | raw `<input>`/`<textarea>` w/ inline `accentColor:#6366f1` | token-driven primitives | 4 |
|
||||
| C8 | **Hardcoded-hex sweep** | `ConsoleLogViewer.tsx:240`, `ComboLiveStudio.tsx:306`, Modal dots, ~14 chart files | literals → tokens | 4 |
|
||||
| C9 | **`cn()` → clsx + tailwind-merge** | `src/shared/utils/cn.ts` | conflicting classes stack; needed for C1 overrides | 2 |
|
||||
|
||||
**Already on-brand (token-driven, only need radius):** `Badge`, `Toggle`, `SegmentedControl`, `Input`, `Select`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Rollout plan
|
||||
|
||||
- **Phase 1 — Grid + identity tokens (THIS PR).** `globals.css` grid + `--surface-2`/`--grad-brand`/`--radius` tokens; `body::before` wallpaper; remove the `bg-bg` blocker; static guard test. Low risk, reversible in one commit.
|
||||
- **Phase 2 — Primitives (C1, C2, C5) — DONE in this PR.** Semantic radius utilities `rounded-card` (14px) / `rounded-control` (9px) added via `@theme` (custom names, so the default `rounded-sm/md/lg/xl` stay untouched — no 400-file blast); Card/Modal → 14px, Button/Input/Select → 9px; Button primary → `--grad-brand` (red→violet) + new `accent` variant; Card borders → the `border-border` token (0.08). **Deferred:** `cn()`→tailwind-merge (C9) needs new deps; the ad-hoc `rounded-lg` sweep (326 files) is left as-is since the primitives carry the bulk of the surface.
|
||||
- **Phase 3 — Status colors + tables (C3, C4) — DONE in this PR.** ✅ **C4** (`src/shared/constants/statusColors.ts` — `STATUS_HEX` single source; `flow/edgeStyles.ts` + `TokenHealthBadge` repointed, faithful/same hex). ✅ **`--font-mono`** token. ✅ **C3 (DataTable)** — replaced every inline rgba + the dead `var(--bg-table-header)` / `var(--text-secondary)` fallbacks with a `--table-*` token set (`--table-header-bg/-row-zebra/-row-hover/-cell-border/-row-selected`) whose **dark values exactly equal the old hardcoded rgba** (dark byte-identical) and whose light values fix the previously-always-dark light theme. Header border → `--color-border`, secondary text → `--color-text-muted`. **Wants a visual pass before merge.** (Not touched: `logTableStyles.ts` and the legacy Ant `.ant-table` rules — separate, lower priority.)
|
||||
- **Phase 4 — Cleanup (C7, C9 done; C6, C8 pending).** ✅ **C9** `cn()` → `twMerge(clsx(...))` (clsx + tailwind-merge added as deps) — a caller's `className` now correctly _replaces_ a primitive's conflicting class instead of stacking. ✅ **C7** new `Checkbox` + `Textarea` primitives (token-driven, exported from the barrel; additive — adoption of the 32 raw checkboxes / 41 raw textareas can follow incrementally). ⏳ **C6** focus-ring reconcile (global indigo vs component red — taste, wants a visual pass). ⏳ **C8 hex-sweep is NOT a blind find/replace** — confirmed offenders that are _intentional_ and must stay: `ConsoleLogViewer.tsx:240` (always-dark terminal), `TokenHealthBadge` popover, ReactFlow SVG strokes. Only migrate hex genuinely meant to be theme-aware.
|
||||
|
||||
Each phase: `npm run lint` + `npm run typecheck:core` + a visual pass.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open decisions (recommendations)
|
||||
|
||||
- **D1 — Button primary:** keep red→red or switch to **red→violet `--grad-brand`**? Rec: **red→violet** (Phase 2).
|
||||
- **D2 — Grid line color:** **neutral** (site style) — chosen — vs brand-red. Size **46px**.
|
||||
- **D3 — Chrome vibrancy:** sidebar/header **solid** — chosen.
|
||||
- **D4 — Auth/login grid:** soften `login/page.tsx` wrappers (Phase 2). Rec: yes.
|
||||
- **D5 — Landing page:** leave animated splash as-is. Chosen.
|
||||
- **D6 — Radius 14/9 product-wide:** Rec: yes (Phase 2).
|
||||
- **D7 — Phase 1 ships first:** Chosen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Out of scope / risks
|
||||
|
||||
- **No palette change** — colors already match; we only add missing tokens. Zero risk of recoloring the product.
|
||||
- **No theme-engine change** — keep `.dark` + Zustand store.
|
||||
- **Radius shift (Phase 2) is broad** — touches every card/button/input; eyeball busy screens (tables, modals) before merge.
|
||||
- **Tables (C3)** carry the most hardcoded styling and the highest regression surface — isolate in their own PR.
|
||||
|
||||
---
|
||||
|
||||
## 10. Reference index
|
||||
|
||||
| Area | Path |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| Dashboard tokens | `src/app/globals.css` (`:root`, `.dark`, `@theme inline`, `body`, `body::before`) |
|
||||
| Theme store | `src/store/themeStore.ts`, `src/shared/components/ThemeProvider.tsx`, `src/shared/constants/appConfig.ts:9-11` |
|
||||
| Panel shell (grid unblocked here) | `src/shared/components/layouts/DashboardLayout.tsx` |
|
||||
| Chrome | `src/shared/components/Header.tsx:207`, `src/shared/components/Sidebar.tsx:430` |
|
||||
| Grid precedent | `src/app/landing/page.tsx:16-26` |
|
||||
| Primitives | `src/shared/components/{Button,Card,Input,Select,Badge,Modal,Toggle,SegmentedControl,Loading,Tooltip,DataTable}.tsx` |
|
||||
| Status-color sources | `flow/edgeStyles.ts`, `TokenHealthBadge.tsx`, `DegradationBadge.tsx`, `logTableStyles.ts` |
|
||||
| `cn` util | `src/shared/utils/cn.ts` |
|
||||
| Phase 1 guard test | `tests/unit/design-grid-background.test.ts` |
|
||||
| Site reference | `_mono_repo/omnirouteSite/css/tokens.css`, `css/base.css` |
|
||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -27,8 +27,8 @@
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.16.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "12.11.1",
|
||||
"bottleneck": "^2.19.5",
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"express": "^5.2.1",
|
||||
@@ -74,6 +74,7 @@
|
||||
"socks": "^2.8.7",
|
||||
"sql.js": "^1.14.1",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tsx": "^4.22.3",
|
||||
"undici": "^8.3.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"axios": "^1.16.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"bottleneck": "^2.19.5",
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"express": "^5.2.1",
|
||||
@@ -247,6 +248,7 @@
|
||||
"socks": "^2.8.7",
|
||||
"sql.js": "^1.14.1",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tsx": "^4.22.3",
|
||||
"undici": "^8.3.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
|
||||
@@ -86,6 +86,29 @@
|
||||
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.015);
|
||||
--shadow-warm: 0 2px 12px -2px rgba(229, 77, 94, 0.12);
|
||||
--shadow-elevated: 0 12px 28px -4px rgba(20, 20, 40, 0.06);
|
||||
|
||||
/* Graph-paper wallpaper grid — shared visual identity with the marketing site
|
||||
(_mono_repo/omnirouteSite/css). Light theme values. Rendered by body::before. */
|
||||
--grid-line: rgba(0, 0, 0, 0.045);
|
||||
--grid-size: 46px;
|
||||
--section-alt: rgba(0, 0, 0, 0.022);
|
||||
|
||||
/* Identity tokens mirrored from the site. Definitions only in this phase —
|
||||
components are repointed to them in a later phase. */
|
||||
--surface-2: #f5f5fa;
|
||||
--radius: 14px;
|
||||
--radius-control: 9px;
|
||||
--grad-brand: linear-gradient(135deg, var(--color-primary), var(--color-accent-light));
|
||||
|
||||
/* Data-table surfaces — theme-aware. The DARK values (in .dark below) exactly mirror
|
||||
the previous hardcoded rgba in DataTable.tsx so dark stays byte-identical; these
|
||||
light values are the fix (the old code was always-dark via dead var fallbacks). */
|
||||
--table-header-bg: rgba(249, 249, 251, 0.95);
|
||||
--table-row-zebra: rgba(0, 0, 0, 0.02);
|
||||
--table-row-hover: rgba(0, 0, 0, 0.04);
|
||||
--table-cell-border: rgba(0, 0, 0, 0.04);
|
||||
/* Selected row — an accent tint that reads on both themes (inherited by .dark). */
|
||||
--table-row-selected: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -125,6 +148,17 @@
|
||||
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-warm: 0 2px 12px -2px rgba(229, 77, 94, 0.15);
|
||||
--shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Graph-paper wallpaper grid — dark theme values. */
|
||||
--grid-line: rgba(255, 255, 255, 0.035);
|
||||
--section-alt: rgba(255, 255, 255, 0.018);
|
||||
--surface-2: #1c2230;
|
||||
|
||||
/* Data-table surfaces — dark = the exact old hardcoded values (byte-identical). */
|
||||
--table-header-bg: rgba(15, 15, 25, 0.95);
|
||||
--table-row-zebra: rgba(255, 255, 255, 0.02);
|
||||
--table-row-hover: rgba(255, 255, 255, 0.04);
|
||||
--table-cell-border: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -176,6 +210,16 @@
|
||||
/* Font - macOS system fonts */
|
||||
--font-sans:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", system-ui, sans-serif;
|
||||
/* Monospace stack (parity with the site) — code blocks, IDs, endpoints, terminals */
|
||||
--font-mono: ui-monospace, "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
|
||||
/* Nested surface (enables bg-surface-2) */
|
||||
--color-surface-2: var(--surface-2);
|
||||
|
||||
/* Semantic radius scale → rounded-card (surfaces, 14px) / rounded-control (controls,
|
||||
9px). Custom names so the default rounded-sm/md/lg/xl utilities stay untouched. */
|
||||
--radius-card: var(--radius);
|
||||
--radius-control: var(--radius-control);
|
||||
}
|
||||
|
||||
/* ── Focus Indicators (U-3) ── */
|
||||
@@ -211,6 +255,21 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Graph-paper wallpaper grid — one fixed, full-viewport layer behind every screen
|
||||
(dashboard, auth, error pages). A z-index:-1 ::before paints above body's own
|
||||
background but below all in-flow content, so it shows through any transparent
|
||||
wrapper without lowering text contrast. Mirrors the marketing site exactly. */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image: linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
|
||||
background-size: var(--grid-size) var(--grid-size);
|
||||
}
|
||||
|
||||
body.electron-macos {
|
||||
--desktop-safe-top: 30px;
|
||||
--desktop-safe-bottom: 8px;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
const variants = {
|
||||
primary: "bg-gradient-to-b from-primary to-primary-hover text-white shadow-sm",
|
||||
primary: "bg-[image:var(--grad-brand)] text-white shadow-sm hover:brightness-105",
|
||||
accent: "bg-accent text-white shadow-sm hover:bg-accent-hover",
|
||||
secondary:
|
||||
"bg-white dark:bg-white/10 border border-black/10 dark:border-white/10 text-text-main hover:bg-black/5 dark:hover:bg-white/5",
|
||||
outline: "border border-black/15 dark:border-white/15 text-text-main hover:bg-black/5",
|
||||
@@ -12,9 +13,9 @@ const variants = {
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: "h-7 px-3 text-xs rounded-md",
|
||||
md: "h-9 px-4 text-sm rounded-lg",
|
||||
lg: "h-11 px-6 text-sm rounded-lg",
|
||||
sm: "h-7 px-3 text-xs rounded-control",
|
||||
md: "h-9 px-4 text-sm rounded-control",
|
||||
lg: "h-11 px-6 text-sm rounded-control",
|
||||
};
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
@@ -36,8 +36,8 @@ export default function Card({
|
||||
<div
|
||||
className={cn(
|
||||
"bg-surface",
|
||||
"border border-black/5 dark:border-white/5",
|
||||
"rounded-lg shadow-sm",
|
||||
"border border-border",
|
||||
"rounded-card shadow-sm",
|
||||
hover && "hover:shadow-md hover:border-primary/30 transition-all cursor-pointer",
|
||||
paddings[padding],
|
||||
className
|
||||
@@ -76,7 +76,7 @@ Card.Section = function CardSection({ children, className, ...props }: CardSecti
|
||||
className={cn(
|
||||
"p-4 rounded-lg",
|
||||
"bg-black/[0.02] dark:bg-white/[0.02]",
|
||||
"border border-black/5 dark:border-white/5",
|
||||
"border border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -96,7 +96,7 @@ Card.Row = function CardRow({ children, className, ...props }: CardRowProps) {
|
||||
<div
|
||||
className={cn(
|
||||
"p-3 -mx-3 px-3 transition-colors",
|
||||
"border-b border-black/5 dark:border-white/5 last:border-b-0",
|
||||
"border-b border-border last:border-b-0",
|
||||
"hover:bg-black/[0.02] dark:hover:bg-white/[0.02]",
|
||||
className
|
||||
)}
|
||||
|
||||
38
src/shared/components/Checkbox.tsx
Normal file
38
src/shared/components/Checkbox.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox — token-driven native checkbox (brand accent + keyboard focus ring).
|
||||
* Replaces the ad-hoc `<input type="checkbox" style={{ accentColor: "#6366f1" }}>`
|
||||
* pattern scattered across the dashboard. Optional `label` wraps it in a clickable row.
|
||||
*/
|
||||
export default function Checkbox({ label, className, id, ...props }: CheckboxProps) {
|
||||
const box = (
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 cursor-pointer rounded-[4px] accent-[var(--color-accent)]",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
if (!label) return box;
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="inline-flex items-center gap-2 cursor-pointer text-sm text-text-main"
|
||||
>
|
||||
{box}
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function DataTable({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "48px 24px",
|
||||
color: "var(--text-secondary, #888)",
|
||||
color: "var(--color-text-muted)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
@@ -89,7 +89,7 @@ export default function DataTable({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "48px 24px",
|
||||
color: "var(--text-secondary, #888)",
|
||||
color: "var(--color-text-muted)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
@@ -118,11 +118,11 @@ export default function DataTable({
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 600,
|
||||
color: "var(--text-secondary, #888)",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
||||
color: "var(--color-text-muted)",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: "var(--bg-table-header, rgba(15,15,25,0.95))",
|
||||
background: "var(--table-header-bg)",
|
||||
zIndex: 1,
|
||||
whiteSpace: "nowrap",
|
||||
fontSize: "11px",
|
||||
@@ -144,21 +144,21 @@ export default function DataTable({
|
||||
cursor: onRowClick ? "pointer" : "default",
|
||||
background:
|
||||
row.id === selectedId
|
||||
? "rgba(99,102,241,0.1)"
|
||||
? "var(--table-row-selected)"
|
||||
: idx % 2 === 0
|
||||
? "transparent"
|
||||
: "rgba(255,255,255,0.02)",
|
||||
: "var(--table-row-zebra)",
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (row.id !== selectedId) {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.04)";
|
||||
e.currentTarget.style.background = "var(--table-row-hover)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (row.id !== selectedId) {
|
||||
e.currentTarget.style.background =
|
||||
idx % 2 === 0 ? "transparent" : "rgba(255,255,255,0.02)";
|
||||
idx % 2 === 0 ? "transparent" : "var(--table-row-zebra)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -167,7 +167,7 @@ export default function DataTable({
|
||||
key={col.key}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
||||
borderBottom: "1px solid var(--table-cell-border)",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: col.maxWidth || "200px",
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function Input({
|
||||
}}
|
||||
className={cn(
|
||||
"w-full py-2 px-3 text-sm text-text-main",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-md",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-control",
|
||||
"placeholder-text-muted/60",
|
||||
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
|
||||
"transition-all shadow-inner disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function Modal({
|
||||
className={cn(
|
||||
"relative w-full bg-surface",
|
||||
"border border-black/10 dark:border-white/10",
|
||||
"rounded-xl shadow-2xl",
|
||||
"rounded-card shadow-2xl",
|
||||
"animate-in fade-in zoom-in-95 duration-200",
|
||||
sizes[size],
|
||||
className
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function Select({
|
||||
aria-describedby={describedBy}
|
||||
className={cn(
|
||||
"w-full py-2 px-3 pe-10 text-sm text-text-main",
|
||||
"bg-surface border border-black/10 dark:border-white/10 rounded-md appearance-none",
|
||||
"bg-surface border border-black/10 dark:border-white/10 rounded-control appearance-none",
|
||||
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
|
||||
"transition-all disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"text-[16px] sm:text-sm",
|
||||
|
||||
29
src/shared/components/Textarea.tsx
Normal file
29
src/shared/components/Textarea.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Textarea — token-driven multiline input mirroring the Input primitive (same border,
|
||||
* focus ring and control radius). Replaces ad-hoc raw `<textarea>` styling at call sites.
|
||||
*/
|
||||
export default function Textarea({ className, error = false, ...props }: TextareaProps) {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full py-2 px-3 text-sm text-text-main",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-control",
|
||||
"placeholder-text-muted/60",
|
||||
"focus:ring-1 focus:ring-primary/30 focus:border-primary/50 focus:outline-none",
|
||||
"transition-all disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"text-[16px] sm:text-sm",
|
||||
error ? "border-red-500 focus:border-red-500 focus:ring-red-500/20" : "",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import { useTranslations } from "next-intl";
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { STATUS_HEX } from "@/shared/constants/statusColors";
|
||||
|
||||
const STATUS_MAP = {
|
||||
healthy: { icon: "check_circle", color: "#22c55e", tooltip: "All tokens healthy" },
|
||||
warning: { icon: "warning", color: "#f59e0b", tooltip: "Some tokens need attention" },
|
||||
error: { icon: "error", color: "#ef4444", tooltip: "Token refresh failures detected" },
|
||||
unknown: { icon: "help", color: "#6b7280", tooltip: "Health status unknown" },
|
||||
healthy: { icon: "check_circle", color: STATUS_HEX.success, tooltip: "All tokens healthy" },
|
||||
warning: { icon: "warning", color: STATUS_HEX.warning, tooltip: "Some tokens need attention" },
|
||||
error: { icon: "error", color: STATUS_HEX.error, tooltip: "Token refresh failures detected" },
|
||||
unknown: { icon: "help", color: STATUS_HEX.muted, tooltip: "Health status unknown" },
|
||||
};
|
||||
|
||||
export default function TokenHealthBadge() {
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
* the Compression Studio (Tela A) so all three flow graphs speak the same
|
||||
* color language: green = active, red = error, amber = last-used, muted = idle.
|
||||
*/
|
||||
import { STATUS_HEX } from "@/shared/constants/statusColors";
|
||||
|
||||
export const FLOW_EDGE_COLORS = {
|
||||
active: "#22c55e",
|
||||
error: "#ef4444",
|
||||
last: "#f59e0b",
|
||||
active: STATUS_HEX.success,
|
||||
error: STATUS_HEX.error,
|
||||
last: STATUS_HEX.warning,
|
||||
idle: "var(--color-border)",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Input } from "./Input";
|
||||
export { default as Select } from "./Select";
|
||||
export { default as Checkbox } from "./Checkbox";
|
||||
export { default as Textarea } from "./Textarea";
|
||||
export { default as Card } from "./Card";
|
||||
export { default as Collapsible } from "./Collapsible";
|
||||
export { default as Modal, ConfirmModal } from "./Modal";
|
||||
|
||||
@@ -59,7 +59,9 @@ export default function DashboardLayout({ children }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh min-h-0 w-full overflow-hidden bg-bg">
|
||||
// No bg-bg here: the body grid wallpaper (globals.css body::before) shows through
|
||||
// this transparent wrapper into the content area. body's --color-bg is the base fill.
|
||||
<div className="flex h-dvh min-h-0 w-full overflow-hidden">
|
||||
<Suspense fallback={null}>
|
||||
<NavigationProgress />
|
||||
</Suspense>
|
||||
|
||||
19
src/shared/constants/statusColors.ts
Normal file
19
src/shared/constants/statusColors.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Canonical status colors — single source of truth for status HEX, mirroring the
|
||||
* semantic tokens in `src/app/globals.css` (--color-success / --color-warning /
|
||||
* --color-error). Previously these same hex values were copy-pasted across several
|
||||
* components (flow edge styles, token-health badge, cascade nodes…); this module
|
||||
* collapses them to one place.
|
||||
*
|
||||
* Use these HEX values ONLY where a CSS class can't reach — canvas, ReactFlow SVG
|
||||
* strokes, or inline styles on third-party nodes. For normal DOM, prefer the Tailwind
|
||||
* utilities (`text-success`, `bg-error/10`, …) which already read the same tokens.
|
||||
*/
|
||||
export const STATUS_HEX = {
|
||||
success: "#22c55e",
|
||||
warning: "#f59e0b",
|
||||
error: "#ef4444",
|
||||
muted: "#6b7280",
|
||||
} as const;
|
||||
|
||||
export type StatusHexKey = keyof typeof STATUS_HEX;
|
||||
@@ -1,6 +1,12 @@
|
||||
// Utility function to merge class names
|
||||
// Handles conditional classes and removes duplicates
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...classes) {
|
||||
return classes.filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
|
||||
/**
|
||||
* Merge class names. Conditional classes are flattened by clsx; conflicting Tailwind
|
||||
* utilities are deduped by tailwind-merge, so a caller's `className` override actually
|
||||
* REPLACES a primitive's class (e.g. `rounded-full` wins over the primitive's radius)
|
||||
* instead of both stacking and relying on CSS source order.
|
||||
*/
|
||||
export function cn(...classes: ClassValue[]) {
|
||||
return twMerge(clsx(classes));
|
||||
}
|
||||
|
||||
163
tests/unit/design-grid-background.test.ts
Normal file
163
tests/unit/design-grid-background.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
// Static guards for the shared visual identity (Phase 1: graph-paper grid wallpaper).
|
||||
// These lock in the cross-product design contract so an accidental edit can't silently
|
||||
// remove the grid or re-introduce the opaque wrapper that hides it. See design.md.
|
||||
|
||||
const globalsCss = fs.readFileSync(new URL("../../src/app/globals.css", import.meta.url), "utf8");
|
||||
const dashboardLayout = fs.readFileSync(
|
||||
new URL("../../src/shared/components/layouts/DashboardLayout.tsx", import.meta.url),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
test("globals.css defines the grid wallpaper tokens for both themes", () => {
|
||||
// light
|
||||
assert.match(globalsCss, /--grid-line:\s*rgba\(0,\s*0,\s*0,\s*0\.045\)/);
|
||||
// dark
|
||||
assert.match(globalsCss, /--grid-line:\s*rgba\(255,\s*255,\s*255,\s*0\.035\)/);
|
||||
// size + alternating-section overlay
|
||||
assert.match(globalsCss, /--grid-size:\s*46px/);
|
||||
assert.match(globalsCss, /--section-alt:\s*rgba\(0,\s*0,\s*0,\s*0\.022\)/);
|
||||
assert.match(globalsCss, /--section-alt:\s*rgba\(255,\s*255,\s*255,\s*0\.018\)/);
|
||||
});
|
||||
|
||||
test("globals.css renders the grid via a body::before fixed layer", () => {
|
||||
// The pseudo-element must exist and be the grid renderer.
|
||||
const before = globalsCss.slice(globalsCss.indexOf("body::before"));
|
||||
assert.ok(before.length > 0, "body::before rule is present");
|
||||
assert.match(before, /position:\s*fixed/);
|
||||
assert.match(before, /z-index:\s*-1/);
|
||||
assert.match(before, /pointer-events:\s*none/);
|
||||
assert.match(before, /linear-gradient\(to right,\s*var\(--grid-line\) 1px, transparent 1px\)/);
|
||||
assert.match(before, /linear-gradient\(to bottom,\s*var\(--grid-line\) 1px, transparent 1px\)/);
|
||||
assert.match(before, /background-size:\s*var\(--grid-size\) var\(--grid-size\)/);
|
||||
});
|
||||
|
||||
test("globals.css adds the shared identity tokens", () => {
|
||||
assert.match(globalsCss, /--surface-2:\s*#f5f5fa/); // light
|
||||
assert.match(globalsCss, /--surface-2:\s*#1c2230/); // dark
|
||||
assert.match(globalsCss, /--radius:\s*14px/);
|
||||
assert.match(
|
||||
globalsCss,
|
||||
/--grad-brand:\s*linear-gradient\(135deg,\s*var\(--color-primary\),\s*var\(--color-accent-light\)\)/
|
||||
);
|
||||
// exposed to Tailwind as bg-surface-2 for later phases
|
||||
assert.match(globalsCss, /--color-surface-2:\s*var\(--surface-2\)/);
|
||||
});
|
||||
|
||||
test("DashboardLayout wrapper stays transparent so the grid shows through", () => {
|
||||
// Regression guard: the outer shell must NOT paint an opaque bg over the body grid.
|
||||
assert.ok(
|
||||
!dashboardLayout.includes("overflow-hidden bg-bg"),
|
||||
"DashboardLayout outer wrapper must not use bg-bg (it would hide the grid wallpaper)"
|
||||
);
|
||||
assert.ok(
|
||||
dashboardLayout.includes('className="flex h-dvh min-h-0 w-full overflow-hidden"'),
|
||||
"DashboardLayout outer wrapper is present and transparent"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Phase 2: primitives adopt the shared radius scale, brand gradient & border token ──
|
||||
|
||||
const read = (p: string) => fs.readFileSync(new URL(p, import.meta.url), "utf8");
|
||||
|
||||
test("globals.css exposes the semantic radius utilities", () => {
|
||||
assert.match(globalsCss, /--radius-control:\s*9px/); // :root value
|
||||
assert.match(globalsCss, /--radius-card:\s*var\(--radius\)/); // @theme → rounded-card (14px)
|
||||
assert.match(globalsCss, /--radius-control:\s*var\(--radius-control\)/); // @theme → rounded-control
|
||||
});
|
||||
|
||||
test("Button uses the brand gradient + accent variant + control radius", () => {
|
||||
const button = read("../../src/shared/components/Button.tsx");
|
||||
assert.match(button, /primary:\s*"bg-\[image:var\(--grad-brand\)\]/);
|
||||
assert.match(button, /accent:\s*"bg-accent/);
|
||||
assert.ok(
|
||||
!button.includes("from-primary to-primary-hover"),
|
||||
"the flat red→red gradient is replaced by --grad-brand"
|
||||
);
|
||||
assert.ok(button.includes("rounded-control"), "button sizes use the control radius");
|
||||
});
|
||||
|
||||
test("Card / Modal / Input / Select adopt the radius scale and border token", () => {
|
||||
const card = read("../../src/shared/components/Card.tsx");
|
||||
const modal = read("../../src/shared/components/Modal.tsx");
|
||||
const input = read("../../src/shared/components/Input.tsx");
|
||||
const select = read("../../src/shared/components/Select.tsx");
|
||||
assert.ok(card.includes("border border-border"), "card uses the --color-border token");
|
||||
assert.ok(card.includes("rounded-card"), "card uses rounded-card (14px)");
|
||||
assert.ok(!card.includes("border-black/5"), "the under-weight /5 border is gone");
|
||||
assert.ok(modal.includes("rounded-card"), "modal uses rounded-card");
|
||||
assert.ok(input.includes("rounded-control"), "input uses rounded-control (9px)");
|
||||
assert.ok(select.includes("rounded-control"), "select uses rounded-control (9px)");
|
||||
});
|
||||
|
||||
// ── Phase 3 (partial): status hex centralized + mono font token ──
|
||||
|
||||
test("status colors come from one canonical module", () => {
|
||||
const mod = read("../../src/shared/constants/statusColors.ts");
|
||||
assert.match(mod, /success:\s*"#22c55e"/);
|
||||
assert.match(mod, /warning:\s*"#f59e0b"/);
|
||||
assert.match(mod, /error:\s*"#ef4444"/);
|
||||
|
||||
const edges = read("../../src/shared/components/flow/edgeStyles.ts");
|
||||
const badge = read("../../src/shared/components/TokenHealthBadge.tsx");
|
||||
assert.ok(
|
||||
edges.includes('from "@/shared/constants/statusColors"'),
|
||||
"edgeStyles imports the module"
|
||||
);
|
||||
assert.ok(edges.includes("STATUS_HEX.success"), "edgeStyles uses STATUS_HEX, not a literal");
|
||||
assert.ok(!edges.includes('"#22c55e"'), "edgeStyles no longer hardcodes the success hex");
|
||||
assert.ok(badge.includes("STATUS_HEX.success"), "TokenHealthBadge uses STATUS_HEX");
|
||||
assert.ok(!badge.includes('"#22c55e"'), "TokenHealthBadge no longer hardcodes the success hex");
|
||||
});
|
||||
|
||||
test("globals.css defines a monospace token (site parity)", () => {
|
||||
assert.match(globalsCss, /--font-mono:\s*ui-monospace/);
|
||||
});
|
||||
|
||||
test("DataTable is theme-aware via --table-* tokens (dark = the exact old values)", () => {
|
||||
// The dark token values must equal the rgba the component used to hardcode, so dark
|
||||
// stays byte-identical while light gets fixed.
|
||||
assert.match(globalsCss, /--table-header-bg:\s*rgba\(15,\s*15,\s*25,\s*0\.95\)/); // dark
|
||||
assert.match(globalsCss, /--table-row-zebra:\s*rgba\(255,\s*255,\s*255,\s*0\.02\)/); // dark
|
||||
assert.match(globalsCss, /--table-row-hover:\s*rgba\(255,\s*255,\s*255,\s*0\.04\)/); // dark
|
||||
assert.match(globalsCss, /--table-cell-border:\s*rgba\(255,\s*255,\s*255,\s*0\.04\)/); // dark
|
||||
assert.match(globalsCss, /--table-header-bg:\s*rgba\(249,\s*249,\s*251,\s*0\.95\)/); // light fix
|
||||
|
||||
const dt = read("../../src/shared/components/DataTable.tsx");
|
||||
assert.ok(dt.includes("var(--table-header-bg)"), "header uses the token");
|
||||
assert.ok(dt.includes("var(--table-row-zebra)"), "zebra uses the token");
|
||||
assert.ok(dt.includes("var(--color-border)"), "header border uses the brand token");
|
||||
assert.ok(
|
||||
!/rgba\(|#[0-9a-fA-F]{3,6}/.test(dt),
|
||||
"DataTable no longer hardcodes any color literal"
|
||||
);
|
||||
assert.ok(
|
||||
!dt.includes("--text-secondary") && !dt.includes("--bg-table-header"),
|
||||
"the dead var fallbacks are gone"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Phase 4 (safe additives): cn() merge + Checkbox / Textarea primitives ──
|
||||
|
||||
test("cn() dedupes conflicting Tailwind classes via tailwind-merge", () => {
|
||||
const cnSrc = read("../../src/shared/utils/cn.ts");
|
||||
assert.match(cnSrc, /from "tailwind-merge"/);
|
||||
assert.match(cnSrc, /from "clsx"/);
|
||||
assert.match(cnSrc, /twMerge\(clsx\(/);
|
||||
});
|
||||
|
||||
test("Checkbox + Textarea primitives exist and are exported", () => {
|
||||
const barrel = read("../../src/shared/components/index.tsx");
|
||||
assert.ok(barrel.includes('export { default as Checkbox } from "./Checkbox"'));
|
||||
assert.ok(barrel.includes('export { default as Textarea } from "./Textarea"'));
|
||||
const checkbox = read("../../src/shared/components/Checkbox.tsx");
|
||||
const textarea = read("../../src/shared/components/Textarea.tsx");
|
||||
assert.ok(
|
||||
checkbox.includes("accent-[var(--color-accent)]"),
|
||||
"checkbox uses the brand accent token"
|
||||
);
|
||||
assert.ok(textarea.includes("rounded-control"), "textarea uses the control radius");
|
||||
});
|
||||
Reference in New Issue
Block a user