Skip to content

feat(design-system): make the design system the default source for all UI - #3

Merged
EliRobinson merged 4 commits into
mainfrom
docs/design-system-first
Aug 11, 2026
Merged

EliRobinson merged 4 commits into
mainfrom
docs/design-system-first

Conversation

@EliRobinson

@EliRobinson EliRobinson commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Makes the design system the primary source of components, tokens, and design patterns for anything built on this template — reinforced across every surface an AI agent or human might read, and enforced by lint and E2E rather than only asked for in prose.

Nothing in this template hardcodes the design system's contents, so bumping the package version is the only maintenance it ever needs.

The mechanism

Docs that list components rot on every design system release. So instead of a list, there's a command — pnpm ds, which reads the installed packages at run time:

pnpm ds                  # components (+ exports & variants), hooks, typography classes, token groups
pnpm ds props Dialog     # props, variant unions, and the exact import line to copy
pnpm ds tokens accent    # tokens filtered by name or value
pnpm ds contracts        # the rules UI must satisfy, and what verifies each
pnpm exec ds-resync      # what's out of date, and what changed while you were away

Every doc points at this command rather than repeating its output. AGENTS.md says it explicitly: never paste or trust a component inventory — it is wrong as of the next release.

The tooling ships from upstream, not from here

The final commit on this branch deletes the hand-built versions of that tooling in favour of the published packages, which are versioned with the code they describe. Each local copy drifted independently, and nothing signalled when it had.

Deleted Replaced by
scripts/design-system.mjs (333 lines) elirobinson-ds, a bin on @elirobinson/ai-patterns
The @theme inline block in src/app/globals.css (~30 aliases) @import '@elirobinson/tokens/tailwind.css'
FOREIGN_UI_LIBRARIES / DESIGN_SYSTEM_BARRELS / DIRECT_PRIMITIVES in eslint.config.mjs @elirobinson/eslint-config
The hand-written Claude skill, Cursor rule, Copilot instructions, and AGENTS.md section pnpm ds init --agents
Most of docs/design-system.md (154 → 100 lines) pnpm ds

The upstream CLI reads a generated manifest.json instead of regex-parsing .d.ts, and is layout-agnostic. The ESLint config adds no-hardcoded-design-values, which catches hex / rgb() / oklch() literals and magic px for radius, shadow and duration in className strings, cn() calls and style objects.

Package versions: react@1.3.0, tokens@0.3.0, ai-patterns@0.5.0, eslint-config@0.2.0, plus axe-core.

Coverage

Surface Role
AGENTS.md / CLAUDE.md Managed "UI: design system first" block leads the file; repo-specific rules and "Do Not" entries around it
docs/design-system.md The four places the design system touches this repo — stylesheets, Tailwind bridge, lint, contract checks
.claude/skills/design-system/SKILL.md Claude Code skill scoped to fire on any UI-shaped task
.cursor/rules/design-system.mdc Cursor rule, auto-attached to .tsx/.css
.github/copilot-instructions.md GitHub Copilot / Copilot Workspace
README.md Callout above the fold, stack table rows, usage section, agent-surface map

The four agent-instruction files are generated by pnpm ds init --agents, so a version bump refreshes them too.

Enforcement, not just instruction

  • Lint@elirobinson/eslint-config blocks foreign UI libraries, direct Radix imports outside src/components/ui/, bare @elirobinson/* imports, and hardcoded design values.
  • Tailwind bridge@elirobinson/tokens/tailwind.css maps the Tailwind namespaces onto the tokens. This fixed a live bug on the way in: text-muted-foreground previously resolved to nothing, and now compiles to var(--fg-3). Dark mode works via [data-theme="dark"] with no extra code.
  • E2E — new e2e/design-system.spec.mts runs expectDesignSystemContracts (touch targets, visible focus, WCAG AA contrast) in both light and dark themes. The existing e2e CI job picks it up with no workflow change.
  • src/app/page.tsx — the template's one page models correct usage (Eyebrow, .t-h1/.t-lead/.t-caption), since agents pattern-match on existing code.

Two things worth a reviewer's attention

The contract spec is .mts, not .ts. The helper is ESM-only, and Playwright compiles a plain .ts spec to CJS, where the subpath fails with ERR_PACKAGE_PATH_NOT_EXPORTED. Adding a require condition to that export map upstream would let the .ts form work.

The next/font variables moved from <body> to <html>. Dropping the @theme block also dropped --font-sans: var(--font-geist-sans), and the token's literal 'Geist' never matches next/font's generated family name — so the page silently rendered in system-ui. The family tokens are now repointed from an unlayered :root (tokens.css declares them unlayered, so anything in @layer base loses), which requires --font-geist-sans to exist at :root. Geist now carries through tokens.css's own body rule, the .t-* classes, and Tailwind's font-sans — previously only body had it.

Verification

  • pnpm build, pnpm lint, pnpm type-check, pnpm format:check, pnpm test all clean. The new lint rule was probed with a throwaway file: it caught a hex color, a rounded-[7px], and a raw boxShadow.
  • pnpm test:e2e --project=chromium against pnpm build && pnpm start: 4/4 passed, including both contract tests.
  • In-browser: bg-background resolves to var(--bg), fonts resolve to Geist, "Geist Fallback", data-theme="dark" flips the page, no console errors.

🤖 Generated with Claude Code

EliRobinson and others added 4 commits August 10, 2026 14:12
…l ui

Establish @elirobinson/react and @elirobinson/tokens as the primary source
of components, tokens, and design patterns for anything built on this
template, across every surface an AI agent or human might read.

Nothing here hardcodes the design system's contents, so bumping the package
version is the only maintenance this template needs:

- add `pnpm ds`, a live inventory read from node_modules at run time
  (components with exports and variants, hooks, typography classes, tokens,
  CSS classes). Docs point at the command instead of repeating its output.
- lead AGENTS.md with the design system rule; drop the hardcoded component
  inventory that would go stale on every release.
- add docs/design-system.md as the deep guide (discovery, token usage, dark
  mode, escalation path when something is missing).
- add a Claude Code skill, a Cursor rule, and Copilot instructions so the
  same rule applies whichever tool is driving.
- block foreign UI libraries and direct Radix imports via ESLint
  no-restricted-imports; shadcn output in src/components/ui/ is exempt.
- alias the Tailwind color layer onto design system tokens in globals.css,
  replacing an unrelated hardcoded palette. This fixes text-muted-foreground
  resolving to nothing, and makes dark mode work via [data-theme="dark"]
  with no extra code.
- use design system typography on the home page so the template models
  correct usage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lower-case subject rule rejected legitimate acronyms and proper nouns
(UI, API, TypeScript). Disable subject-case rather than inherit
config-conventional's default, which also bans sentence-case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o 1.1.0

Pull the third design system package into the same treatment tokens and
react already get, and move to the react version its contracts describe.

@elirobinson/ai-patterns ships the design system's expectations for how UI
gets built — uiContracts (touch target, visible focus, WCAG AA), named
componentConstraints each with a machine-checkable `check`, working
patterns, and reusable prompt templates. It is a devDependency; nothing
reaches the browser.

- add `pnpm ds contracts`, `pnpm ds patterns`, and `pnpm ds prompts [name]`,
  and surface the package's version in the inventory header.
- upgrade @elirobinson/react 0.4.0 -> 1.1.0. The contracts describe tiered
  subpath imports (components/<tier>/<Name>), which 0.4.0 predates; 1.1.0
  also grows the inventory from 24 flat components to 45 across
  atoms/molecules/organisms.
- make component discovery layout-agnostic: `pnpm ds` walks the package
  tree rather than assuming a flat directory, so the 0.4 -> 1.1
  reorganisation needed no change to the script or to any doc. `pnpm ds
  props` accepts a bare name or a full subpath and prints the specifier.
- extend the ESLint import ban with the `no-barrel-imports` contract:
  bare @elirobinson/* specifiers are blocked, since no barrel file exists
  and the import would not resolve.
- cover ai-patterns, the tier layout and the contracts across AGENTS.md,
  docs/design-system.md, README, the Claude Code skill, the Cursor rule,
  the Copilot instructions and .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d packages

Every piece of design system tooling this repo built by hand now ships from
EliRobinson/design-system, versioned with the code it describes. Each local
copy drifted independently and nothing signalled when it had.

- scripts/design-system.mjs (333 lines) -> elirobinson-ds, a bin on
  @elirobinson/ai-patterns. It reads a generated manifest instead of
  regex-parsing .d.ts, and is layout-agnostic.
- The @theme inline block in globals.css -> @elirobinson/tokens/tailwind.css.
- The import bans in eslint.config.mjs -> @elirobinson/eslint-config, which
  also adds no-hardcoded-design-values.
- The four agent-instruction files -> pnpm ds init --agents, with AGENTS.md
  now carrying a managed block.
- Most of docs/design-system.md -> pnpm ds. What is left covers only the four
  places the design system touches this repo.

Adds e2e/design-system.spec.mts, which runs the contracts a linter cannot
settle (touch targets, visible focus, WCAG AA contrast) in both themes. It is
.mts because the helper is ESM-only and Playwright compiles a plain .ts spec
to CJS, where the subpath does not resolve.

Moves the next/font variables from <body> to <html>. Dropping the @theme block
also dropped --font-sans: var(--font-geist-sans), and the token's literal
'Geist' never matches next/font's generated family name, so the page silently
rendered in system-ui. The family tokens are now repointed from an unlayered
:root — tokens.css declares them unlayered, so anything in @layer base loses —
which carries Geist through tokens.css's own body rule, the .t-* classes, and
Tailwind's font-sans.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@EliRobinson
EliRobinson merged commit 6f2af1e into main Aug 11, 2026
3 checks passed
@EliRobinson
EliRobinson deleted the docs/design-system-first branch August 11, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant