diff --git a/apps/website/next-env.d.ts b/apps/website/next-env.d.ts index fdbfe5258..c4b7818fb 100644 --- a/apps/website/next-env.d.ts +++ b/apps/website/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./../../dist/apps/website/.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/website/scripts/check-style-migration.mts b/apps/website/scripts/check-style-migration.mts new file mode 100644 index 000000000..8869892ba --- /dev/null +++ b/apps/website/scripts/check-style-migration.mts @@ -0,0 +1,111 @@ +#!/usr/bin/env tsx +/** + * Advisory value-equality check for the inline-style migration. + * + * Diffs the working tree (or HEAD) against a base ref, extracts + * `property: value` pairs REMOVED from .tsx style objects and pairs ADDED to + * the migration CSS files, normalises both sides (camelCase→kebab, React + * numeric px, tokens.* → resolved value, var(--x) → resolved value), and + * reports removals with no matching addition. + * + * KNOWN BLIND SPOTS (by design — this is a text tool, not a harness): + * - cascade/specificity: a correct value can still lose to another rule; + * - shorthand vs longhand (`padding: '0 16px'` vs padding-top…); + * - selectors: it compares property/value multisets, not which element + * they apply to. + * Treat every flagged line as a question to answer in the PR body, not + * necessarily a bug. + * + * Usage: npx tsx apps/website/scripts/check-style-migration.mts [baseRef=origin/main] [--strict] + */ +import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +// NOTE: libs/design-tokens has no `"type": "module"` in its package.json, so +// Node's ESM loader resolves this .ts file as CommonJS. A static named +// import (`import { tokens } from ...`) then depends on cjs-module-lexer +// statically detecting the export from tsx's on-the-fly transform, which it +// does not for this file — it only ever finds the synthetic `default` +// (whole-module) binding. Importing the namespace and reading `.default` +// sidesteps that without touching the shared library's module format. +import * as designTokens from '../../../libs/design-tokens/src/index.ts'; +const { tokens } = (designTokens as unknown as { default: typeof designTokens }).default + ?? designTokens; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = resolve(HERE, '..', '..', '..'); +const BASE = process.argv[2] && !process.argv[2].startsWith('--') + ? process.argv[2] : 'origin/main'; + +// --- resolve var(--x) via the committed theme.css + website-local :root vars +const themeCss = readFileSync( + resolve(REPO, 'libs/design-tokens/src/lib/theme.css'), 'utf8'); +const globalCss = readFileSync( + resolve(REPO, 'apps/website/src/app/global.css'), 'utf8'); +const cssVars = new Map(); +for (const m of (themeCss + globalCss).matchAll(/(--[a-z0-9-]+):\s*([^;]+);/g)) + cssVars.set(m[1], m[2].trim()); + +// --- resolve tokens.a.b.c to its value +function tokenValue(path: string): string | undefined { + let cur: unknown = tokens; + for (const k of path.split('.')) { + if (cur == null || typeof cur !== 'object') return undefined; + cur = (cur as Record)[k]; + } + return cur == null || typeof cur === 'object' ? undefined : String(cur); +} + +const UNITLESS = new Set(['line-height','font-weight','opacity','z-index', + 'flex','flex-grow','flex-shrink','order']); + +function normProp(p: string): string { + return p.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); +} +function normValue(prop: string, raw: string): string { + let v = raw.trim().replace(/^['"`]|['"`]$/g, ''); + const tok = v.match(/^tokens\.([a-zA-Z.]+)$/); + if (tok) v = tokenValue(tok[1]) ?? v; + v = v.replace(/var\((--[a-z0-9-]+)\)/g, (_, name) => cssVars.get(name) ?? _); + if (/^-?\d+(\.\d+)?$/.test(v) && !UNITLESS.has(prop)) v = `${v}px`; + return v.replace(/\s+/g, ' ').toLowerCase(); +} + +function diffLines(pathspec: string, sign: '+' | '-'): string[] { + const out = execSync( + `git diff -U0 ${BASE} -- ${pathspec}`, { cwd: REPO, encoding: 'utf8' }); + return out.split('\n') + .filter((l) => l.startsWith(sign) && !l.startsWith(sign.repeat(3))) + .map((l) => l.slice(1)); +} + +// pairs removed from TSX (style-object members: `foo: bar,`) +const removed = new Map(); +for (const line of diffLines("'apps/website/src/**/*.tsx'", '-')) { + const m = line.match(/^\s*([a-zA-Z]+):\s*(.+?),?\s*$/); + if (!m) continue; + const prop = normProp(m[1]); + if (!/^[a-z-]+$/.test(prop)) continue; + const key = `${prop} :: ${normValue(prop, m[2])}`; + removed.set(key, (removed.get(key) ?? 0) + 1); +} +// pairs added to migration CSS +const added = new Map(); +for (const line of diffLines( + "'apps/website/src/styles/*.css' 'apps/website/src/app/global.css'", '+')) { + for (const m of line.matchAll(/([a-z-]+):\s*([^;{}]+);/g)) { + const key = `${m[1]} :: ${normValue(m[1], m[2])}`; + added.set(key, (added.get(key) ?? 0) + 1); + } +} + +let flagged = 0; +for (const [key, n] of [...removed.entries()].sort()) { + if (!added.has(key)) { + console.log(`REMOVED, NO MATCHING CSS (${n}x) ${key}`); + flagged++; + } +} +console.log(`\n${removed.size} distinct pairs removed, ${added.size} added, ${flagged} unaccounted.`); +if (flagged > 0 && process.argv.includes('--strict')) process.exit(1); diff --git a/apps/website/scripts/computed-style-snapshot.js b/apps/website/scripts/computed-style-snapshot.js new file mode 100644 index 000000000..d5ee9a2cd --- /dev/null +++ b/apps/website/scripts/computed-style-snapshot.js @@ -0,0 +1,22 @@ +/** + * Paste into the browser console on a page BEFORE and AFTER a migration + * batch (on main's dev server, then the branch's), then diff the two JSONs. + * Serialises computed styles for every element carrying a migration hook. + */ +(() => { + const PROPS = ['color','background-color','border-color','border-radius', + 'font-size','font-family','font-weight','line-height','letter-spacing', + 'padding','margin','gap','height','width','max-width','box-shadow', + 'display','align-items','justify-content','text-transform','opacity']; + const hooks = document.querySelectorAll( + '[data-ui],[data-mdx],[data-docs-navlink],[class]'); + const out = {}; + hooks.forEach((el, i) => { + const key = `${el.tagName}#${el.id || i}.${el.getAttribute('data-ui') + || el.getAttribute('data-mdx') || String(el.className).slice(0, 40)}`; + const cs = getComputedStyle(el); + out[key] = Object.fromEntries(PROPS.map((p) => [p, cs.getPropertyValue(p)])); + }); + copy(JSON.stringify(out, null, 1)); + return `snapshot of ${hooks.length} elements copied to clipboard`; +})(); diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index 2869e1953..efe57991a 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -30,6 +30,13 @@ --docs-accent-tint-line: color-mix(in srgb, var(--color-accent) 10%, transparent); } +@import "../styles/ui.css"; +@import "../styles/chrome.css"; +@import "../styles/docs.css"; +@import "../styles/landing.css"; +@import "../styles/marketing.css"; +@import "../styles/pages.css"; + * { box-sizing: border-box; } @@ -225,63 +232,6 @@ html { .docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--docs-accent-tint-soft); color: var(--color-text-secondary); } .docs-prose td code { font-size: 0.8em; } -/* UI primitive — Card. - * Resting background / border / shadow live here (not inline on the element) - * so the :hover rules below can override border-color and box-shadow. Inline - * styles beat any stylesheet :hover rule, which previously left the lift - * rendering only the transform. See components/ui/Card.tsx. */ -[data-ui="card"] { - background: var(--color-surface); - border: 1px solid var(--color-border); - box-shadow: var(--shadow-sm); - transition: box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease; -} -[data-ui="card"][data-surface="tinted"] { - background: var(--color-surface-tinted); -} -[data-ui="card"][data-surface="dim"] { - background: var(--color-surface-dim); -} -/* Accent-filled cards (e.g. docs backend / generative-UI cards). */ -[data-ui="card"][data-accent] { - background: var(--color-accent-surface); - border-color: var(--color-accent-border); -} - -/* Hoverable cards are virtually always links; pointer is the right default. - * If a non-link gets `data-hoverable`, override inline. */ -[data-ui="card"][data-hoverable] { - cursor: pointer; -} -[data-ui="card"][data-hoverable]:not([data-accent]):hover { - box-shadow: var(--shadow-md); - border-color: var(--color-border-strong); - transform: translateY(-1px); -} -/* Accent cards keep their accent border and gain an accent ring on hover. */ -[data-ui="card"][data-hoverable][data-accent]:hover { - box-shadow: 0 0 0 3px var(--color-accent-glow), var(--shadow-md); - transform: translateY(-1px); -} -@media (prefers-reduced-motion: reduce) { - [data-ui="card"][data-hoverable]:hover { - transform: none; - } -} - -/* UI primitive — FAQ */ -[data-ui="faq-item"] > summary::-webkit-details-marker { - display: none; -} -[data-ui="faq-item"][open] [data-ui="faq-chevron"] { - transform: rotate(180deg); -} -[data-ui="faq-item"] > summary:focus-visible { - outline: none; - box-shadow: var(--shadow-focus); - border-radius: var(--radius-sm); -} - /* AG-UI architecture diagram */ .ag-ui-arch-grid { display: grid; diff --git a/apps/website/src/app/page.module.css b/apps/website/src/app/page.module.css deleted file mode 100644 index 8a13e21cb..000000000 --- a/apps/website/src/app/page.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.page { -} diff --git a/apps/website/src/components/ui/BrowserFrame.tsx b/apps/website/src/components/ui/BrowserFrame.tsx index 39ff24c28..9866f3cf2 100644 --- a/apps/website/src/components/ui/BrowserFrame.tsx +++ b/apps/website/src/components/ui/BrowserFrame.tsx @@ -1,5 +1,4 @@ import type { ReactNode, HTMLAttributes } from 'react'; -import { tokens } from '@threadplane/design-tokens'; import { cn } from '../../lib/cn'; type Elevation = 'sm' | 'md' | 'lg'; @@ -16,12 +15,6 @@ interface BrowserFrameProps extends HTMLAttributes { maxWidth?: number | string; } -const ELEVATION: Record = { - sm: tokens.shadows.sm, - md: tokens.shadows.md, - lg: tokens.shadows.lg, -}; - export function BrowserFrame({ children, url, @@ -35,13 +28,11 @@ export function BrowserFrame({ return (
{/* Title bar */} -
+
{/* Traffic lights */} -