diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8a309d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [main] + tags: + - '@constructive-io/ui@*' + - '@constructive-io/schema-builder@*' + pull_request: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate packages, Blocks, and registry + run: pnpm check + + - name: Install Playwright Chromium + run: pnpm --filter blocks exec playwright install --with-deps chromium + + - name: Run visual regression suite + run: pnpm --filter blocks test:visual + + - name: Upload visual failure artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-results + path: | + apps/blocks/playwright-report + apps/blocks/test-results + if-no-files-found: ignore + +# 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 new file mode 100644 index 0000000..4acf069 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,76 @@ +name: Pages + +on: + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: pages + cancel-in-progress: true + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate repository + run: pnpm check + + - 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 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + +# npm publication is a maintainer-run local process and is intentionally absent. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a549a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz +.artifacts/ +storybook-static/ + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp directory +.temp + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4c2f52b --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=true +strict-peer-dependencies=false diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0981b7c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "printWidth": 120 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aabede7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# Constructive Blocks Repository Guide + +This public monorepo owns the Constructive Blocks documentation, the +`@constructive` shadcn registry, and the `@constructive-io/ui` and +`@constructive-io/schema-builder` packages. + +## Invariants + +- Canonical source lives in `apps/blocks`, `packages/ui`, and + `packages/schema-builder`; do not edit generated registry output. +- Keep the registry collision-free and `@constructive`-namespaced. +- Preserve registry requirements sidecars. +- Never auto-discover or mutate sibling repositories from flow tooling. +- Keep generated SDK fixtures pruned and normal CI independent of live endpoints. +- Never add automated npm publishing; publish verified tarballs manually. + +## Verification + +Use Node 22 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 new file mode 100644 index 0000000..825d0f4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# Constructive Blocks Repository Guide + +This public monorepo owns the Constructive Blocks documentation, the +`@constructive` shadcn registry, and the `@constructive-io/ui` and +`@constructive-io/schema-builder` packages. + +## Invariants + +- `apps/blocks`, `packages/ui`, and `packages/schema-builder` are canonical + source trees; generated registry output is never edited. +- The combined registry must remain collision-free and every install command + must use the `@constructive` namespace. +- Registry requirements sidecars are part of the public contract. +- Flow outputs outside this repository are generated only when explicit output + environment variables are supplied. +- Generated SDK fixtures are intentionally pruned. Refresh them explicitly; + do not add complete SDK trees or make normal CI depend on live endpoints. +- npm packages are versioned with Lerna and published manually from locally + verified tarballs. Do not add automated npm publishing. + +## Commands + +```bash +pnpm check +pnpm build:pages +pnpm build:storybook +pnpm pack:local +``` + +Use Node 22 and pnpm 10.28.0. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c702001 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Constructive + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..36fd073 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Constructive Blocks + +Constructive Blocks is the public home for the Constructive shadcn registry, +component documentation, and published React packages. + +## Workspaces + +- `apps/blocks` — documentation, demos, flows, 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. + +The documentation site is published at +. Registry JSON is served from +`https://constructive-io.github.io/blocks/r/{name}.json`. + +## Development + +```bash +pnpm install +pnpm check +pnpm build:pages +pnpm pack:local +``` + +`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. + +## Releases + +Releases use independent Lerna versions. A maintainer runs +`pnpm release:version`, pushes the reviewed release commit and tags, waits for +CI, runs `pnpm pack:local` from the validated tag, and publishes each tarball +manually with npm. GitHub Actions never publishes npm packages. + +## License + +MIT diff --git a/apps/blocks/.gitignore b/apps/blocks/.gitignore new file mode 100644 index 0000000..09acd9e --- /dev/null +++ b/apps/blocks/.gitignore @@ -0,0 +1,9 @@ +.next +node_modules +next-env.d.ts +.source +# build:registry output + temp transformed registry +public/r/ +.registry-build.json +playwright-report/ +test-results/ diff --git a/apps/blocks/README.md b/apps/blocks/README.md new file mode 100644 index 0000000..72dfd69 --- /dev/null +++ b/apps/blocks/README.md @@ -0,0 +1,59 @@ +# Blocks + +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. + +Live: **https://constructive-io.github.io/blocks/** (GitHub Pages, static export under basePath `/blocks`). + +## Develop + +```bash +pnpm --filter blocks dev # Next.js + Turbopack on http://localhost:3005 +``` + +`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 + +`.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. diff --git a/apps/blocks/catalog/blocks.json b/apps/blocks/catalog/blocks.json new file mode 100644 index 0000000..ef6f2c3 --- /dev/null +++ b/apps/blocks/catalog/blocks.json @@ -0,0 +1,841 @@ +{ + "version": 1, + "blocks": [ + { + "name": "auth-sign-in-card", + "slug": "auth-sign-in-card", + "title": "Auth Sign In Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Email+password sign-in form; MFA-branch aware", + "specFile": "auth-sign-in-card.md" + }, + { + "name": "auth-sign-in-page", + "slug": "auth-sign-in-page", + "title": "Auth Sign In Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Next.js route composing sign-in-card + layout-kit", + "specFile": "auth-sign-in-page.md" + }, + { + "name": "auth-sign-up-card", + "slug": "auth-sign-up-card", + "title": "Auth Sign Up Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Email+password registration form", + "specFile": "auth-sign-up-card.md" + }, + { + "name": "auth-sign-up-page", + "slug": "auth-sign-up-page", + "title": "Auth Sign Up Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Next.js route composing sign-up-card + layout-kit", + "specFile": "auth-sign-up-page.md" + }, + { + "name": "auth-forgot-password-card", + "slug": "auth-forgot-password-card", + "title": "Auth Forgot Password Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Requests password reset email", + "specFile": "auth-forgot-password-card.md" + }, + { + "name": "auth-forgot-password-page", + "slug": "auth-forgot-password-page", + "title": "Auth Forgot Password Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Next.js route composing forgot-password-card", + "specFile": "auth-forgot-password-page.md" + }, + { + "name": "auth-reset-password-card", + "slug": "auth-reset-password-card", + "title": "Auth Reset Password Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "New-password form; consumes role_id+token from URL", + "specFile": "auth-reset-password-card.md" + }, + { + "name": "auth-reset-password-page", + "slug": "auth-reset-password-page", + "title": "Auth Reset Password Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Next.js route composing reset-password-card", + "specFile": "auth-reset-password-page.md" + }, + { + "name": "auth-verify-email-page", + "slug": "auth-verify-email-page", + "title": "Auth Verify Email Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Handles the ?email_id=&token= email-verification callback", + "specFile": "auth-verify-email-page.md" + }, + { + "name": "auth-verify-email-banner", + "slug": "auth-verify-email-banner", + "title": "Auth Verify Email Banner", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Inline prompt to verify email; shows send-again action", + "specFile": "auth-verify-email-banner.md" + }, + { + "name": "auth-change-password-form", + "slug": "auth-change-password-form", + "title": "Auth Change Password Form", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Current+new password form; step-up tier=medium gates submission", + "specFile": "auth-change-password-form.md" + }, + { + "name": "auth-sign-out-button", + "slug": "auth-sign-out-button", + "title": "Auth Sign Out Button", + "category": "auth", + "type": "primitive", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Single sign-out button; ends the session and clears local cache", + "specFile": "auth-sign-out-button.md" + }, + { + "name": "auth-anonymous-sign-in-button", + "slug": "auth-anonymous-sign-in-button", + "title": "Auth Anonymous Sign In Button", + "category": "auth", + "type": "primitive", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Single button; starts an anonymous guest session", + "specFile": "auth-anonymous-sign-in-button.md" + }, + { + "name": "auth-cross-origin-link", + "slug": "auth-cross-origin-link", + "title": "Auth Cross Origin Link", + "category": "auth", + "type": "primitive", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Cross-origin token exchange UI; redirects to another origin with a one-time token", + "specFile": "auth-cross-origin-link.md" + }, + { + "name": "auth-social-buttons", + "slug": "auth-social-buttons", + "title": "Auth Social Buttons", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Dynamic OAuth provider buttons; queries identityProviders by default", + "specFile": "auth-social-buttons.md" + }, + { + "name": "auth-social-providers-grid", + "slug": "auth-social-providers-grid", + "title": "Auth Social Providers Grid", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "All enabled OAuth providers as a sign-in/sign-up grid; queries enabled identity providers", + "specFile": "auth-social-providers-grid.md" + }, + { + "name": "auth-mfa-totp-challenge", + "slug": "auth-mfa-totp-challenge", + "title": "Auth Mfa Totp Challenge", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "6-digit TOTP code input for the MFA challenge step", + "specFile": "auth-mfa-totp-challenge.md" + }, + { + "name": "auth-mfa-totp-challenge-page", + "slug": "auth-mfa-totp-challenge-page", + "title": "Auth Mfa Totp Challenge Page", + "category": "auth", + "type": "page", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Next.js route at /auth/mfa/totp; reads ?token= + ?redirect= and mounts the challenge card", + "specFile": "auth-mfa-totp-challenge-page.md" + }, + { + "name": "auth-passkey-enroll", + "slug": "auth-passkey-enroll", + "title": "Auth Passkey Enroll", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "WebAuthn passkey credential registration; override-driven enroll path", + "specFile": "auth-passkey-enroll.md" + }, + { + "name": "auth-passkey-sign-in", + "slug": "auth-passkey-sign-in", + "title": "Auth Passkey Sign In", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "WebAuthn passkey assertion for sign-in; override-driven verify path", + "specFile": "auth-passkey-sign-in.md" + }, + { + "name": "auth-passkey-management-list", + "slug": "auth-passkey-management-list", + "title": "Auth Passkey Management List", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Lists + renames + removes registered passkeys; step-up tier=high for delete", + "specFile": "auth-passkey-management-list.md" + }, + { + "name": "auth-step-up-dialog", + "slug": "auth-step-up-dialog", + "title": "Auth Step Up Dialog", + "category": "auth", + "type": "dialog", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Shared step-up verifier (password or TOTP); used by sensitive blocks", + "specFile": "auth-step-up-dialog.md" + }, + { + "name": "use-step-up", + "slug": "use-step-up", + "title": "Use Step Up", + "category": "auth", + "type": "hook", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "await stepUp({ tier: 'high' |'medium' }) imperative hook; wraps dialog trigger", + "specFile": "use-step-up.md" + }, + { + "name": "auth-account-profile-card", + "slug": "auth-account-profile-card", + "title": "Auth Account Profile Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Display name + profile picture upload", + "specFile": "auth-account-profile-card.md" + }, + { + "name": "auth-account-emails-list", + "slug": "auth-account-emails-list", + "title": "Auth Account Emails List", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Multi-email add/verify/set-primary/remove with resend verification", + "specFile": "auth-account-emails-list.md" + }, + { + "name": "auth-account-security-card", + "slug": "auth-account-security-card", + "title": "Auth Account Security Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Password change + MFA status summary + passkeys overview", + "specFile": "auth-account-security-card.md" + }, + { + "name": "auth-account-sessions-list", + "slug": "auth-account-sessions-list", + "title": "Auth Account Sessions List", + "category": "auth", + "type": "card", + "status": "api-config-pending", + "statusLabel": "out-of-frontend-scope (API-config-pending)", + "purpose": "Lists active sessions with individual and revoke-all actions; sensitive revokes require step-up", + "specFile": "auth-account-sessions-list.md" + }, + { + "name": "auth-account-api-keys-list", + "slug": "auth-account-api-keys-list", + "title": "Auth Account Api Keys List", + "category": "auth", + "type": "card", + "status": "api-config-pending", + "statusLabel": "out-of-frontend-scope (API-config-pending)", + "purpose": "Lists user-scoped API keys with revoke and create-key actions", + "specFile": "auth-account-api-keys-list.md" + }, + { + "name": "auth-api-key-create-dialog", + "slug": "auth-api-key-create-dialog", + "title": "Auth Api Key Create Dialog", + "category": "auth", + "type": "dialog", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "API key creation flow; step-up tier=high gates submission", + "specFile": "auth-api-key-create-dialog.md" + }, + { + "name": "auth-api-key-created-modal", + "slug": "auth-api-key-created-modal", + "title": "Auth Api Key Created Modal", + "category": "auth", + "type": "dialog", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "One-time display of new API key value", + "specFile": "auth-api-key-created-modal.md" + }, + { + "name": "auth-account-connected-accounts", + "slug": "auth-account-connected-accounts", + "title": "Auth Account Connected Accounts", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Settings surface: linked OAuth providers with disconnect; step-up tier=medium for disconnect", + "specFile": "auth-account-connected-accounts.md" + }, + { + "name": "auth-account-danger-card", + "slug": "auth-account-danger-card", + "title": "Auth Account Danger Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Delete account entry point; step-up tier=high; sends deletion email", + "specFile": "auth-account-danger-card.md" + }, + { + "name": "auth-account-deletion-confirm-page", + "slug": "auth-account-deletion-confirm-page", + "title": "Auth Account Deletion Confirm Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Handles ?user_id=&token= deletion confirmation", + "specFile": "auth-account-deletion-confirm-page.md" + }, + { + "name": "auth-account-settings-page", + "slug": "auth-account-settings-page", + "title": "Auth Account Settings Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Tabs page composing all account section cards", + "specFile": "auth-account-settings-page.md" + }, + { + "name": "auth-invitation-acceptance-card", + "slug": "auth-invitation-acceptance-card", + "title": "Auth Invitation Acceptance Card", + "category": "auth", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Invitation acceptance card for embedding in your own route; the page block composes it", + "specFile": "auth-invitation-acceptance-card.md" + }, + { + "name": "auth-invitation-acceptance-page", + "slug": "auth-invitation-acceptance-page", + "title": "Auth Invitation Acceptance Page", + "category": "auth", + "type": "page", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Token-gated invite claim page; composes invitation-acceptance-card", + "specFile": "auth-invitation-acceptance-page.md" + }, + { + "name": "auth-magic-link-request-card", + "slug": "auth-magic-link-request-card", + "title": "Auth Magic Link Request Card", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Magic-link email request form; override-driven send path", + "specFile": "auth-magic-link-request-card.md" + }, + { + "name": "auth-magic-link-sent-page", + "slug": "auth-magic-link-sent-page", + "title": "Auth Magic Link Sent Page", + "category": "auth", + "type": "page", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Post-request confirmation screen", + "specFile": "auth-magic-link-sent-page.md" + }, + { + "name": "auth-magic-link-callback-page", + "slug": "auth-magic-link-callback-page", + "title": "Auth Magic Link Callback Page", + "category": "auth", + "type": "page", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Handles the ?token= callback; override-driven verification path", + "specFile": "auth-magic-link-callback-page.md" + }, + { + "name": "auth-email-otp-request-card", + "slug": "auth-email-otp-request-card", + "title": "Auth Email Otp Request Card", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Email OTP send form; override-driven send path", + "specFile": "auth-email-otp-request-card.md" + }, + { + "name": "auth-email-otp-input", + "slug": "auth-email-otp-input", + "title": "Auth Email Otp Input", + "category": "auth", + "type": "primitive", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "6-digit OTP entry with auto-submit and resend; override-driven verify path", + "specFile": "auth-email-otp-input.md" + }, + { + "name": "auth-mfa-totp-enroll", + "slug": "auth-mfa-totp-enroll", + "title": "Auth Mfa Totp Enroll", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "TOTP enrollment: QR + manual key + confirm code; override-driven enroll path", + "specFile": "auth-mfa-totp-enroll.md" + }, + { + "name": "auth-mfa-totp-disable-confirm", + "slug": "auth-mfa-totp-disable-confirm", + "title": "Auth Mfa Totp Disable Confirm", + "category": "auth", + "type": "dialog", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Confirm + step-up tier=high before disabling TOTP; override-driven disable path", + "specFile": "auth-mfa-totp-disable-confirm.md" + }, + { + "name": "auth-mfa-backup-codes-display", + "slug": "auth-mfa-backup-codes-display", + "title": "Auth Mfa Backup Codes Display", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Shows freshly generated backup codes; one-time view with copy and download", + "specFile": "auth-mfa-backup-codes-display.md" + }, + { + "name": "auth-mfa-backup-codes-regenerate", + "slug": "auth-mfa-backup-codes-regenerate", + "title": "Auth Mfa Backup Codes Regenerate", + "category": "auth", + "type": "dialog", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Regenerates backup codes; step-up gated; override-driven regenerate path", + "specFile": "auth-mfa-backup-codes-regenerate.md" + }, + { + "name": "auth-account-phones-list", + "slug": "auth-account-phones-list", + "title": "Auth Account Phones List", + "category": "auth", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Phone number add/verify/remove", + "specFile": "auth-account-phones-list.md" + }, + { + "name": "auth-sso-setup-card", + "slug": "auth-sso-setup-card", + "title": "Auth Sso Setup Card", + "category": "auth", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "Enterprise SSO provider configuration card (OIDC and SAML)", + "specFile": "auth-sso-setup-card.md" + }, + { + "name": "auth-sso-sign-in-card", + "slug": "auth-sso-sign-in-card", + "title": "Auth Sso Sign In Card", + "category": "auth", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "SSO-initiated sign-in card with email-domain lookup", + "specFile": "auth-sso-sign-in-card.md" + }, + { + "name": "auth-domain-verification-step", + "slug": "auth-domain-verification-step", + "title": "Auth Domain Verification Step", + "category": "auth", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "DNS TXT record verification step for an SSO domain claim", + "specFile": "auth-domain-verification-step.md" + }, + { + "name": "user-avatar", + "slug": "user-avatar", + "title": "User Avatar", + "category": "user", + "type": "component", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Renders profile_picture (image domain) with initials fallback; round for person, square for org", + "specFile": "user-avatar.md" + }, + { + "name": "user-context-switcher", + "slug": "user-context-switcher", + "title": "User Context Switcher", + "category": "user", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Switch the active acting-as context between personal account and orgs", + "specFile": "user-context-switcher.md" + }, + { + "name": "org-create-card", + "slug": "org-create-card", + "title": "Org Create Card", + "category": "org", + "type": "card", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "New organization creation wizard; name, slug, and optional logo", + "specFile": "org-create-card.md" + }, + { + "name": "org-members-list", + "slug": "org-members-list", + "title": "Org Members List", + "category": "org", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "List org members with role chips and remove action", + "specFile": "org-members-list.md" + }, + { + "name": "org-invite-dialog", + "slug": "org-invite-dialog", + "title": "Org Invite Dialog", + "category": "org", + "type": "dialog", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Send org membership invitation", + "specFile": "org-invite-dialog.md" + }, + { + "name": "org-roles-editor", + "slug": "org-roles-editor", + "title": "Org Roles Editor", + "category": "org", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Manage custom org roles + permissions", + "specFile": "org-roles-editor.md" + }, + { + "name": "org-settings-form", + "slug": "org-settings-form", + "title": "Org Settings Form", + "category": "org", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Org name, slug, avatar; delete-org (step-up)", + "specFile": "org-settings-form.md" + }, + { + "name": "org-app-memberships", + "slug": "org-app-memberships", + "title": "Org App Memberships", + "category": "org", + "type": "card", + "status": "backend-pending", + "statusLabel": "v1 (frontend ready, backend pending)", + "purpose": "Manage org's app-level memberships", + "specFile": "org-app-memberships.md" + }, + { + "name": "org-scim-token-generation-card", + "slug": "org-scim-token-generation-card", + "title": "Org Scim Token Generation Card", + "category": "org", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "Generate and revoke the SCIM bearer token for an identity provider", + "specFile": "org-scim-token-generation-card.md" + }, + { + "name": "org-scim-connections-list", + "slug": "org-scim-connections-list", + "title": "Org Scim Connections List", + "category": "org", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "Lists active SCIM provider connections for an organization", + "specFile": "org-scim-connections-list.md" + }, + { + "name": "org-scim-setup-guide", + "slug": "org-scim-setup-guide", + "title": "Org Scim Setup Guide", + "category": "org", + "type": "stub", + "status": "planned", + "statusLabel": "v2 (deferred)", + "purpose": "Provider-specific SCIM 2.0 provisioning setup guide", + "specFile": "org-scim-setup-guide.md" + }, + { + "name": "shell-sidebar", + "slug": "shell-sidebar", + "title": "Shell Sidebar", + "category": "shell", + "type": "layout", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "App sidebar with nav + context switcher slot", + "specFile": "shell-sidebar.md" + }, + { + "name": "shell-header", + "slug": "shell-header", + "title": "Shell Header", + "category": "shell", + "type": "layout", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Top header bar with breadcrumbs + account menu slot", + "specFile": "shell-header.md" + }, + { + "name": "shell-account-menu", + "slug": "shell-account-menu", + "title": "Shell Account Menu", + "category": "shell", + "type": "menu", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Dropdown: current user, switch context, sign out (uses auth-sign-out-button)", + "specFile": "shell-account-menu.md" + }, + { + "name": "shell-command-palette", + "slug": "shell-command-palette", + "title": "Shell Command Palette", + "category": "shell", + "type": "overlay", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Cmd-K command palette with grouped, permission-filtered commands", + "specFile": "shell-command-palette.md" + }, + { + "name": "shell-notifications", + "slug": "shell-notifications", + "title": "Shell Notifications", + "category": "shell", + "type": "panel", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Notification bell + panel", + "specFile": "shell-notifications.md" + }, + { + "name": "shell-breadcrumbs", + "slug": "shell-breadcrumbs", + "title": "Shell Breadcrumbs", + "category": "shell", + "type": "primitive", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Route-aware breadcrumbs", + "specFile": "shell-breadcrumbs.md" + }, + { + "name": "schema-builder-core", + "slug": "schema-builder-core", + "title": "Schema Builder — Core", + "category": "schema", + "type": "lib", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Shared config provider, schema selectors/store, read hooks, databases UI, and diagram substrate for the schema-builder family", + "specFile": "schema-builder-core.md" + }, + { + "name": "schema-builder-fields", + "slug": "schema-builder-fields", + "title": "Schema Builder — Structure", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Structure tab — the table editor for columns, types, defaults, and primary-key/unique constraints", + "specFile": "schema-builder-fields.md" + }, + { + "name": "schema-builder-relationships", + "slug": "schema-builder-relationships", + "title": "Schema Builder — Relationships", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Relationships tab — foreign keys plus the relation-provision and secure-table flows", + "specFile": "schema-builder-relationships.md" + }, + { + "name": "schema-builder-indexes", + "slug": "schema-builder-indexes", + "title": "Schema Builder — Indexes", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Indexes tab — create, edit, and drop a table's indexes", + "specFile": "schema-builder-indexes.md" + }, + { + "name": "schema-builder-policies", + "slug": "schema-builder-policies", + "title": "Schema Builder — Policies", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Policies tab — row-level security policy editing, table grants, and create-table-with-policies", + "specFile": "schema-builder-policies.md" + }, + { + "name": "schema-builder-tables", + "slug": "schema-builder-tables", + "title": "Schema Builder — Tables", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Tables sidebar — schema/table navigation tree, table metadata, and table create/rename/delete", + "specFile": "schema-builder-tables.md" + }, + { + "name": "schema-builder", + "slug": "schema-builder", + "title": "Schema Builder", + "category": "schema", + "type": "block", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "Composer shell wiring core + the Structure / Relationships / Indexes / Policies area blocks into one database schema editor", + "specFile": "schema-builder.md" + }, + { + "name": "chat", + "slug": "chat", + "title": "Chat Widget", + "category": "chat", + "type": "widget", + "status": "ready", + "statusLabel": "v1 (frontend ready)", + "purpose": "AI chat widget with page-context awareness, provider settings, tool approval UI, and floating or embedded presentation", + "specFile": "chat.md" + }, + { + "name": "lib/auth-errors", + "slug": "lib-auth-errors", + "title": "Auth errors (parseGraphQLError)", + "category": "lib", + "type": "registry:lib", + "status": "ready", + "statusLabel": "v1", + "purpose": "parseGraphQLError(err, options) — PostGraphile error code → user string", + "specFile": null + }, + { + "name": "lib/auth/messages.ts", + "slug": "lib-auth-messages-ts", + "title": "Auth message types", + "category": "lib", + "type": "registry:lib", + "status": "ready", + "statusLabel": "v1", + "purpose": "Shared message type helpers (base catalog type, merge utility)", + "specFile": null + } + ] +} diff --git a/apps/blocks/components.json b/apps/blocks/components.json new file mode 100644 index 0000000..26ed6d4 --- /dev/null +++ b/apps/blocks/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@constructive": "https://constructive-io.github.io/blocks/r/{name}.json" + } +} diff --git a/apps/blocks/docs/redesign/DESIGN.md b/apps/blocks/docs/redesign/DESIGN.md new file mode 100644 index 0000000..01061d6 --- /dev/null +++ b/apps/blocks/docs/redesign/DESIGN.md @@ -0,0 +1,513 @@ +# 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 `