{
  "source": "AGENTS.md",
  "guide": "# Design system guide for AI agents\n\nHow to build UI with the design system behind the AI Engineering Lab (`/lab`).\n\n## Where things live\n\n| What | Where |\n| --- | --- |\n| Design tokens (source of truth, W3C format) | `tokens/lab.tokens.json` |\n| Generated tokens (CSS, SwiftUI, Compose). Do not edit | `tokens/generated/` |\n| Component specs, one per component | `specs/*.spec.json` |\n| Spec schema (JSON Schema and TypeScript type) | `specs/schema/` |\n| Web components and their style recipes | `components/lab/demo-components.tsx`, `RECIPES` in `lib/lab/playground.ts` |\n| Code templates for React, Angular, Vue, Swift, Kotlin | `lib/lab/templates/`, filled with tokens by `lib/lab/stacks.ts` |\n| Shared web CSS for the templates (`.ds-button`, `.ds-card`, `.ds-input`) | `lib/lab/component-css.ts` |\n| Registry core (read-only, used by `/r`) | `lib/registry/` |\n\nBefore using a component, read its spec: `whenToUse` and `whenNotToUse` say whether it is the right one, `props` and `variants` say what exists, `composition` says what goes inside it.\n\n## Reading the system over HTTP\n\nThe same tokens, specs and templates are published as static, read-only JSON (CORS open), built by `lib/registry/`:\n\n| Route | Returns |\n| --- | --- |\n| `/r/index.json` | Components, their variants and sizes, and the supported stacks |\n| `/r/tokens.json` | The full token set, with CSS, Swift and Kotlin names |\n| `/r/rules.json` | These rules plus each spec's `accessibility` and `whenNotToUse` |\n| `/r/<component>.json?stack=<stack>` | One component's spec, resolved tokens and code. Stacks: `react`, `angular`, `vue`, `swift`, `kotlin` |\n\nAn unknown component or stack returns `{ \"error\": { \"code\", \"message\", \"received\", \"valid\" } }`.\n\n## Rules\n\n1. **Always use tokens, never hardcode values.** Colours, radii, spacing and font sizes come from `tokens/lab.tokens.json`. In specs, refer to a token by its path (`color.primary`). In code, use the generated names: `var(--ds-color-primary)` on the web, `Tokens.colorPrimary` in Swift and Kotlin. If no token fits, add one to `tokens/lab.tokens.json` first; do not inline the value.\n2. **Follow the `accessibility` section of the spec.** It defines the role, keyboard behaviour, focus visibility, minimum contrast and required labels. Never remove a focus style without a visible replacement. Never use a placeholder as a label.\n3. **Use only what the spec describes.** No props, variants, sizes or states that are not in the spec. If you need one, change the spec first.\n\n## Adding a component\n\n1. **Spec first.** Create `specs/<name>.spec.json` with `\"$schema\": \"./schema/component-spec.schema.json\"`. Reference tokens by path only.\n2. **Tokens.** Reuse existing token paths. If you add tokens, run `pnpm tokens:build` and `pnpm tokens:check`.\n3. **Code.** Implement it in `components/lab/demo-components.tsx`, with its styles in `RECIPES.<name>` in `lib/lab/playground.ts`. Variants, sizes and prop defaults must match the spec. For the stack switcher and the registry, add its CSS rules to `lib/lab/component-css.ts` and a template per stack in `lib/lab/templates/`.\n4. **Validate.** Run `pnpm specs:validate` and `npx tsc --noEmit`. The validator checks the schema, that every token path exists, and that variants, sizes and defaults match the code. Fix whichever side is wrong; do not loosen the check.\n\n## Commands\n\n```bash\npnpm specs:validate   # specs vs schema, tokens and code\npnpm tokens:build     # tokens/lab.tokens.json → tokens/generated/\npnpm tokens:check     # the lab's live token files match Style Dictionary\nnpx tsc --noEmit      # type check\npnpm test             # registry core and /r routes\n```\n\n## Known gaps\n\nDo not copy these; fix them when you touch the component.\n\n- No tokens yet for the primary hover shade, the page canvas (used as the focus ring offset) or control heights. The web recipes compute or hardcode them. `color.muted` now exists, but the playground recipes still read their own `--ui-muted`.\n- Button has no loading state; Input has no error state.\n- Contrast below what the specs require: primary Button label 4.36:1 and ghost Button text 4.32:1 (dark) and 3.88:1 (light), against 4.5:1; Input outline border about 1.3:1, against 3:1.\n",
  "rules": [
    {
      "id": "rule-1",
      "title": "Always use tokens, never hardcode values.",
      "text": "Always use tokens, never hardcode values. Colours, radii, spacing and font sizes come from tokens/lab.tokens.json. In specs, refer to a token by its path (color.primary). In code, use the generated names: var(--ds-color-primary) on the web, Tokens.colorPrimary in Swift and Kotlin. If no token fits, add one to tokens/lab.tokens.json first; do not inline the value."
    },
    {
      "id": "rule-2",
      "title": "Follow the accessibility section of the spec.",
      "text": "Follow the accessibility section of the spec. It defines the role, keyboard behaviour, focus visibility, minimum contrast and required labels. Never remove a focus style without a visible replacement. Never use a placeholder as a label."
    },
    {
      "id": "rule-3",
      "title": "Use only what the spec describes.",
      "text": "Use only what the spec describes. No props, variants, sizes or states that are not in the spec. If you need one, change the spec first."
    }
  ],
  "components": {
    "button": {
      "accessibility": {
        "role": "button, from the native <button> element.",
        "keyboard": [
          "Tab moves focus to it; Enter and Space activate it (native behaviour).",
          "When disabled it is skipped by Tab and does not fire onClick."
        ],
        "focus": "Keep the focus-visible ring (2px color.primary, 2px offset). Never remove it without a visible replacement.",
        "contrast": "Label text at least 4.5:1 against the button background (WCAG 2.2 AA, 1.4.3). Focus ring at least 3:1 against the page (1.4.11).",
        "labels": "Needs a visible text label as children. Icon-only buttons need an aria-label."
      },
      "whenNotToUse": [
        "Navigating to another page or URL: use a link (<a>).",
        "Turning a setting on or off: use a switch or a checkbox.",
        "Showing progress while an action runs: Button has no loading state yet."
      ]
    },
    "card": {
      "accessibility": {
        "role": "article, from the native <article> element. The Card itself is not interactive.",
        "keyboard": [
          "The Card is not focusable; Tab reaches the links or buttons inside it.",
          "When disabled, also take inner actions out of the tab order (tabIndex={-1}): aria-disabled and pointer-events do not do that."
        ],
        "focus": "The action (cardParts.action) shows a 2px color.primary ring on focus-visible. Inner Buttons keep their own ring.",
        "contrast": "Title and body text at least 4.5:1 against the card background in every variant (WCAG 2.2 AA, 1.4.3).",
        "labels": "Start the content with a heading (cardParts.title) so the article can be found by heading navigation."
      },
      "whenNotToUse": [
        "As a clickable surface: Card has no button role or keyboard handling. Put a link or a Button inside it instead.",
        "For page layout or whole sections: use plain layout elements."
      ]
    },
    "input": {
      "accessibility": {
        "role": "textbox for text-like types, from the native <input> element.",
        "keyboard": [
          "Tab moves focus to it; typing and editing are native.",
          "No custom key handling."
        ],
        "focus": "Keep the focus-visible style (color.primary border and ring). Never remove it without a visible replacement.",
        "contrast": "Typed text, label and hint at least 4.5:1 (WCAG 2.2 AA, 1.4.3). The field boundary at least 3:1 against its surroundings (1.4.11).",
        "labels": "label is required and rendered as a visible <label for>. Do not use the placeholder as the label. The hint is announced through aria-describedby."
      },
      "whenNotToUse": [
        "Multi-line text: use a textarea.",
        "Choosing from a fixed list: use a select or a radio group.",
        "Showing a validation error: Input has no error state yet."
      ]
    }
  }
}
