diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8a309d..77100b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,17 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: '22' + node-version: '24' cache: pnpm - name: Install dependencies @@ -48,7 +50,7 @@ jobs: - name: Upload visual failure artifacts if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: playwright-results path: | @@ -56,5 +58,29 @@ jobs: apps/blocks/test-results if-no-files-found: ignore + pages: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '24' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build and verify Pages artifact + run: pnpm build:pages + # npm releases are intentionally manual. This workflow only validates package # tags; it contains no registry token, trusted-publishing step, or publish job. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4acf069..c010253 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -6,13 +6,11 @@ on: workflow_dispatch: {} concurrency: - group: pages - cancel-in-progress: true + group: pages-${{ github.ref }} + cancel-in-progress: false permissions: contents: read - pages: write - id-token: write jobs: build: @@ -20,20 +18,19 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: '22' + node-version: '24' cache: pnpm - - name: Configure GitHub Pages - uses: actions/configure-pages@v5 - - name: Install dependencies run: pnpm install --frozen-lockfile @@ -43,34 +40,24 @@ jobs: - name: Build static Blocks site run: pnpm build:pages - - name: Assemble Pages artifact - run: | - rm -rf _site - mkdir -p _site/r - cp -R apps/blocks/out/. _site/ - cp apps/registry/public/r/*.json _site/r/ - test -f _site/index.html - test -f _site/blocks/index.html - test -f _site/r/registry.json - test -f _site/r/chat.json - test -f _site/r/schema-builder.json - grep -q '/blocks/_next/' _site/index.html - ! grep -R 'github.io/dashboard' _site - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: - path: _site + path: .artifacts/pages deploy: + if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 # npm publication is a maintainer-run local process and is intentionally absent. diff --git a/.node-version b/.node-version index 2bd5a0a..a45fd52 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22 +24 diff --git a/AGENTS.md b/AGENTS.md index aabede7..5a65032 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,5 +16,5 @@ This public monorepo owns the Constructive Blocks documentation, the ## Verification -Use Node 22 and pnpm 10.28.0, then run `pnpm check`, `pnpm build:pages`, and +Use Node 24 LTS and pnpm 10.28.0, then run `pnpm check`, `pnpm build:pages`, and `pnpm pack:local` before release-related changes. diff --git a/CLAUDE.md b/CLAUDE.md index 825d0f4..d7ddb36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,4 +27,4 @@ pnpm build:storybook pnpm pack:local ``` -Use Node 22 and pnpm 10.28.0. +Use Node 24 LTS and pnpm 10.28.0. diff --git a/README.md b/README.md index 36fd073..fe2aa6a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ component documentation, and published React packages. ## Workspaces -- `apps/blocks` — documentation, demos, flows, and canonical block source. +- `apps/blocks` — primitive documentation and canonical block source. - `apps/registry` — private builder for the `@constructive` shadcn registry. - `packages/ui` — the `@constructive-io/ui` npm package and UI registry source. - `packages/schema-builder` — the shared schema-builder npm package and registry source. @@ -14,6 +14,20 @@ The documentation site is published at . Registry JSON is served from `https://constructive-io.github.io/blocks/r/{name}.json`. +The npm and registry distributions are independent. After mapping the +`@constructive` namespace in `components.json`, either surface can be used on +its own: + +```bash +pnpm add @constructive-io/ui +pnpm dlx shadcn@4.13.1 add @constructive/button +``` + +The package exposes its Tailwind foundation at +`@constructive-io/ui/globals.css`. Registry installs copy the required UI +source and Constructive theme into the consumer and do not install the npm +package. Registry consumers require shadcn CLI 4.13.1 or newer. + ## Development ```bash @@ -23,6 +37,9 @@ pnpm build:pages pnpm pack:local ``` +Development and release verification use Node 24 LTS and pnpm 10.28.0. All +first-party executable tooling is TypeScript and runs through `tsx`. + `pnpm pack:local` builds the public packages and writes publishable tarballs to the ignored `.artifacts/npm` directory. Consume those tarballs from downstream projects before publishing so validation exercises the real package contents. diff --git a/apps/blocks/README.md b/apps/blocks/README.md index 72dfd69..2792d64 100644 --- a/apps/blocks/README.md +++ b/apps/blocks/README.md @@ -1,59 +1,26 @@ -# Blocks +# Blocks docs -Docs and live-demo showcase for the **Constructive blocks registry** — the `@constructive` shadcn registry of data-bound blocks and UI primitives. Every block renders as a real, interactive demo next to its install command, props, and source. +This Next.js app is the clean documentation surface for the Constructive UI foundation. It intentionally exposes only +the landing page, setup guidance, and the 29 base primitive pages while the complete block catalog remains available +through the public registry. -Live: **https://constructive-io.github.io/blocks/** (GitHub Pages, static export under basePath `/blocks`). +The same primitive implementation is shown through two distribution modes: -## Develop +- npm consumers import `@constructive-io/ui/` and the package stylesheet. +- shadcn consumers install `@constructive/` as editable source without preinstalling the npm package. + +## Development ```bash -pnpm --filter blocks dev # Next.js + Turbopack on http://localhost:3005 +pnpm --filter blocks dev +pnpm --filter blocks lint:types +pnpm --filter blocks test +pnpm --filter blocks build:pages ``` -`predev` regenerates artifacts first, so the dev server always reflects current content. - -## Content model - -Docs are **generated from source**, not hand-written per page. Authored inputs and the UI/schema package manifests feed two generators: - -| Authored input | Generator | Generated output (DO NOT EDIT) | -|----------------|-----------|--------------------------------| -| `src/content/blocks/*.md`, `catalog/blocks.json`, all three registry manifests, sidecar `scripts/*content*.mjs` | `scripts/generate-manifest.mjs` | `src/blocks-manifest.json`, `src/lib/docs/registry-data.ts` | -| `scripts/flows-content.mjs`, `registry.json` | `scripts/generate-flows.mjs` | `src/flows/flows.json` | - -Run `pnpm gen` after changing any authored input. Never edit the generated files directly — `pnpm gen:check` re-runs the generators into a temp dir and fails on any drift. It runs in `prebuild` and in CI, so drift blocks the build. - -## Scripts - -| Script | Does | -|--------|------| -| `gen` | Regenerate manifest, docs data, and flows | -| `gen:check` | Fail if generated files drift from their sources | -| `check:flows` | Validate `flows.json`, namespaced installs, and pruned SDK fixtures | -| `fixtures:refresh` | Refresh reachable SDK fixtures from an explicit generated source root | -| `fixtures:check` | Fail when generated SDK imports drift from committed fixtures | -| `check:selections` | Reject empty mutation selections in `src/blocks` | -| `lint:types` | `tsc --noEmit` | -| `test` | `vitest run` | -| `test:visual` | Compare the static Pages site with the Playwright screenshot baselines | -| `build:pages` | `BLOCKS_PAGES=1 next build` — static export for GitHub Pages | - -## Layout - -| Path | Role | -|------|------| -| `src/blocks` | Registry-shipped app block source (auth, org, user, chat, primitives, runtime, …) | -| `src/generated` | Pruned, committed generated SDK fixtures used only by docs and tests | -| `src/components/docs` | Docs site chrome — nav, preview frame, code surface, demos | -| `src/app` | Next.js App Router routes | -| `src/content/blocks` | Authored per-block markdown (generator input) | -| `scripts` | Generators + drift/contract guards | - -`src/blocks` is the source shadcn ships to consumers; everything else is the site that documents it. - -## Deploy +`src/lib/base-primitives.ts` is the single docs catalog. `pnpm gen:check` validates that every entry has an npm export, +a registry item, and a preview that imports the npm subpath. The SDK fixture and mutation-selection checks remain in +place for the canonical block source under `src/blocks`. -`.github/workflows/ci.yml` runs the repository validation suite. On `main`, -`.github/workflows/pages.yml` repeats the validated build, assembles `_site/` — -the Blocks site plus registry JSON under `/r/` — and deploys it to GitHub Pages. -Neither workflow publishes npm packages. +The static Pages build uses `/blocks` as its deployment base path. Publishing npm packages remains a separate manual +release step. diff --git a/apps/blocks/docs/redesign/DESIGN.md b/apps/blocks/docs/redesign/DESIGN.md deleted file mode 100644 index 01061d6..0000000 --- a/apps/blocks/docs/redesign/DESIGN.md +++ /dev/null @@ -1,513 +0,0 @@ -# DESIGN.md — Fluid-Functionalism Redesign (v2) - -**Status: ACTIVE — the single reference for the 2026-07 UI refactor.** -Supersedes the visual prescriptions in `RESEARCH.md` / `IMPLEMENTATION_PLAN.md` (v1 "calm dark-first" pass). Those documents remain useful for pipeline/architecture background only. Where they conflict with this file, **this file wins**. - -Reference implementation: `~/workspace/projects/fluid-functionalism` (local clone, called **FF** below). All values in this doc were extracted from FF source, not eyeballed. When in doubt, open the FF file cited and copy the real thing. - ---- - -## 0. Mission - -The current site is a coherent but conservative docs app wrapped in dev-tool marketing chrome. The redesign makes it feel like FF: **you land on the product itself**. No hero theater, no atmosphere, no scroll animations — a quiet neutral shell whose only job is to present live, working components beautifully, with motion spent exclusively on *interaction* (hover, press, open), never on *decoration*. - -Principles (in priority order): - -1. **Immediate value** — the landing page IS a grid of live, usable component demos. -2. **Neutral canvas, product carries the color** — chrome is achromatic; Constructive blue appears as accent (links, focus, dots, washes) and inside the showcased blocks themselves. -3. **One measure, one scale, one radius table** — no competing vocabularies. -4. **Motion = communication** — spring enters, quicker tween exits, weight-shift actives; zero entrance/scroll theater. -5. **Elevation by ladder** — bg-surface-N pairs with shadow-surface-N; borders are hairlines, never structure. - -Brand deltas from FF (locked, do not "fix" these toward FF): -- **Geist + Geist Mono stay** (FF uses Inter). Geist has a `wght` axis but **no `opsz`** — see §4.3. -- **Constructive blue stays** the accent: `oklch(0.688 0.175 245.6)` dark / `oklch(0.55 0.16 245.6)` light. It replaces FF's periwinkle `#6B97FF` everywhere that color appears (focus rings, hover washes, new-dots). -- Dark remains the default theme (`next-themes`, `defaultTheme="dark"`, System available). - ---- - -## 1. Token system — `src/app/globals.css` rewrite - -> **Values tuned for contrast 2026-07-03, light ladder retuned 2026-07-06 — see section 1.10.** The FF aesthetic (pure neutral, hairline, calm, single accent) is preserved; the tuning raised the contrast *floor*, not the character. The 07-06 pass strengthened the LIGHT ladder only (secondary text, borders, overlays, card shadow) — dark output is unchanged. - -Replace the current OKLCH knob system (`--cb-bg-l`, `--cb-hue: 264` cool charcoal) with FF's **pure-neutral hex ladders**. The cool tint and the L=0.145 near-black canvas are two of the biggest "not-FF" tells: FF's dark canvas is `#171717` — lighter, and dead neutral. - -Keep: the `@import '@constructive-io/ui/globals.css'` + `@source` scanning setup (lines 1–27 — the Tailwind-v4 `@source`-in-`@import` gotcha comment must survive), the `.prose` token bridge, the `@tailwindcss/typography` plugin, the reduced-motion net. - -Delete: `registry-theme.css` (see kill list §8; fold the few survivors into `globals.css`). - -### 1.1 Surfaces + canvas - -```css -:root { - /* LIGHT — canvas #FAFAFA, ladder compresses to flat white; shadow does the work */ - --surface-1: #FAFAFA; - --surface-2: #FCFCFC; - --surface-3: #FFFFFF; - --surface-4: #FFFFFF; - --surface-5: #FFFFFF; - --surface-6: #FFFFFF; - --surface-7: #FFFFFF; - --surface-8: #FFFFFF; - - --background: var(--surface-1); - --foreground: #171717; - --card: var(--surface-3); - --card-foreground: #171717; - --popover: var(--surface-5); - --popover-foreground: #171717; - --muted: #EFEFEF; /* decoupled from surface-2 — FF rationale: the light - ladder is so compressed that bg-muted must be - a real gray to read at all. Darkened #F4F4F5→#EFEFEF - 2026-07-03 for canvas separation (delta 6→11) */ - --muted-foreground: #4F4F4F; /* 7.85:1 canvas / 8.19:1 card / 7.12:1 muted — #737373 - (~4.5:1) → #5F5F5F (~5.5–6.4, still washed out) → #4F4F4F - (2026-07-06 retune, clears the ≥7 target everywhere); see §1.10 */ - --accent: #EBEBEB; /* solid tint for chips/menu-hover fills; sits below - --muted in the fill ladder (was #F0F0F0) */ - --accent-foreground: #171717; - --secondary: #E5E5E5; /* FF secondary-button fill */ - --secondary-foreground: #171717; - --selected: #D4D4D4; -} - -.dark { - /* DARK — additive white over #171717, exact FF ladder */ - --surface-1: #171717; - --surface-2: #1E1E1E; - --surface-3: #252525; - --surface-4: #2C2C2C; - --surface-5: #333333; - --surface-6: #3A3A3A; - --surface-7: #414141; - --surface-8: #484848; - - --background: var(--surface-1); - --foreground: #F5F5F5; - --card: var(--surface-3); - --card-foreground: #F5F5F5; - --popover: var(--surface-5); - --popover-foreground: #F5F5F5; - --muted: var(--surface-2); - --muted-foreground: #A3A3A3; /* KEPT: already 6.1–7.1:1 on dark surfaces (passes the - ≥5.5 secondary floor). Not darkened like light's twin — - it would compress the primary↔secondary hierarchy and - drift off the FF anchor. See §1.10 */ - --accent: #333333; - --accent-foreground: #F5F5F5; - --secondary: #525252; - --secondary-foreground: #F5F5F5; - --selected: #525252; -} -``` - -### 1.2 Borders (contrast-tuned formula, both themes) - -```css -/* LIGHT (:root) — raised 2026-07-06 so flat-white hairlines read */ ---border: color-mix(in oklab, var(--foreground) 22%, transparent); ---border-strong: color-mix(in oklab, var(--foreground) 34%, transparent); ---input: color-mix(in oklab, var(--foreground) 24%, transparent); -/* DARK (.dark) — kept: near-black canvas + additive-white ring shadows need less */ ---border: color-mix(in oklab, var(--foreground) 17%, transparent); ---border-strong: color-mix(in oklab, var(--foreground) 30%, transparent); ---input: color-mix(in oklab, var(--foreground) 18%, transparent); -``` -Hairlines only. The mix strengths were raised in two passes; the **2026-07-06 light-contrast retune split the themes** because a flat-white hairline needs more ink than a hairline on the near-black dark canvas (where additive-white ring shadows already separate cards). **Light** (`:root`): border 12→17→**22**, strong 22→30→**34**, input 16→18→**24** — resting card/input hairlines (`border-border/60`) now = **13.2%** effective mix (was 10.2%), internal dividers (`/40`) = **8.8%** (was 6.8%, the sub-8% "borders too weak" case). **Dark** (`.dark`) keeps **17/30/18** (re-declared so color-mix recomputes against the dark foreground). In both themes: full-strength `border-border` is the *hover/emphasis* baseline, `--border-strong` the explicit emphasis tier (kept clearly above resting), and `--input` keeps form fields a hair above the resting card hairline. This also auto-strengthens `.bento-card-border` (it references `--border`). - -### 1.3 Shadow ladder (offsets FF verbatim; light `--shadow-color` alpha contrast-tuned) - -```css -:root { - --shadow-color: rgb(0 0 0 / 0.11); /* 0.06→0.08 (07-03) → 0.11 (07-06 retune) — on the - flat-white light ladder the 1px ring in --shadow-1 IS - the card edge (canvas↔card WCAG 1.04), so this alpha - is the card-separation lever */ - --shadow-1: 0 0 0 1px var(--shadow-color); - --shadow-2: 0 0 0 1px var(--shadow-color), 0 1px 1px -0.5px var(--shadow-color); - --shadow-3: /* + 0 3px 3px -1.5px */; - --shadow-4: /* + 0 6px 6px -3px */; - --shadow-5: /* + 0 12px 12px -6px */; - --shadow-6: /* + 0 24px 24px -12px */; - --shadow-7: /* + 0 48px 48px -24px */; - --shadow-8: /* + 0 96px 96px -48px */; -} -.dark { - --dm-hi-base: rgba(255,255,255,0.01); --dm-hi-mid: rgba(255,255,255,0.02); - --dm-hi-high: rgba(255,255,255,0.04); --dm-hi-peak: rgba(255,255,255,0.06); - --dm-ring-base: rgba(255,255,255,0.02); --dm-ring-mid: rgba(255,255,255,0.04); - --dm-ring-high: rgba(255,255,255,0.06); --dm-drop: rgba(0,0,0,0.18); - --shadow-1: inset 0 0 0 1px var(--dm-ring-base); - --shadow-2: inset 0 1px 0 0 var(--dm-hi-base), inset 0 0 0 1px var(--dm-ring-base), 0 1px 1px -0.5px var(--dm-drop); - /* …levels 3–8: copy EXACTLY from FF app/globals.css lines 119–124 — - inset highlight + inset ring + 1px black ring + stacked drops */ -} -``` -Copy levels 3–8 for both themes **verbatim** from FF `app/globals.css` (light L174–181, dark L117–124). Additive layers, halving offsets (1/3/6/12/24/48/96). Keep the existing `@theme inline` mapping (`--shadow-surface-N: var(--shadow-N)`) and the `@source inline("bg-surface-{1..8}")` force-generation — the utility names `bg-surface-N` / `shadow-surface-N` don't change, so consumers keep working. Retire the legacy `--shadow-sm/md/lg` + `--inset-hi` tokens and all their usages. - -### 1.4 Interaction overlays (new — replaces ad-hoc `bg-foreground/[0.04]`) - -```css -:root { --overlay: 0 0 0; --hover: rgb(var(--overlay) / 0.08); --active: rgb(var(--overlay) / 0.14); } -.dark { --overlay: 255 255 255; --hover: rgb(var(--overlay) / 0.08); --active: rgb(var(--overlay) / 0.13); } -@theme inline { --color-hover: var(--hover); --color-active: var(--active); } -``` -`bg-hover` = pointer-over surface, `bg-active` = current/selected/pressed surface. These work at any elevation (they're overlays, not fixed grays). All chrome hover/selected fills use these two utilities — no more per-component alpha literals. Light alphas were raised again in the **2026-07-06 retune** (light 0.04/0.07→0.06/0.10→**0.08/0.14**; dark kept 0.08/0.13) so the fills read on white: light hover fill `#E6E6E6` = Δ20 over the `#FAFAFA` canvas, active fill `#D7D7D7` = Δ35 over canvas / Δ15 over hover (and stays lighter than the solid `--selected` `#D4D4D4`, so the fill ladder holds); dark active ≈ Δ30 vs hover ≈ Δ19. - -### 1.5 Accent + status - -```css -:root { - --primary: oklch(0.55 0.16 245.6); --primary-foreground: #FFFFFF; - --ring: var(--primary); - --destructive: #EF4444; --destructive-light: #FEF2F2; - --success: #22C55E; --warning: #F59E0B; --info: var(--primary); -} -.dark { - --primary: oklch(0.688 0.175 245.6); --primary-foreground: oklch(0.985 0 0); - --ring: var(--primary); - --destructive: #F87171; --destructive-light: #450A0A; - --success: #22C55E; --warning: #F59E0B; --info: var(--primary); -} -``` -Status colors appear ONLY as (a) dot-badge dots at full strength, or (b) 15% washes: `color-mix(in srgb, 15%, var(--background))` with **foreground** text (FF Badge recipe — never saturated fills, never colored body text). Keep `--success-foreground`/`--warning-foreground` tokens for the shipped-ui compatibility but the docs chrome should stop using colored text on tints. - -### 1.6 Radius - -**Keep `--radius: 0.375rem` (6px).** The ui package derives the whole scale from it (`packages/ui/src/styles/globals.css:162-167`): `rounded-md = 6px`, `rounded-lg = +2px = 8px`, `rounded-xl = +6px = 12px`. That means the FF "rounded" table falls out of the EXISTING knob — and product components inside previews keep their consumer-faithful 6px `rounded-md`. Do not change the knob; change which utilities chrome uses: - -| Use | Class | Computed px | -|---|---|---| -| Rows, buttons, inputs, pills, tab pills | `rounded-lg` | 8 | -| Cards, preview containers, code panels, right panel, bento cards | `rounded-xl` | 12 | -| Focus ring (concentric, +2px outside an 8px item) | `rounded-[10px]` | 10 | -| Dots, avatars | `rounded-full` | — | - -Kill: `rounded-[5px]`, `rounded-[1px]`, `rounded-sm`, `rounded-md` in chrome (audit found all of these). `rounded-md` inside shipped `src/blocks/**` / `packages/ui` is not our concern. - -### 1.7 Type scale - -Keep the `@theme` text tokens but retune to FF sizes; the workhorse stays 13px: - -| Token / literal | Size | Weight | Use | -|---|---|---|---| -| `text-h1` | 22px mobile / 28px sm+ | 600, `leading-none`, `-0.02em` | Page titles | -| `text-h2` → **16px** | 16px | 600, `leading-none` | Section headings ("Installation", "Live preview") | -| h3 literal | 15–16px | 600 | Sub-features, thesis blocks | -| `text-body` | 14px | 400, `leading-relaxed` | Guide/introduction prose (`text-foreground/90`) | -| `text-[13px]` | 13px | 400 | THE workhorse: nav, descriptions, labels, table cells, buttons | -| `text-[12px]` | 12px | 400 | Mono code, props table mono, captions, install notes | -| `text-[11px]` | 11px | 400–500 | Counts, badges, kbd | - -Rules: no `text-sm`/`text-xs`/`text-base` in chrome; no sizes below 11px in chrome (demo-internal 9px literals are demo content, leave them); heading elements set `leading-none` and rely on flex `gap` for rhythm (FF pattern) rather than margins. - -### 1.8 Motion CSS vars - -```css ---dur-fast: 80ms; /* hover, color, weight, focus (was 120ms) */ ---dur: 160ms; /* indicators, small travel */ ---dur-slow: 240ms; /* panels, dialogs */ ---ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* unify: this is the JS reveal curve; kill the second curve */ ---ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); -``` -CSS transitions in chrome default to `duration-[var(--dur-fast)]` — FF runs micro-transitions at `duration-80` and that snappiness is part of the feel. Delete `--ease-in-out` (unused after the kill list). - -### 1.9 Utilities to port from FF `app/globals.css` - -- `.bento-card-border` (+ hover, + `:focus-within`, + dark variants) — L396–444. -- `.bento-grid` (`grid-auto-flow: dense`, `grid-auto-rows: 300px` at md+) — L446–456. -- `.scroll-fade` / `.scroll-fade-x` with the `@supports (animation-timeline: scroll())` scroll-aware variant — L588–706. -- `.xl-fade-flex` / `.xl-fade-block` (side-panel display+opacity cross-fade with `@starting-style`) — L536–586. -- `.scrollbar-hide` + the `@media (pointer: fine)` native scrollbar theming (thin, `color-mix` thumb) — L344–378. -- `html { scrollbar-gutter: stable }` and NO explicit overflow on html/body (FF comment L334–339 explains: an explicit overflow breaks dialog scroll-locks + sticky). - -### 1.10 Contrast floors (added 2026-07-03) - -The token values in §1.1–1.4 were tuned to satisfy these WCAG contrast floors, measured against the surface the text/border actually sits on. This is the contrast contract — do not regress a token below its tier. Same tokens, same pairings, pure neutrals (chroma 0), single Constructive-blue accent: the floor was raised, the FF character was not. - -| Tier | Applies to | Floor | How it's met now | -|---|---|---|---| -| **Primary** | headings, body, values (`--foreground`) | **≥ 12:1** | `#F5F5F5`/`#171717` dark, `#171717`/`#FAFAFA` light — already clear this; unchanged. | -| **Secondary** | 13px workhorse muted text: descriptions, nav resting rows, table cells, intros, install notes (`--muted-foreground`) | **≥ 5.5:1** (target ~7) | Light `#4F4F4F` = 7.85 canvas / 8.19 card / 7.12 muted (`#737373` ~4.5 → `#5F5F5F` ~5.5–6.4, still read washed out → `#4F4F4F`, 2026-07-06 retune: clears the ≥7 target on every surface). Dark `#A3A3A3` = 7.1 / 6.1 / 6.6 — **kept** (already passes; darkening would compress the primary↔secondary hierarchy and drift off the FF anchor). | -| **Decorative** | section caption labels, counts, kbd hints, footer status (alpha'd `--muted-foreground`) | **≥ 3.5:1** | Class-usage tier: decorative text renders at `/75` off the bases above — light `#4F4F4F` at `/75` = **4.13:1** (was 3.5 at the razor floor; the darker 07-06 base now gives margin), dark `#A3A3A3` at `/75` = 4.54:1. Literal `/50–/60` caption usages sit below 3.5 and rely on the darker base — a chrome-file concern, not a token one. | -| **Borders** | resting card/input hairlines (`--border` at `/60`), internal `/40` dividers | **≥ 8% effective** foreground-mix after alpha | Light (`:root`) `--border` **22%** → `/60` = **13.2%**, `/40` = **8.8%** (both clear 8%; the old 17% gave `/40` = 6.8%, under floor); `--border-strong` **34%**, `--input` **24%** keep the emphasis/field tiers above resting. Dark (`.dark`) keeps 17/30/18 — its ring shadows + lighter surfaces separate cards, so `/60` = 10.2% suffices. | -| **Active fills** | `bg-active` (selected nav row, active tab, segmented control) | visibly distinct from hover *and* canvas | Light active **0.14** → `#D7D7D7` (Δ35 over canvas, Δ15 over hover **0.08** → `#E6E6E6`, Δ20), capped just under the solid `--selected` `#D4D4D4`; dark active 0.13 (Δ≈30) vs hover 0.08 (Δ≈19). | - ---- - -## 2. Motion system — `src/lib/motion/springs.ts` (new) - -Port FF `registry/default/lib/springs.ts` verbatim (it's ~45 lines): - -```ts -export const spring = { - fast: { type: 'spring', duration: 0.08, bounce: 0, exit: { duration: 0.06 } }, - moderate: { type: 'spring', duration: 0.16, bounce: 0.08, exit: { duration: 0.12 } }, - settle: { type: 'spring', duration: 0.16, bounce: 0, exit: { duration: 0.12 } }, - slow: { type: 'spring', duration: 0.24, bounce: 0.12, exit: { duration: 0.16 } }, -} as const; -export const exitFallbackMs = (tier) => Math.round(tier.exit.duration * 1000) + 100; -``` - -Rules (from FF guidelines, adopted wholesale for docs chrome): -- The bigger the moving thing, the slower the spring. Enter on a tier; **exit on `tier.exit`** (a plain tween, one tier quicker — dismissals read crisp, never bouncy). -- Never hand-write a duration in JS motion. Never `ease-in` on UI. -- `` wraps the app at root layout (moves out of the landing-only `MotionProvider`). Animate `transform`/`opacity` only, so reduced-motion coverage is automatic. -- **Kill scroll/entrance theater**: `Reveal`, `Stagger`, `StaggerItem`, `CountUp`, `useScrollSpy`, hero entrance sequences, `whileInView` everywhere. FF pages render instantly, static. Motion lives in hover/press/open/close only. -- Keep the CSS-token transitions for non-JS surfaces (tabs indicator via Base UI, drawer, nav rows). - ---- - -## 3. Interaction language - -### 3.1 Weight-shift actives (ghost-span, Geist-adapted) - -Active/selected text gets **heavier, not recolored-only**: `'wght' 400 → 550` via `font-variation-settings`, transitioned at `duration-[var(--dur-fast)]`. The transition class MUST list `font-variation-settings` explicitly (`transition-[color,font-variation-settings]`). - -`src/lib/motion/font-weight.ts` (new): -```ts -export const fontWeights = { - normal: "'wght' 400", - medium: "'wght' 500", - semibold: "'wght' 550", - bold: "'wght' 640", -} as const; -``` -(Geist has no `opsz` axis — FF's optical-size compensation doesn't apply; the ghost span alone absorbs the width delta.) - -Ghost-span pattern (required wherever weight animates — nav rows, tab labels): -```tsx - - - {label} - -``` -Skip the ghost only when weight never changes for the node's lifetime. Don't invent new weight pairs: resting `normal`, active `semibold`; that's it. - -### 3.2 Focus - -`focus-visible:ring-1 focus-visible:ring-ring` (1px Constructive-blue ring; FF uses ring-1, our current ring-2 is louder than reference). Rings that would clip in `overflow` containers use FF's negative-margin trick: `-mx-1 px-1 -my-1 py-1` on the scroll container. Keyboard-focus containers (bento cards, preview frames) strengthen their border via `:focus-within` instead of drawing a second ring. - -### 3.3 Buttons (docs chrome only — shipped ui Button untouched) - -New `src/components/docs/site-button.tsx` implementing FF's two-layer recipe: -- Element carries text/border + `focus-visible:ring-1 ring-ring`; an inset `` carries the FILL. Press squishes the fill, not the label. -- Variants: `primary` = fill `bg-foreground`, text `text-background` (monochrome inverted — the FF look; Constructive blue is NOT a button fill in chrome). `secondary` = fill `bg-secondary`. `tertiary` = `border border-border`, transparent fill, `hover:bg-hover`. `ghost` = `text-muted-foreground hover:text-foreground`, fill `hover:bg-hover active:bg-active`. -- Sizes: sm `h-7 px-3 text-[12px]`, md `h-8 px-4 text-[13px]`, icon `h-9 w-9 [&_svg]:size-4`. Icons `strokeWidth={1.5}` → `group-hover:stroke-[2]`. -- Replaces: `cb-btn-primary`/`cb-btn-secondary` CSS classes and all ad-hoc chrome `