diff --git a/DESIGN.md b/DESIGN.md index b646a3f6..c78ef5c4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -19,20 +19,17 @@ the tests and contributor docs. ## Source of Truth VisionSet's visual language is a **shadcn preset**, not a hand-authored system: preset -code `b3bXyyPdWj`, decoded as `style: nova` (on the Radix base — `radix-nova` in -`components.json`), `baseColor: neutral`, `chart: orange`, `icons: tabler`, -`font: inter`, `heading: geist`, `radius: medium`, `menu: inverted/subtle`. The preset -was generated with shadcn CLI **4.18.0** and transcribed verbatim into this repository; -that CLI version is the reference for every value in this document. Nothing here is -invented — every token, every derived radius, every chart colour traces back to the -CLI's own output. - -**One property deliberately diverges: the icon set.** The preset decodes -`icons: tabler`, and that line above says so because it is what the code decodes to; -the product draws `lucide-react`, and `components.json` says `iconLibrary: lucide`. -The schema accepts the value, so this is a decision rather than drift — but it is the -one place where reading the preset and reading the configuration give different -answers, and it is written here so nobody has to discover that by diffing them. +code `b2iH`, decoded as `style: nova` (on the Radix base — `radix-nova` in +`components.json`), `baseColor: neutral`, `chart: neutral`, `icons: lucide`, +`font: geist`, `heading: inherit`, `radius: medium`, `menu: inverted/subtle`, with the +CLI's `--pointer` option on. The preset was generated with shadcn CLI **4.19.0** and +transcribed verbatim into this repository; that CLI version is the reference for every +value in this document. Nothing here is invented — every token, every derived radius, +every chart colour traces back to the CLI's own output. + +`heading: inherit` is the one decoded value with no token of its own: it means the +heading face *is* the body face, which the stylesheet spells as +`--font-heading: var(--font-sans)` rather than as a second family. `@visionset/ui-core` owns the implementation, in exactly three files: @@ -140,6 +137,11 @@ picks one of these three; it does not compose a fill from scratch. - **Hover** on a filled control is the same fill at reduced opacity (`hover:bg-primary/80`), not a colour change of meaning; a menu or list item highlights with `accent` instead. - **Press** reads as the control moving, not recolouring. +- **The cursor turns to a hand over anything pressable**, which is the preset's + `--pointer` option: `button` and `[role="button"]`, in the base layer. It stops at + `:not(:disabled)` on purpose — a hand over a control that will not respond is the + cursor making a promise the control does not keep, and the disabled rule below is what + the reader should be getting instead. - **Disabled** is uniform reduced opacity plus `pointer-events-none` — the control dims as itself rather than swapping to a separate greyed-out skin. A disabled control still explains itself; see the product principles' never-disable-without-explanation rule. @@ -149,12 +151,14 @@ picks one of these three; it does not compose a fill from scratch. ## Typography -- **Body copy: Inter**, via `--font-sans` (`'Inter Variable', sans-serif`). -- **Headings: Geist**, via `--font-heading` (`'Geist Variable', sans-serif`), applied at - the semantic-HTML level — `h1`–`h4` carry it in the base layer, so a screen never has - to remember `font-heading` on every heading it writes. -- Both are bundled offline through `@fontsource-variable/{inter,geist}` — no runtime - fetch to a font host, ever. +- **One family: Geist**, via `--font-sans` (`'Geist Variable', sans-serif`). The preset + sets `--font-heading` to `var(--font-sans)`, so a heading is the same face as body + copy at a different size and weight rather than a second typeface. +- **`font-heading` survives as a hook, not as a difference.** `h1`–`h4` still carry it + in the base layer, so no screen writes it by hand and a later preset that splits the + two families again lands in one declaration rather than at every heading. +- Bundled offline through `@fontsource-variable/geist` — no runtime fetch to a font + host, ever. - **One justified technical role: `font-mono`.** Tailwind's default monospace stack (no Geist Mono package is bundled) marks *machine-shaped* content — identifiers, hashes, model references, measurements. It is never decoration, and prose never wears it; this @@ -313,16 +317,22 @@ something a component opts into. ## Charts -The preset's chart palette — five orange steps, **identical in both themes** (a chart -does not restyle when the page switches theme): +The preset's chart palette — five neutral steps from light to dark, **identical in both +themes** (a chart does not restyle when the page switches theme): | Token | Value | | --- | --- | -| `chart-1` | `oklch(0.837 0.128 66.29)` | -| `chart-2` | `oklch(0.705 0.213 47.604)` | -| `chart-3` | `oklch(0.646 0.222 41.116)` | -| `chart-4` | `oklch(0.553 0.195 38.402)` | -| `chart-5` | `oklch(0.47 0.157 37.304)` | +| `chart-1` | `oklch(0.87 0 0)` | +| `chart-2` | `oklch(0.556 0 0)` | +| `chart-3` | `oklch(0.439 0 0)` | +| `chart-4` | `oklch(0.371 0 0)` | +| `chart-5` | `oklch(0.269 0 0)` | + +They separate by **lightness rather than by hue**, which is the trade this preset makes: +a series stays legible to a reader who cannot separate two hues, and it survives being +printed or screenshotted in greyscale — at the cost of holding fewer series apart before +the steps run together. A chart needing more than five wants a second channel, a shape +or a label, not a sixth colour invented here. These are **series colours, never status.** A chart never leans on `chart-2` to mean "warning" — status uses the status tokens below, a chart uses these to tell one series diff --git a/docs/content/architecture/frontend/ui-core.md b/docs/content/architecture/frontend/ui-core.md index 2f2019d7..b4b6f65d 100644 --- a/docs/content/architecture/frontend/ui-core.md +++ b/docs/content/architecture/frontend/ui-core.md @@ -73,9 +73,10 @@ whenever its asserted type is and `tsc` cannot see the mismatch. ## The design system is a shadcn preset -`styles.css` is the shadcn preset `b3bXyyPdWj` (style `nova` on the Radix base, -base colour `neutral`, chart palette `orange`, icons `tabler`, Inter body / -Geist heading fonts, radius `medium`, menu `inverted`/`subtle`) - the CLI's own +`styles.css` is the shadcn preset `b2iH` (style `nova` on the Radix base, base +colour `neutral`, chart palette `neutral`, icons `lucide`, Geist throughout with +the heading face inheriting the body's, radius `medium`, menu +`inverted`/`subtle`, pointer cursor on pressable controls) - the CLI's own generated output, transcribed verbatim, plus five VisionSet extension roles (`stage`, `brand`, `success`, `warning`, `origin-*`) added through shadcn's own extension convention. `components.json` (`style: "radix-nova"`, @@ -84,10 +85,7 @@ the preset properties shadcn's own tools read - the fields its config schema defines, and no others. The schema is strict, so the properties it has no field for - the radius, the fonts, the chart palette, every colour - are values carried by `styles.css` instead; see [`DESIGN.md`](../../../../DESIGN.md)'s -Source of Truth for the three layers. The icon set is the one place the -configuration and the preset disagree on purpose: the preset decodes `tabler`, the -product draws `lucide-react`, and `iconLibrary` records the product rather than the -preset. `tokens.ts` is the TypeScript mirror for a caller that +Source of Truth for the three layers. `tokens.ts` is the TypeScript mirror for a caller that cannot read CSS. Both themes - light and dark - are declared in full from the preset, so `bg-primary` in a component here and `bg-primary` in a screen mean the same colour by construction. There is no `tailwind.config.js` in this diff --git a/frontend/app/e2e/styleguide.spec.ts b/frontend/app/e2e/styleguide.spec.ts index 3e8f03a9..b51de83e 100644 --- a/frontend/app/e2e/styleguide.spec.ts +++ b/frontend/app/e2e/styleguide.spec.ts @@ -175,10 +175,15 @@ test("the progress fill wears the functional primary colour, not the brand", asy * `@layer base`, so every screen inherited one rhythm without asking. The * shadcn preset does not: typography is Tailwind's ordinary scale, applied per * element (`DESIGN.md`, *Typography* and *Density and Spacing*), and the base - * layer's job shrank to three things — `html` gets `font-sans` (Inter), `h1`–`h4` - * get `font-heading` (Geist) and `body` gets `bg-background`/`text-foreground`. + * layer's job shrank to three things — `html` gets `font-sans`, `h1`–`h4` get + * `font-heading` and `body` gets `bg-background`/`text-foreground`. * Those three are what this now asserts; a body-wide font-size is no longer * part of the claim because it is no longer part of the contract. + * + * Both hooks resolve to Geist under `b2iH`, which sets `--font-heading` to + * `--font-sans`. Each is still asserted by name rather than against the other: a + * heading that had fallen back to the browser's default would satisfy "the two + * agree" while proving the hook was never wired. */ test("the base layer wires fonts and background, not a per-screen font-size", async ({ page }) => { const body = page.locator("body"); @@ -189,7 +194,7 @@ test("the base layer wires fonts and background, not a per-screen font-size", as const bodyBg = await rgbaOf(page, body, "background-color"); // `html { @apply font-sans }` — every screen, not a class on each page. - expect(bodyFont).toContain("Inter"); + expect(bodyFont).toContain("Geist"); // `h1`–`h4` carry `font-heading` in the base layer, so a heading never repeats it. expect(headingFont).toContain("Geist"); // `body { @apply bg-background }` — the preset's own white, applied once. diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-dark-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-dark-visual-linux.png index 6ae82a8b..b4c04a64 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-dark-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-dark-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-light-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-light-visual-linux.png index 333daeec..48d31866 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-light-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/annotator-desktop-light-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-dark-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-dark-visual-linux.png index be5eac50..9eb7345c 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-dark-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-dark-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-light-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-light-visual-linux.png index 7da1e4d7..642d6853 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-light-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/menu-open-light-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-dark-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-dark-visual-linux.png index e80b8e32..f462bff4 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-dark-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-dark-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-light-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-light-visual-linux.png index 10683c6f..d68b2696 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-light-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/models-desktop-light-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-dark-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-dark-visual-linux.png index aa9b1292..9ddfb435 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-dark-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-dark-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-light-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-light-visual-linux.png index f3ccd00c..df4f0cff 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-light-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/projects-desktop-light-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/schema-desktop-dark-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/schema-desktop-dark-visual-linux.png index d7d4ca99..22a64f6a 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/schema-desktop-dark-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/schema-desktop-dark-visual-linux.png differ diff --git a/frontend/app/e2e/visual.spec.ts-snapshots/schema-narrow-light-visual-linux.png b/frontend/app/e2e/visual.spec.ts-snapshots/schema-narrow-light-visual-linux.png index 18e8ff0f..02ac5672 100644 Binary files a/frontend/app/e2e/visual.spec.ts-snapshots/schema-narrow-light-visual-linux.png and b/frontend/app/e2e/visual.spec.ts-snapshots/schema-narrow-light-visual-linux.png differ diff --git a/frontend/ui-core/package.json b/frontend/ui-core/package.json index 1a2320fc..b2717542 100644 --- a/frontend/ui-core/package.json +++ b/frontend/ui-core/package.json @@ -44,7 +44,6 @@ }, "dependencies": { "@fontsource-variable/geist": "^5.3.0", - "@fontsource-variable/inter": "^5.3.0", "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-label": "^2.1.15", diff --git a/frontend/ui-core/src/styles.css b/frontend/ui-core/src/styles.css index e4b5ddf5..3fea9bf5 100644 --- a/frontend/ui-core/src/styles.css +++ b/frontend/ui-core/src/styles.css @@ -1,11 +1,11 @@ /* * @visionset/ui-core — the design system's stylesheet. * - * This is the shadcn preset `b3bXyyPdWj` (style `nova` on the Radix base, - * base colour `neutral`, chart palette `orange`, icons `tabler`, body font - * Inter, heading font Geist, radius medium, menu inverted/subtle) — the CLI's - * exact generated output, transcribed verbatim from a Docker scratch project - * built with `shadcn` 4.18.0. The preset is the source of truth for every + * This is the shadcn preset `b2iH` (style `nova` on the Radix base, base colour + * `neutral`, chart palette `neutral`, icons `lucide`, Geist throughout with the + * heading face inheriting the body's, radius medium, menu inverted/subtle, and + * the pointer cursor on pressable controls) — the CLI's exact generated output, + * transcribed verbatim from a Docker scratch project built with `shadcn` 4.19.0. The preset is the source of truth for every * name below except nine: `stage`, `brand`, `success`/`success-foreground`, * `warning`/`warning-foreground` and the three `origin-*` marks are * VisionSet's own, each justified in @@ -26,7 +26,6 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; -@import "@fontsource-variable/inter"; @import "@fontsource-variable/geist"; /* @@ -57,11 +56,11 @@ --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); - --chart-1: oklch(0.837 0.128 66.29); - --chart-2: oklch(0.705 0.213 47.604); - --chart-3: oklch(0.646 0.222 41.116); - --chart-4: oklch(0.553 0.195 38.402); - --chart-5: oklch(0.47 0.157 37.304); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); --radius: 0.625rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); @@ -126,11 +125,11 @@ --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); - --chart-1: oklch(0.837 0.128 66.29); - --chart-2: oklch(0.705 0.213 47.604); - --chart-3: oklch(0.646 0.222 41.116); - --chart-4: oklch(0.553 0.195 38.402); - --chart-5: oklch(0.47 0.157 37.304); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); @@ -152,8 +151,8 @@ } @theme inline { - --font-sans: 'Inter Variable', sans-serif; - --font-heading: 'Geist Variable', sans-serif; + --font-sans: 'Geist Variable', sans-serif; + --font-heading: var(--font-sans); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -238,11 +237,18 @@ body { @apply bg-background text-foreground; } + /* The preset's pointer option. Scoped to what is actually pressable and away + from what is not: a disabled control keeps the arrow, because a hand over + something that will not respond is the cursor telling a lie. */ + button:not(:disabled), [role="button"]:not(:disabled) { + cursor: pointer; + } html { @apply font-sans; } - /* The preset's heading font, applied at the semantic-HTML level so a - screen never has to remember `font-heading` on every