mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(design-systems): add structured tokens.css schema (default + kami) Compile each brand's DESIGN.md prose into a machine-readable :root block agents paste verbatim, removing the "Primary → --accent" translation step where most token misuse happens. Daemon prompt injection lands in a follow-up; lint-artifact already enforces the shared token vocabulary so no rule changes needed. Schema validated across two contrasting aesthetics: - default (sans-serif, cobalt, B2B utility) — stress test the shallow form, 2-level fg / 2-level surface - kami (serif, parchment, ink-blue, print-first) — stress test the rich form, 4-level fg ramp, 3-level surface, ring elevation, i18n font stacks, and solid-hex tag tints (print renderers double-paint alpha) Schema growth from kami's stress test (5 new optional slots, all backward-compatible — default aliases via var() to existing tokens): - --fg-2 / --meta (4-level fg ramp) - --surface-warm (3-level surface) - --border-soft (2-level border) - --elev-ring (ring elevation as first-class level) Brand-specific extensions live in tokens.css with explicit "NOT in shared schema" labels and a documented promotion path (≥2 brands need it → promote to schema slot). components.html in each brand is a self-contained reference fixture that exercises every token through real layouts. Both fixtures lint clean against apps/daemon/src/lint-artifact.ts. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): add token-fixture drift guard Each design system in design-systems/<brand>/ ships two files agents consume in tandem: tokens.css (canonical token bindings) and components.html (a self-contained fixture whose first <style> embeds the same :root paste so the file renders standalone). The fixture's :root block is a copy of tokens.css's :root block, kept in sync only by an inline comment. This adds scripts/check-tokens-fixture-sync.ts and registers it in pnpm guard. The check pairs each brand's tokens.css with its components.html and asserts the unscoped :root block is byte-equivalent after canonical normalization (CSS comments stripped, whitespace collapsed, separator spacing normalized). Brands missing one half of the pair, or with no :root rule in either file, fail the guard. Scoped overrides like :root[lang="zh-CN"] are not required to appear in the fixture (per the kami fixture's inline comment they are pasted only when an artifact's <html lang> matches), so the check only compares the unscoped :root block. Verified: pnpm guard passes for default + kami, fails on intentional value drift, fails on missing token, tolerates whitespace-only formatting differences. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(design-systems): point fixture CTAs to real files Both default and kami components.html advertised in-page anchors (#tokens, #spec, #surface, #accent, #type, #components) but defined no matching ids, so every CTA was a no-op when the fixture was opened locally — flagged by mrcfps in #1231. Re-point each link to a real artifact in the same brand directory: - "View tokens" / "Inspect tokens" / "Inspect typography" → ./tokens.css - "Read the spec" / "Read the rule" → ./DESIGN.md Browsers render these as raw source views, which is the desired UX for a reference fixture: clicking the CTA shows the underlying contract instead of jumping to nothing. Agents copying the fixture also learn the pattern of "buttons link to actual sibling resources". The :root token block is unchanged, so the token-fixture drift guard still passes for both brands. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): codify token schema (A1/A2/B/C layers) The two-brand pilot (default + kami) settled the shape of the shared token schema; this commit codifies it as a machine-readable contract and enforces it in pnpm guard, addressing lefarcen's review on #1231: > the optional-vs-required split won't generalize cleanly when brand > #3 needs different Layer A tokens or when multiple brands converge > on the same extension (promoting C→B→A). Consider surfacing that > limitation in the PR narrative or in a future SCHEMA.md. Schema lives under design-systems/_schema/ as three files: - tokens.schema.ts — TypeScript declaration of every shared token with its layer (A1-identity / A1-structure / A2 / B-slot), plus per-brand C-extension allowlists and a global C-prefix allowlist - defaults.css — CSS mirror of A2 fallback values, used as the human-readable contract reviewer's-eye copy and the future input to the derive script - AGENTS.md — schema layer model, C → B-slot → A2 promotion rules, when-not-to-add-a-token guidance Layer model: A1-identity 8 tokens — bg/surface/fg/muted/border/accent + font-display/font-body. The brand IS these values; no fallback is defensible. A1-structure 18 tokens — type scale (8), leading (2), tracking (1), section-y (3), container (4). Structural decisions vary per brand by design and have no cross-brand default. A2 26 tokens — accent states, semantic colors, motion, base spacing scale, radius, elevation, focus, font-mono. Required in every tokens.css; fallback lives in defaults.css for the future derive script to inline when DESIGN.md does not specify the value. B-slot 4 tokens — fg-2 / meta / surface-warm / border-soft. Brand may bind independently or alias the named sibling via var(...) for components that target the richer ramp. C-extension n tokens — brand-specific names (kami's tag-bg-*, leading-display, accent-light, etc.). Allowlisted per-brand in BRAND_EXTENSIONS or globally by prefix in BRAND_EXTENSION_PREFIXES. Promote when a second brand adopts the same name. Why A2 fails the guard today: Artifacts are generated by agents pasting one brand's :root block into a single <style>; there is no global stylesheet that supplies fallbacks at runtime. A tokens.css missing an A2 declaration would silently break any var() reference in the fixture. Until the derive script (PR-B) lands and inlines defaults, every brand's tokens.css must declare every A2 token directly. The guard enforces this strictly. Why --font-mono lands in A2 (not A1): 149 brands' DESIGN.md files were surveyed: 87 (58%) declare a monospace stack, 62 (42%) do not — including major brands like bmw / nike / apple / notion / mastercard / meta. Agent paste cannot rely on the brand author having written it down; a defaultable A2 fallback (with CJK brands like kami overriding) is safer than forcing every brand author to add a field they may not realize their kbd / code-block components need. Five guard checks, each registered as its own entry in scripts/guard.ts so failures attribute to a specific contract: 1. token-fixture sync — components.html :root ↔ tokens.css :root byte-equivalent (existing) 2. A1 required tokens — every brand declares every A1 token 3. A2 required tokens — every brand declares every A2 token 4. unknown token allowlist — every declared token is in schema or brand-extension allowlist 5. A2 defaults parity — defaults.css ↔ tokens.schema.ts fallback byte-equivalent Verified on default + kami: - 26 A1 tokens declared in both brands - 26 A2 tokens declared in both brands - 129 total declarations, all match shared schema or brand extensions - defaults.css ↔ tokens.schema.ts parity holds - sanity test: drifting --motion-fast in defaults.css fails check 5 with a clear divergence message The PR description originally listed "Dedicated SCHEMA.md" as explicitly NOT in this PR ("Once 3+ brands ship, extracting a single source of truth becomes worthwhile"). That boundary moves: lefarcen's review surfaced the schema-generalization risk, and the schema must exist as a machine-enforced contract before the derive script can read it. The TS file replaces the markdown that was deferred. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web/tests): pass missing designTemplates prop to ProjectView Pre-existing typecheck regression on main: PR #955 (b5eb8c16, "generic skills + split skills/design-templates + finalize-design API") added required `designTemplates: SkillSummary[]` to ProjectView Props but updated only two of the three test fixtures that render ProjectView directly. The third — ProjectView.api-empty-response.test.tsx — was missed, so `pnpm typecheck` (and CI on any PR merging into main) fails on: apps/web/tests/components/ProjectView.api-empty-response.test.tsx (168,6): error TS2741: Property 'designTemplates' is missing in type ... The other two ProjectView tests already pass `designTemplates={[]}`, so this aligns this fixture with the existing pattern. Out of scope for #1231 strictly, but the regression blocks the merged-state typecheck CI runs that #1231 triggers, and the one-line fix here restores main's typecheck health for everyone. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(design-systems): enforce B-slot required tokens in pnpm guard Closes mrcfps + lefarcen review comment thread on #1231: > The guard validates A2 required tokens here, but there's no > sibling check for B-slot aliases (--fg-2, --meta, --surface-warm, > --border-soft). Per the schema docs, every brand must declare > A1 + A2 + B-slot names so shared components can safely read > var(--fg-2) etc. Without a B-slot guard, a brand can omit those > aliases, pass pnpm guard, and break any artifact that references > them. Same artifact-paste constraint as A2: agents render artifacts by pasting one brand's :root block into a single <style>; there is no runtime cascade, so a missing B-slot makes any var(--fg-2) reference resolve to nothing. Until now the schema narrative claimed B-slots were optional with a var() default, but no machine check enforced declaration — a contract gap reviewers reasonably refused to merge. This commit closes the gap in three places so machine and narrative agree: 1. scripts/check-tokens-fixture-sync.ts - Add checkDesignSystemBSlotRequiredTokens, mirroring the A2 check but using getBSlotNames() from the schema. - Failure message names each missing slot AND the schema-suggested alias (--fg-2 (default alias: var(--fg))) so a brand author fixing the failure has a copy-pasteable resolution. - Renumber section comments: 5 checks → 6 checks. 2. scripts/guard.ts - Register the new check between A2 required and unknown allowlist so failures attribute to a specific contract. 3. design-systems/_schema/AGENTS.md - Update the layer table: B-slot row's "If omitted" column changes from "resolves via var() to a richer sibling" to "guard fails — brand must declare, either as var(--sibling) (collapsed) or independent value (richer)". - Add a "Why B-slot is required (and what the alias is for)" section that distinguishes the schema-suggested alias from a runtime fallback, with worked examples for default (alias) and kami (independent bind). Verified on default + kami: - pnpm guard passes all 6 design-system checks - 4 B-slot tokens declared in both brands (default aliases via var(), kami binds independently — both forms satisfy the contract) - pnpm typecheck clean across the workspace - Sanity test: removing --fg-2 + --meta from default/tokens.css fires the new guard with a precise per-token alias hint: [default] design-systems/default/tokens.css is missing 2 B-slot tokens (alias the named sibling via var(...) or bind independently): --fg-2 (default alias: var(--fg)), --meta (default alias: var(--muted)) The schema contract is now machine-enforced end-to-end (A1 + A2 + B-slot all required-with-fixed-form-of-fallback). The derive script in PR-B can rely on every brand's tokens.css containing every shared slot name. Co-authored-by: Cursor <cursoragent@cursor.com> * test(e2e): skip leading-underscore meta-directories under design-systems/ CI for #1231 went red on `Validate workspace` after merging origin/main. Cause is a clean collision between two recently-landed changes: - main #1270 (be77dc03"Default English resource i18n fallback") tightened tests/localized-content.test.ts so every directory under design-systems/ is run through assertResourceId() with the strict RESOURCE_ID_PATTERN /^[a-z0-9][a-z0-9-]*$/. - this branch #1231 introduced design-systems/_schema/ as the home of the shared token contract (tokens.schema.ts, defaults.css, AGENTS.md). The leading underscore signals "meta-directory, not brand" — the same convention SCSS partials, Jekyll, Hugo all use. The two changes never met until CI built the merge commit, where assertResourceId('_schema') deterministically failed: Error: Design system directory _schema has malformed resource id: _schema at invariant tests/localized-content.test.ts:66:11 at assertResourceId tests/localized-content.test.ts:71:3 at readDesignSystemResources tests/localized-content.test.ts:202:8 Fix tightens readDesignSystemResources's directory filter so the leading-underscore convention is recognised explicitly: .filter((entry) => entry.isDirectory() && !entry.name.startsWith('_')) This aligns with what apps/daemon/src/design-systems.ts:listDesignSystems already does implicitly — it requires DESIGN.md per directory, so _schema/ was always invisible at runtime; the test was the only place that surfaced it. Verified locally on the post-merge tree: - pnpm test (e2e vitest) — tests/localized-content.test.ts: 4 passed - pnpm guard — all 6 design-system checks pass on default + kami - pnpm typecheck — clean across the workspace (after pnpm install to pull deps for tools/pr that arrived with main) The fix is intentionally narrow (one filter line in one test) and documents the convention inline so future meta-directories under design-systems/ (e.g. _archive/, _drafts/) are covered for free. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: chaoxiaoche <chaoxiaoche@192.168.10.16> Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
11 KiB
CSS
201 lines
11 KiB
CSS
/* ─────────────────────────────────────────────────────────────────────────
|
||
* design-systems/default/tokens.css
|
||
*
|
||
* Structured token bindings for "Neutral Modern" — the canonical
|
||
* starter design system. This file is the *machine-readable* form of
|
||
* the values described in `DESIGN.md`. Agents are expected to paste
|
||
* the `:root { … }` block verbatim into the first `<style>` of every
|
||
* artifact they generate against this design system, then reference
|
||
* tokens via `var(--name)` from then on.
|
||
*
|
||
* Why this file exists:
|
||
* DESIGN.md gives humans context ("Accent #2F6FEB — primary CTAs"),
|
||
* but agents have to translate prose names like "Accent" into the
|
||
* standard token names the lint enforces (`--accent`). That
|
||
* translation is where token misuse happens. This file pre-translates
|
||
* the brand once, so agents copy structure instead of inventing it.
|
||
*
|
||
* Contract sources:
|
||
* - Standard token names: craft/color.md
|
||
* (--bg / --surface / --fg / --muted / --border / --accent
|
||
* / --success / --warn / --danger)
|
||
* - Display-face contract: craft/anti-ai-slop.md
|
||
* (--font-display, must be referenced via var())
|
||
* - Lint enforcement: apps/daemon/src/lint-artifact.ts
|
||
* (raw-hex >12 outside :root → P1; indigo laundering → P0)
|
||
*
|
||
* Schema notes (the shared schema grew from kami's stress test —
|
||
* see design-systems/kami/tokens.css for the rich form, and the
|
||
* #Gap N tags below for what each addition resolves):
|
||
* #Gap 1 — 4-level foreground ramp (fg / fg-2 / muted / meta)
|
||
* #Gap 2 — 3-level surface (bg / surface / surface-warm)
|
||
* #Gap 3 — 2-level border (border / border-soft)
|
||
* #Gap 4 — accent-hover binds to value, not formula
|
||
* #Gap 5 — ring elevation as a first-class level (--elev-ring)
|
||
*
|
||
* Default doesn't differentiate every level kami needs, so the
|
||
* shallow tokens (`--fg-2`, `--meta`, `--surface-warm`, `--border-soft`)
|
||
* collapse to their richer siblings via `var()`. The names still
|
||
* exist so components can reference them uniformly across brands.
|
||
*
|
||
* Keep this file additive: never invent token names not also documented
|
||
* in DESIGN.md or the craft contracts. New brands cloning this template
|
||
* should overwrite values, not rename keys.
|
||
* ─────────────────────────────────────────────────────────────────── */
|
||
|
||
:root {
|
||
/* ─── Surface (3 levels — #Gap 2) ─────────────────────────────────
|
||
* Per craft/color.md: never pure black, never pure white. Default
|
||
* uses #FAFAFA for bg and #FFFFFF for surface — the cream-to-white
|
||
* contrast gives cards lift without a shadow. `--surface-warm` is a
|
||
* schema slot for brands that need a tertiary tier (kami's warm-sand
|
||
* button bg); default has no third tier and aliases it to surface. */
|
||
--bg: #fafafa;
|
||
--surface: #ffffff;
|
||
--surface-warm: var(--surface); /* alias — default has no warm tier */
|
||
|
||
/* ─── Foreground ramp (4 levels — #Gap 1) ───────────────────────
|
||
* Default differentiates only two text levels (primary + muted).
|
||
* `--fg-2` and `--meta` are schema slots for brands with richer
|
||
* ramps (kami uses near-black / dark-warm / olive / stone). They
|
||
* alias here so components targeting the full ramp resolve. */
|
||
--fg: #111111;
|
||
--fg-2: var(--fg); /* alias — default has no secondary tier */
|
||
--muted: #6b6b6b;
|
||
--meta: var(--muted); /* alias — default has no metadata tier */
|
||
|
||
/* ─── Border (2 levels — #Gap 3) ────────────────────────────────
|
||
* Default has one border weight; `--border-soft` is a schema slot
|
||
* for brands with row-separator vs card-edge differentiation. */
|
||
--border: #e5e5e5;
|
||
--border-soft: var(--border); /* alias — default has no soft tier */
|
||
|
||
/* ─── Accent ──────────────────────────────────────────────────────
|
||
* Cobalt — primary CTAs, links, ONE hero element per screen.
|
||
* Hard cap of 2 visible uses per screen is enforced by lint
|
||
* (`accent-overuse` P1 fires at >6 inline occurrences). */
|
||
--accent: #2f6feb;
|
||
--accent-on: #ffffff; /* fg when accent is the bg (e.g. button label) */
|
||
|
||
/* ─── Accent states (#Gap 4) ────────────────────────────────────
|
||
* Hover and active variants for any element using --accent as bg.
|
||
* Default's mid-luminance cobalt admits a black-mix formula
|
||
* cleanly; that's a brand-specific binding, NOT a schema rule.
|
||
* kami binds --accent-hover to var(--accent) (no color shift,
|
||
* hover via elevation) because ink-blue is too dark for further
|
||
* darkening to read; other brands with light accents must
|
||
* hand-pick.
|
||
*
|
||
* Schema rule: every brand provides --accent-hover and
|
||
* --accent-active. The binding strategy (formula / identity /
|
||
* hand-picked) is brand-decided.
|
||
*
|
||
* Why these two are tokens (and other tints stay inline):
|
||
* - cross-component (button, chip, tab, dropdown all need them)
|
||
* - cross-mode reversal (dark-mode hover should mix white, not
|
||
* black — token re-binding is one line per mode)
|
||
* - cross-brand customization (formula breaks on light accents)
|
||
* - lint-enforceable contract (every brand must provide these)
|
||
* Other inline `color-mix(...)` calls in components don't yet hit
|
||
* any of the above — promote them to tokens when a second use
|
||
* appears. */
|
||
--accent-hover: color-mix(in oklab, var(--accent), black 8%);
|
||
--accent-active: color-mix(in oklab, var(--accent), black 14%);
|
||
|
||
/* ─── Semantic ────────────────────────────────────────────────────
|
||
* Reserved for state, not decoration. Keep total semantic-color
|
||
* pixels under 5% of the surface. */
|
||
--success: #17a34a;
|
||
--warn: #eab308;
|
||
--danger: #dc2626;
|
||
|
||
/* ─── Typography ──────────────────────────────────────────────────
|
||
* Inter for display is the documented "modern minimal" override
|
||
* to anti-ai-slop's serif-display rule (see DESIGN.md
|
||
* §Visual Theme & Atmosphere — "Calm, functional, quietly
|
||
* confident"). Other brands should rebind --font-display to a
|
||
* serif unless their direction is also tech/utility. */
|
||
--font-display: "Inter", -apple-system, system-ui, sans-serif;
|
||
--font-body: "Inter", -apple-system, system-ui, sans-serif;
|
||
--font-mono: ui-monospace, "JetBrains Mono", monospace;
|
||
|
||
/* Type scale (px) — direct copy of DESIGN.md §Typography Rules */
|
||
--text-xs: 12px;
|
||
--text-sm: 14px;
|
||
--text-base: 16px;
|
||
--text-lg: 20px;
|
||
--text-xl: 24px;
|
||
--text-2xl: 32px;
|
||
--text-3xl: 48px;
|
||
--text-4xl: 64px;
|
||
|
||
--leading-body: 1.5;
|
||
--leading-tight: 1.2;
|
||
--tracking-display: -0.01em; /* applied to display sizes ≥32px */
|
||
|
||
/* ─── Spacing ─────────────────────────────────────────────────────
|
||
* 4px base unit. Section rhythm (80/48/32) lives below as named
|
||
* tokens because DESIGN.md §Layout Principles treats them as
|
||
* breakpoint-specific decisions, not generic spacing. */
|
||
--space-1: 4px;
|
||
--space-2: 8px;
|
||
--space-3: 12px;
|
||
--space-4: 16px;
|
||
--space-5: 20px;
|
||
--space-6: 24px;
|
||
--space-8: 32px;
|
||
--space-12: 48px;
|
||
--space-20: 80px;
|
||
|
||
--section-y-desktop: 80px;
|
||
--section-y-tablet: 48px;
|
||
--section-y-phone: 32px;
|
||
|
||
/* ─── Radius ──────────────────────────────────────────────────────
|
||
* Two intents: small (button/input) and medium (card/modal).
|
||
* `--radius-pill` reserved for chips/avatars; do not use it on
|
||
* cards or buttons. */
|
||
--radius-sm: 8px;
|
||
--radius-md: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-pill: 9999px;
|
||
|
||
/* ─── Elevation (3 levels — #Gap 5) ─────────────────────────────
|
||
* Default uses two levels (flat + raised blur shadow) per its own
|
||
* DESIGN.md §Depth & Elevation. The schema gains `--elev-ring` as
|
||
* a first-class level so brands using ring shadows as primary
|
||
* elevation (kami, paper, editorial) don't need to rebind
|
||
* --elev-raised away from blur. Default declares all three; ring
|
||
* is available for hairline edges where a 1px border would shift
|
||
* layout. No fourth level — that's neumorphism territory. */
|
||
--elev-flat: none;
|
||
--elev-ring: 0 0 0 1px var(--border);
|
||
--elev-raised: 0 2px 8px color-mix(in oklab, var(--fg), transparent 92%);
|
||
|
||
/* ─── Focus ring ──────────────────────────────────────────────────
|
||
* Single source of truth for keyboard-focus indicators. Every
|
||
* `:focus-visible` rule on buttons, inputs, links, and tabs must
|
||
* use this token — uniform behavior is itself a brand signal, and
|
||
* craft/accessibility-baseline.md treats focus visibility as a
|
||
* non-negotiable. Implemented as a `box-shadow` so it layers
|
||
* outside the element without affecting layout. */
|
||
--focus-ring: 0 0 0 3px color-mix(in oklab, var(--accent), transparent 70%);
|
||
|
||
/* ─── Motion ──────────────────────────────────────────────────────
|
||
* Two durations + one easing curve, per anti-ai-slop's "short,
|
||
* purposeful transitions (150–250ms) with stable easing". Add a
|
||
* third duration only when a real interaction needs it; do not
|
||
* invent `--motion-slow` speculatively. */
|
||
--motion-fast: 150ms; /* hover, focus, micro-states */
|
||
--motion-base: 200ms; /* general state changes */
|
||
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||
|
||
/* ─── Layout ──────────────────────────────────────────────────────
|
||
* Container width and per-breakpoint gutter. Skill-side responsive
|
||
* code reads these to decide grid columns. */
|
||
--container-max: 1200px;
|
||
--container-gutter-desktop: 24px;
|
||
--container-gutter-tablet: 16px;
|
||
--container-gutter-phone: 12px;
|
||
}
|