Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/website/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.
111 changes: 111 additions & 0 deletions apps/website/scripts/check-style-migration.mts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
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<string, unknown>)[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<string, number>();
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<string, number>();
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);
22 changes: 22 additions & 0 deletions apps/website/scripts/computed-style-snapshot.js
Original file line number Diff line number Diff line change
@@ -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`;
})();
64 changes: 7 additions & 57 deletions apps/website/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions apps/website/src/app/page.module.css

This file was deleted.

63 changes: 11 additions & 52 deletions apps/website/src/components/ui/BrowserFrame.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,12 +15,6 @@ interface BrowserFrameProps extends HTMLAttributes<HTMLDivElement> {
maxWidth?: number | string;
}

const ELEVATION: Record<Elevation, string> = {
sm: tokens.shadows.sm,
md: tokens.shadows.md,
lg: tokens.shadows.lg,
};

export function BrowserFrame({
children,
url,
Expand All @@ -35,67 +28,33 @@ export function BrowserFrame({
return (
<div
data-ui="browser-frame"
data-elevation={elevation}
className={cn(className)}
style={{
background: tokens.surfaces.surface,
border: `1px solid ${tokens.surfaces.border}`,
borderRadius: tokens.radius.lg,
boxShadow: ELEVATION[elevation],
overflow: 'hidden',
// Genuinely dynamic — computed from unbounded caller props, so these
// stay inline. Everything else here is static and lives in ui.css.
transform: rotate ? `rotate(${rotate}deg)` : undefined,
maxWidth,
...style,
}}
{...rest}
>
{/* Title bar */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '10px 14px',
background: tokens.surfaces.surfaceTinted,
borderBottom: `1px solid ${tokens.surfaces.border}`,
}}
>
<div data-ui="browser-frame-titlebar">
{/* Traffic lights */}
<div style={{ display: 'flex', gap: 6 }} aria-hidden="true">
<span style={{ width: 12, height: 12, borderRadius: tokens.radius.full, background: '#FF5F57' }} />
<span style={{ width: 12, height: 12, borderRadius: tokens.radius.full, background: '#FEBC2E' }} />
<span style={{ width: 12, height: 12, borderRadius: tokens.radius.full, background: '#28C840' }} />
<div data-ui="browser-frame-dots" aria-hidden="true">
<span />
<span />
<span />
</div>
{/* URL pill */}
{url ? (
<div
style={{
flex: 1,
textAlign: 'center',
fontFamily: tokens.typography.fontMono,
fontSize: 11,
color: tokens.colors.textMuted,
background: tokens.surfaces.surface,
border: `1px solid ${tokens.surfaces.border}`,
borderRadius: tokens.radius.sm,
padding: '4px 10px',
maxWidth: 360,
margin: '0 auto',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{url}
</div>
) : null}
{url ? <div data-ui="browser-frame-url">{url}</div> : null}
{/* Right spacer to balance traffic lights */}
<div style={{ width: 54 }} aria-hidden="true" />
<div data-ui="browser-frame-spacer" aria-hidden="true" />
</div>

{/* Frame body */}
<div data-ui="browser-frame-body" style={{ position: 'relative', background: tokens.surfaces.surface }}>
{children}
</div>
<div data-ui="browser-frame-body">{children}</div>
</div>
);
}
Loading
Loading