{
  "$schema": "https://loui.dev/schemas/manifest.schema.json",
  "schemaVersion": 1,
  "package": {
    "name": "loui-ds",
    "version": "0.7.4"
  },
  "generator": "scripts/manifest/build.ts",
  "entryPoints": [
    {
      "path": ".",
      "importPath": "loui-ds",
      "description": "Core components, hooks, i18n and utilities."
    },
    {
      "path": "./patterns",
      "importPath": "loui-ds/patterns",
      "description": "Domain patterns (generic names). Empty until patterns land."
    },
    {
      "path": "./styles.css",
      "importPath": "loui-ds/styles.css",
      "description": "Layered CSS: tokens (--ds-*), base and every component."
    }
  ],
  "counts": {
    "components": 120,
    "hooks": 11,
    "utilities": 46,
    "icons": 40,
    "recipes": 13
  },
  "components": [
    {
      "name": "Accordion",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Accordion } from 'loui-ds';",
      "folder": "src/components/Accordion",
      "sourceFile": "src/components/Accordion/Accordion.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A stack of sections that expand and collapse. Each header is a heading that wraps a real\nbutton (`aria-expanded`, `aria-controls`), so it appears in the page outline and works with\n`Enter` and `Space`. Built on React Aria `DisclosureGroup`.\n\nUse it to shorten a long page of related, optional content (FAQs, settings groups). Do not\nhide content people need to finish their task.",
      "props": [
        {
          "name": "items",
          "type": "readonly AccordionItem[]",
          "required": true,
          "description": "The sections, in order."
        },
        {
          "name": "multiple",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Allow several sections open at once. By default opening one section closes the others\n(single expansion)."
        },
        {
          "name": "value",
          "type": "readonly string[]",
          "required": false,
          "description": "Values of the expanded items (controlled)."
        },
        {
          "name": "defaultValue",
          "type": "readonly string[]",
          "required": false,
          "default": "[]",
          "description": "Values of the items expanded at first (uncontrolled)."
        },
        {
          "name": "onChange",
          "type": "(value: string[]) => void",
          "required": false,
          "description": "Called with the values of all expanded items after the user toggles one."
        },
        {
          "name": "variant",
          "type": "'bordered' | 'plain'",
          "required": false,
          "default": "'bordered'",
          "description": "`bordered` (default) draws a card around the sections; `plain` only draws dividers.",
          "values": ["bordered", "plain"]
        },
        {
          "name": "headingLevel",
          "type": "2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "3",
          "description": "Level of the heading that wraps each trigger, `3` by default. Use the level that follows\nthe heading above the accordion in the page outline.",
          "values": ["2", "3", "4", "5", "6"]
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Disables every section."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the root element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "variant": ["bordered", "plain"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accordion-padding-inline",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-icon-sm",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-subtle",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-row-h",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Accordion",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-accordion--default",
            "hasPlay": true
          },
          {
            "name": "Plain",
            "id": "components-accordion--plain",
            "hasPlay": false
          },
          {
            "name": "InitiallyExpanded",
            "id": "components-accordion--initially-expanded",
            "hasPlay": true
          },
          {
            "name": "KeyboardSingle",
            "id": "components-accordion--keyboard-single",
            "description": "Enter and Space toggle the focused header; opening one closes the others.",
            "hasPlay": true
          },
          {
            "name": "Multiple",
            "id": "components-accordion--multiple",
            "hasPlay": true
          },
          {
            "name": "DisabledItem",
            "id": "components-accordion--disabled-item",
            "hasPlay": true
          },
          {
            "name": "HeadingLevel2",
            "id": "components-accordion--heading-level2",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-accordion--controlled",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-accordion--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-accordion--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-accordion--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Accordion/Accordion.md",
        "summary": "A stack of sections that expand and collapse. Each header is a heading that wraps a real button. Built on React Aria `DisclosureGroup`, `Disclosure` and `DisclosurePanel`.\n\n- Handoff source: `components/layout/Accordion.jsx` (TODO `CMP-LAY-04`).",
        "whenToUse": [
          "FAQs, grouped settings, long optional detail on a record.",
          "To shorten a page whose sections people read one at a time."
        ],
        "whenNotToUse": [
          "For content people must see to finish the task.",
          "For switching between views: use `Tabs`.",
          "For a single show/hide of one block: a plain disclosure is enough."
        ],
        "keyboard": [
          "Keys: `Tab` moves between triggers; `Enter` and `Space` toggle. Optional arrow-key movement between headers (APG) is not added: each trigger is a tab stop."
        ],
        "examples": [
          "<Accordion\n  headingLevel={2}\n  items={[\n    { value: 'shipping', title: 'How long does shipping take?', content: 'Two to five days.' },\n    { value: 'returns', title: 'Can I return an order?', meta: '30 days', content: '…' },\n  ]}\n/>\n\n<Accordion multiple variant=\"plain\" value={open} onChange={setOpen} items={items} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- FAQs, grouped settings, long optional detail on a record.\n- To shorten a page whose sections people read one at a time."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For content people must see to finish the task.\n- For switching between views: use `Tabs`.\n- For a single show/hide of one block: a plain disclosure is enough."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                     | Type                        | Default      | Notes                                                                   |\n| ------------------------ | --------------------------- | ------------ | ----------------------------------------------------------------------- |\n| `items`                  | `AccordionItem[]`           | required     | `{ value, title, meta?, content, isDisabled? }`.                        |\n| `multiple`               | `boolean`                   | `false`      | Several sections open at once. Otherwise opening one closes the others. |\n| `value` / `defaultValue` | `string[]`                  | `[]`         | Expanded values, controlled / uncontrolled.                             |\n| `onChange`               | `(value: string[]) => void` | none         | All expanded values after a toggle.                                     |\n| `variant`                | `'bordered' \\| 'plain'`     | `'bordered'` | `bordered`: card border and surface. `plain`: dividers only.            |\n| `headingLevel`           | `2`-`6`                     | `3`          | Level of the heading around each trigger; follow the page outline.      |\n| `isDisabled`             | `boolean`                   | `false`      | Disables every section.                                                 |\n\n`ref` points to the root; `className` and other attributes pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Each header is `h3` (configurable) containing a `button` with `aria-expanded` and `aria-controls`; the panel is a `group` labelled by the button. Headings appear in the page outline and in screen reader heading lists.\n- Keys: `Tab` moves between triggers; `Enter` and `Space` toggle. Optional arrow-key movement between headers (APG) is not added: each trigger is a tab stop.\n- Collapsed panels are hidden (`hidden=\"until-found\"`), so find-in-page can open them, and they take no space.\n- The chevron rotates 180 degrees to show the state; the state itself is `aria-expanded`, not the icon. Under `prefers-reduced-motion` the rotation is instant.\n- The focus indicator is an inset accent outline because the group clips overflow. Forced colors use `Highlight`.\n- Disabled items are not focusable.\n- Meta text is read after the title; keep it short."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Titles are short phrases or questions in sentence case, and say what is inside.\n- Do not put the only copy of important information in a collapsed panel."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Accordion\n  headingLevel={2}\n  items={[\n    { value: 'shipping', title: 'How long does shipping take?', content: 'Two to five days.' },\n    { value: 'returns', title: 'Can I return an order?', meta: '30 days', content: '…' },\n  ]}\n/>\n\n<Accordion multiple variant=\"plain\" value={open} onChange={setOpen} items={items} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The trigger is wrapped in a heading and linked to its panel (`aria-controls`, labelled panel); the handoff had a bare `button` and no panel relationship.\n- `multiple` and controlled `value` / `onChange` replace the internal-only `defaultValue` array.\n- `variant=\"plain\"` replaces `bordered={false}`. The chevron is the shared `ChevronDownIcon` that rotates (down collapsed, up expanded), instead of a custom right-pointing SVG.\n- Not included yet: height animation of the panel (the chevron animates; the panel appears instantly) and arrow-key movement between headers."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Tabs",
            "reason": "For switching between views: use `Tabs`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": []
      }
    },
    {
      "name": "ActionList",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ActionList } from 'loui-ds';",
      "folder": "src/components/ActionList",
      "sourceFile": "src/components/ActionList/ActionList.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A vertical list of records, each with a title, optional description, a mono meta value and a\n\"more actions\" menu, with optional row checkboxes (`selectionMode=\"multiple\"`). Built on React Aria `GridList`: rows are reached with the arrow keys\n(one tab stop for the list), `Enter` runs `onAction`, and `Tab` or the right arrow moves\nfrom a row to its menu button.",
      "props": [
        {
          "name": "items",
          "type": "readonly ActionListItem[]",
          "required": true,
          "description": "The rows, in the order given. The list never sorts: lists are alphabetical by default in\nthis design system, so sort with `sortByText` before passing them (tasks by urgency and\ndated records newest first are the exceptions)."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the list root."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the list root element."
        },
        {
          "name": "onAction",
          "type": "(item: ActionListItem) => void",
          "required": false,
          "description": "Called when the user activates a row (click, or Enter on the focused row). Makes the whole\nrow a press target with hover and pointer cursor. The menu button does not trigger it."
        },
        {
          "name": "selectionMode",
          "type": "'none' | 'multiple'",
          "required": false,
          "default": "'none'",
          "description": "Adds a checkbox at the start of every row. `'multiple'` toggles rows independently (the\ncheckbox toggles, the row still runs `onAction`). Default `'none'`.",
          "values": ["none", "multiple"]
        },
        {
          "name": "selectedKeys",
          "type": "\"all\" | Iterable<Key>",
          "required": false,
          "description": "Controlled selected row ids. Use together with `onSelectionChange`."
        },
        {
          "name": "defaultSelectedKeys",
          "type": "\"all\" | Iterable<Key>",
          "required": false,
          "description": "Initially selected row ids (uncontrolled)."
        },
        {
          "name": "onSelectionChange",
          "type": "(keys: Selection) => void",
          "required": false,
          "description": "Called when the checkbox of a row is toggled (click, or `Space` on the focused row or\ncheckbox). The list never shows an Undo toast itself: update your state, then call\n`toastWithUndo` (reversible toggles never ask for confirmation)."
        },
        {
          "name": "bordered",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Draws the border, surface and radius around the list. Turn off inside a `Card`. Default `true`."
        },
        {
          "name": "emptyState",
          "type": "ReactNode",
          "required": false,
          "description": "Content when `items` is empty. Default: an `EmptyState` with built-in text."
        },
        {
          "name": "messages",
          "type": "Partial<{ moreActions: (title: string) => string; select: string; done: string; emptyTitle: string; emptyDescription: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the list. Required unless `aria-labelledby` is set."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the list (a visible heading)."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaGridListProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "escapeKeyBehavior",
              "type": "'none' | 'clearSelection'",
              "required": false
            },
            {
              "name": "keyboardNavigationBehavior",
              "type": "'arrow' | 'tab'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "GridListProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean | FocusStrategy",
              "required": false
            },
            {
              "name": "disabledBehavior",
              "type": "'all' | 'selection'",
              "required": false
            },
            {
              "name": "shouldSelectOnPressUp",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "GridListProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "disallowTypeAhead",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ActionList",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "moreActions",
            "kind": "function",
            "default": "More actions for {title}",
            "signature": "(title: string)",
            "description": "Accessible name of the row actions button. `title` is the text of the row (\"More actions for Price list\")."
          },
          {
            "key": "select",
            "kind": "string",
            "default": "Select",
            "description": "Start of the accessible name of a row checkbox. React Aria appends the text of the row\n(\"Select\" + \"Price list 2026\"), so keep it a bare verb."
          },
          {
            "key": "done",
            "kind": "string",
            "default": "Done",
            "description": "Visually hidden text added to a row marked `isDone`, read after the title."
          },
          {
            "key": "emptyTitle",
            "kind": "string",
            "default": "No items found",
            "description": "Title of the default empty state."
          },
          {
            "key": "emptyDescription",
            "kind": "string",
            "default": "Try another filter or search term.",
            "description": "Description of the default empty state."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-line",
        "--ds-line-subtle",
        "--ds-radius-lg",
        "--ds-row-h",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/ActionList",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-action-list--default",
            "description": "Rows are listed in the order given; sort with `sortByText` for the alphabetical default.",
            "hasPlay": true
          },
          {
            "name": "Actionable",
            "id": "components-action-list--actionable",
            "description": "With `onAction` the whole row is a press target; the menu button does not trigger it.",
            "hasPlay": true
          },
          {
            "name": "KeyboardNavigation",
            "id": "components-action-list--keyboard-navigation",
            "description": "The arrow keys, Home and End move between rows; the right arrow reaches the row menu.",
            "hasPlay": true
          },
          {
            "name": "RowMenu",
            "id": "components-action-list--row-menu",
            "description": "Enter opens the row menu, Escape closes it and returns focus to the button.",
            "hasPlay": true
          },
          {
            "name": "Borderless",
            "id": "components-action-list--borderless",
            "hasPlay": false
          },
          {
            "name": "WithLeadingAndLongText",
            "id": "components-action-list--with-leading-and-long-text",
            "hasPlay": false
          },
          {
            "name": "DisabledRow",
            "id": "components-action-list--disabled-row",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-action-list--empty",
            "hasPlay": true
          },
          {
            "name": "CustomEmpty",
            "id": "components-action-list--custom-empty",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-action-list--portuguese-brazil",
            "description": "Row action button names, the empty state and numbers follow the provider locale.",
            "hasPlay": true
          },
          {
            "name": "WithCheckboxes",
            "id": "components-action-list--with-checkboxes",
            "description": "`selectionMode=\"multiple\"` adds a named checkbox to each row; `Space` toggles it.",
            "hasPlay": true
          },
          {
            "name": "CheckboxWithRowActionAndMenu",
            "id": "components-action-list--checkbox-with-row-action-and-menu",
            "description": "Checkbox, row action and menu together: the checkbox toggles, `Enter` or a click runs the action.",
            "hasPlay": true
          },
          {
            "name": "TaskRows",
            "id": "components-action-list--task-rows",
            "description": "Task rows: control on the left, module label and due chip in `meta`, menu on the right.",
            "hasPlay": false
          },
          {
            "name": "DoneRows",
            "id": "components-action-list--done-rows",
            "description": "`isDone` strikes the title through and adds a hidden \"Done\" for screen readers.",
            "hasPlay": true
          },
          {
            "name": "CheckboxesLongText",
            "id": "components-action-list--checkboxes-long-text",
            "hasPlay": false
          },
          {
            "name": "CheckboxesDisabledRow",
            "id": "components-action-list--checkboxes-disabled-row",
            "hasPlay": false
          },
          {
            "name": "ToggleWithUndo",
            "id": "components-action-list--toggle-with-undo",
            "description": "Consumer pattern for reversible toggles: update the state, then `toastWithUndo`. The list never\nasks for confirmation and never toggles silently.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ActionList/ActionList.md",
        "summary": "A vertical list of records. Each row has an optional leading node, a title, an optional description, a mono meta value and an optional \"more actions\" menu. Built on React Aria `GridList`, so rows are reachable with the arrow keys and can run an action.",
        "whenToUse": [
          "Short lists of things with a few actions each: files, saved filters, members, recent items.",
          "One main line of text per record, with secondary detail."
        ],
        "whenNotToUse": [
          "Many comparable columns or sorting: use `DataTable`.",
          "Navigation between pages: use `NavItem` or `Tabs`.",
          "Choosing options in a form: use `RadioGroup`, `CheckboxGroup` or `Select`."
        ],
        "keyboard": [
          "Keyboard: `Tab` enters the list on one row (one tab stop); Arrow Up/Down, Home and End move between rows; Arrow Right moves from the row to its menu button (Arrow Left back); `Enter` on a row runs `onAction`; the menu button follows the Menu contract (`Enter`/`Space`/`Arrow Down` opens, `Escape` closes and focus returns to the button).",
          "With `selectionMode=\"multiple\"`: `Space` on the focused row (or on its checkbox) toggles it, `Enter` and a click on the row content run `onAction`, a click on the checkbox toggles. Each checkbox is named \"Select {title}\" (React Aria composes the message `select` with the row text). `Tab`/`ArrowRight` moves from the row to its checkbox and menu button."
        ],
        "examples": [
          "<ActionList\n  aria-label=\"Files\"\n  items={sortByText(files, (f) => f.name, locale).map((file) => ({\n    id: file.id,\n    title: file.name,\n    description: `Updated by ${file.author}`,\n    meta: formatBytes(file.size),\n    actions: (\n      <>\n        <MenuItem onAction={() => download(file)}>Download file</MenuItem>\n        <MenuSeparator />\n        <MenuItem tone=\"danger\" onAction={() => askToDelete(file)}>\n          Delete file\n        </MenuItem>\n      </>\n    ),\n  }))}\n  onAction={(item) => openFile(item.id)}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Short lists of things with a few actions each: files, saved filters, members, recent items.\n- One main line of text per record, with secondary detail."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Many comparable columns or sorting: use `DataTable`.\n- Navigation between pages: use `NavItem` or `Tabs`.\n- Choosing options in a form: use `RadioGroup`, `CheckboxGroup` or `Select`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=grid]            named by aria-label / aria-labelledby\n  div[role=row]           one tab stop for the whole list; arrow keys move between rows\n    Checkbox  selection   optional (selectionMode=\"multiple\"), named \"Select {title}\"\n    span  leading         optional (Avatar, Icon)\n    span  title / description\n    span  meta            mono, text-2; a node: a module label plus a Badge / DueIndicator\n    MoreActionsMenu       IconButton \"More actions for {title}\" + Menu (optional)\n```\n\nIn a list 480 px wide or narrower (a container query on the list, not the viewport), `meta` moves under the title and description, so a long title keeps its width instead of shrinking to a few characters next to a due chip."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                   | Type                        | Default  | Notes                                                                                           |\n| -------------------------------------- | --------------------------- | -------- | ----------------------------------------------------------------------------------------------- |\n| `items`                                | `ActionListItem[]`          | -        | `id`, `title`, `textValue`, `description`, `meta`, `leading`, `actions`, `isDisabled`, `isDone` |\n| `aria-label`                           | `string`                    | -        | Required (or `aria-labelledby`)                                                                 |\n| `onAction`                             | `(item) => void`            |          | Row click or Enter. Makes the row a press target                                                |\n| `selectionMode`                        | `'none' \\| 'multiple'`      | `'none'` | `'multiple'` adds a checkbox to every row (toggle behavior)                                     |\n| `selectedKeys` / `defaultSelectedKeys` | `'all' \\| Iterable<Key>`    |          | Controlled / initial selected row ids                                                           |\n| `onSelectionChange`                    | `(keys: Selection) => void` |          | Called when a checkbox is toggled. The consumer owns Undo                                       |\n| `bordered`                             | `boolean`                   | `true`   | Border, surface and radius. Turn off inside a `Card`                                            |\n| `emptyState`                           | `ReactNode`                 |          | Default: compact `EmptyState` with built-in text                                                |\n| `messages`                             | `Partial<messages>`         |          | `moreActions(title)`, `select`, `done`, `emptyTitle`, `emptyDescription`                        |\n\n`actions` is a node: `MenuItem`, `MenuSection` and `MenuSeparator` elements from `Menu`. Put the handler on the item (`<MenuItem onAction={...}>`). Destructive items use `tone=\"danger\"` and still ask for confirmation in a dialog.\n\nThe list renders `items` in the order given. Lists are alphabetical by default in this design system: pass them through `sortByText` first (exceptions: tasks by urgency, dated records newest first)."
          },
          {
            "id": "checkbox-rows-and-done-state",
            "heading": "Checkbox rows and done state",
            "content": "```tsx\nconst [done, setDone] = useState<Set<string>>(new Set());\n\n<ActionList\n  aria-label=\"Tasks\"\n  selectionMode=\"multiple\"\n  selectedKeys={done}\n  onSelectionChange={(keys) => {\n    if (keys === 'all') return;\n    const before = done;\n    setDone(new Set([...keys].map(String)));\n    toastWithUndo('Task updated', { onUndo: () => setDone(before) });\n  }}\n  onAction={(task) => openTask(task.id)}\n  items={tasks.map((task) => ({\n    id: task.id,\n    title: task.name,\n    isDone: done.has(task.id),\n    meta: (\n      <>\n        <span>{task.module}</span>\n        <DueIndicator date={task.due} />\n      </>\n    ),\n    actions: <MenuItem onAction={() => edit(task)}>Edit task</MenuItem>,\n  }))}\n/>;\n```\n\n- `isDone` and the checkbox are independent: the list does not derive one from the other. A to-do usually passes `isDone: selected` as above.\n- `isDone` sets the title to `text-3` with a line-through (also in forced colors) and adds a visually hidden \"Done\" after the title (`messages.done`), so the state is never color or decoration alone.\n- Toggling is reversible, so it never asks for confirmation. The list does not toggle silently with undo built in: show `toastWithUndo` from `onSelectionChange` as above."
          },
          {
            "id": "design-note-gap-017-gap-015",
            "heading": "Design note (GAP-017, GAP-015)",
            "content": "Decision: extend `ActionList`; no separate `CheckList`. A checkbox list is the same anatomy (title, description, meta, menu) plus a leading control, and consumers need both the checkbox and the row action, which is what `GridList` models. Only `selectionMode: 'none' | 'multiple'` is exposed with `selectionBehavior=\"toggle\"` fixed; `single` is a different pattern (`RadioGroup`, `ListBox`). The \"control, module label, due chip, actions\" row is built with the existing slots: the checkbox is the selection control, `meta` accepts nodes (module label plus `Badge` or `DueIndicator`), `actions` is the menu. No new slot was needed.\n\nReact Aria turns a row into a pure selection toggle as soon as one row is selected, which would silence `onAction`. With `selectionMode=\"multiple\"` the list therefore handles the row action itself (click on the row content, `Enter` on the focused row), so it works whether or not rows are selected. Only the checkbox and `Space` toggle selection."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `GridList` gives `role=\"grid\"` with `row` and `gridcell` roles; rows are not a `list`/`listitem` pair because they are focusable and actionable.\n- Each selection checkbox is named \"Select {title}\" (pt-BR \"Selecionar {title}\"): the `select` message plus the row's plain-text name, never the meta. A `title` that is not a string needs `textValue`; without it the name falls back to the row `id` and a development warning says so.\n- Keyboard: `Tab` enters the list on one row (one tab stop); Arrow Up/Down, Home and End move between rows; Arrow Right moves from the row to its menu button (Arrow Left back); `Enter` on a row runs `onAction`; the menu button follows the Menu contract (`Enter`/`Space`/`Arrow Down` opens, `Escape` closes and focus returns to the button).\n- With `selectionMode=\"multiple\"`: `Space` on the focused row (or on its checkbox) toggles it, `Enter` and a click on the row content run `onAction`, a click on the checkbox toggles. Each checkbox is named \"Select {title}\" (React Aria composes the message `select` with the row text). `Tab`/`ArrowRight` moves from the row to its checkbox and menu button.\n- Every menu button has its own name (\"More actions for Price list 2026\") so a screen reader user can tell them apart.\n- Row actions never rely on color; the meta value is text. Disabled rows are dimmed and skipped.\n- Known quirk: React Aria moves focus to the row after an overlay inside a row closes. The component hands focus back to the button that was pressed."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Titles are the record name, not a sentence. Descriptions say who or when (\"Updated by Ana\").\n- Menu items are verb + object (\"Download file\", \"Delete file\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<ActionList\n  aria-label=\"Files\"\n  items={sortByText(files, (f) => f.name, locale).map((file) => ({\n    id: file.id,\n    title: file.name,\n    description: `Updated by ${file.author}`,\n    meta: formatBytes(file.size),\n    actions: (\n      <>\n        <MenuItem onAction={() => download(file)}>Download file</MenuItem>\n        <MenuSeparator />\n        <MenuItem tone=\"danger\" onAction={() => askToDelete(file)}>\n          Delete file\n        </MenuItem>\n      </>\n    ),\n  }))}\n  onAction={(item) => openFile(item.id)}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Real list semantics and keyboard support (`GridList`) instead of clickable `div`s; rows can be activated with `Enter`.\n- The `···` button is the shared `MoreActionsMenu` (label from messages, names the row, `Menu` keyboard model) instead of a hand-made popover.\n- `actions` is a `MenuItem` node (composition) instead of an array of `{ label, danger, separator }`; `onItemClick` is `onAction`; `title`/`description`/`meta` accept nodes.\n- Text uses `text-2` for descriptions (contrast stays above 4.5:1 on hover and raised surfaces).\n- Empty state and disabled rows added. Drag handle is not included."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Many comparable columns or sorting: use `DataTable`."
          },
          {
            "name": "NavItem",
            "reason": "Navigation between pages: use `NavItem` or `Tabs`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `NavItem` or `Tabs`."
          },
          {
            "name": "RadioGroup",
            "reason": "Choosing options in a form: use `RadioGroup`, `CheckboxGroup` or `Select`."
          },
          {
            "name": "CheckboxGroup",
            "reason": "Choosing options in a form: use `RadioGroup`, `CheckboxGroup` or `Select`."
          },
          {
            "name": "Select",
            "reason": "Choosing options in a form: use `RadioGroup`, `CheckboxGroup` or `Select`."
          }
        ],
        "uses": ["Checkbox", "EmptyState", "MoreActionsMenu", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "AddCard",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { AddCard } from 'loui-ds';",
      "folder": "src/components/AddCard",
      "sourceFile": "src/components/AddCard/AddCard.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A dashed \"create new\" tile that sits in a grid next to cards. The whole tile is one link\n(`href`) or button (`onPress`) with a centred plus icon and a verb-plus-object label. It\nstretches to the height of its row.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "Label: a short verb plus object in sentence case (\"Create agent\"). It is the accessible name."
        },
        {
          "name": "href",
          "type": "string",
          "required": false,
          "description": "Destination of the create flow. Renders a real `<a>`."
        },
        {
          "name": "target",
          "type": "string",
          "required": false,
          "description": "Link target. `_blank` adds `rel=\"noopener noreferrer\"` and a hidden \"opens in a new tab\" note."
        },
        {
          "name": "rel",
          "type": "string",
          "required": false,
          "description": "Link relationship, merged with `noopener noreferrer` for `_blank`."
        },
        {
          "name": "onPress",
          "type": "((event: PressEvent) => void) | ((event: PressEvent) => void)",
          "required": false,
          "description": "Fired when the link is pressed, before navigation.\nAction run when the tile is pressed (usually opens a dialog). Renders a real `<button type=\"button\">`."
        },
        {
          "name": "ref",
          "type": "((instance: HTMLAnchorElement | null) => void | (() => VoidOrUndefinedOnly)) | RefObject<HTMLAnchorElement | null> | ((instance: HTMLButtonElement | null) => void | (() => VoidOrUndefinedOnly)) | RefObject<...> | null",
          "required": false,
          "description": "Ref to the underlying `<a>` element.\nRef to the underlying `<button>` element."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the tile: it is dimmed, skipped by `Tab` and ignores presses."
        },
        {
          "name": "messages",
          "type": "Partial<{ opensInNewTab: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "AddCard",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "opensInNewTab",
            "kind": "string",
            "default": "(opens in a new tab)"
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-add-card-gap",
        "--ds-add-card-min-block-size",
        "--ds-card-padding",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-fs-body",
        "--ds-lh-body",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/AddCard",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-add-card--default",
            "hasPlay": true
          },
          {
            "name": "AsLink",
            "id": "components-add-card--as-link",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-add-card--disabled",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-add-card--focused",
            "description": "Keyboard focus draws the accent border and ring.",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-add-card--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-add-card--right-to-left",
            "hasPlay": false
          },
          {
            "name": "InGrid",
            "id": "components-add-card--in-grid",
            "description": "In a grid the tile stretches to the height of the tallest card in its row.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/AddCard/AddCard.md",
        "summary": "A dashed \"create new\" tile that sits in a grid next to cards. Decision: [ADR-0011](../../../docs/adr/0011-interactive-cards.md).",
        "whenToUse": [
          "The last tile of a grid of cards, to create another one (\"Create agent\", \"Create report\")."
        ],
        "whenNotToUse": [
          "A navigation or action tile with a title and description: use `CardLink`.",
          "A card that groups content: use `Card`.",
          "The primary action of a screen: use a primary `Button`.",
          "A tile that is not available yet and does nothing: render a non-interactive card (for example `AgentCard status=\"soon\"`), not a disabled `AddCard`, unless you want to show that creation is currently blocked."
        ],
        "keyboard": [
          "`Tab` reaches it once; `Enter` activates a link or a button, `Space` also activates a button."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The last tile of a grid of cards, to create another one (\"Create agent\", \"Create report\")."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A navigation or action tile with a title and description: use `CardLink`.\n- A card that groups content: use `Card`.\n- The primary action of a screen: use a primary `Button`.\n- A tile that is not available yet and does nothing: render a non-interactive card (for example `AgentCard status=\"soon\"`), not a disabled `AddCard`, unless you want to show that creation is currently blocked."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                         | Default | Notes                                                                   |\n| -------------- | ---------------------------- | ------- | ----------------------------------------------------------------------- |\n| `children`     | `ReactNode`                  |         | Required. Verb plus object in sentence case (\"Create agent\"). The name. |\n| `href`         | `string`                     |         | Renders an `<a>`.                                                       |\n| `target`/`rel` | `string`                     |         | With `href`. `_blank` adds `rel` and a hidden note.                     |\n| `onPress`      | `(e: PressEvent) => void`    |         | Required without `href` (renders a `<button>`).                         |\n| `isDisabled`   | `boolean`                    | `false` | Dimmed, skipped by `Tab`.                                               |\n| `messages`     | `{ opensInNewTab?: string }` |         | Per-instance message override.                                          |\n| `className`    | `string`                     |         |                                                                         |\n| `ref`          | element ref                  |         | The `<a>` or `<button>`.                                                |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- One real `<a>` or `<button>` named by the label; the plus icon is decorative.\n- `Tab` reaches it once; `Enter` activates a link or a button, `Space` also activates a button.\n- Focus: accent border and ring (kept in `forced-colors`). Hover: one surface step lighter. Dashed border marks an \"add\" slot (AGENTS.md section 7).\n- It stretches to the height of its row in a `Grid`; minimum height 64 px."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Grid minItemWidth=\"md\">\n  <Card title=\"Support agent\">…</Card>\n  <AddCard onPress={openCreateAgent}>Create agent</AddCard>\n</Grid>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "CardLink",
            "reason": "A navigation or action tile with a title and description: use `CardLink`."
          },
          {
            "name": "Card",
            "reason": "A card that groups content: use `Card`."
          },
          {
            "name": "Button",
            "reason": "The primary action of a screen: use a primary `Button`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "AgentCard",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { AgentCard } from 'loui-ds/patterns';",
      "folder": "src/patterns/AgentCard",
      "sourceFile": "src/patterns/AgentCard/AgentCard.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A card for one agent: icon, name, status, description and a footer with the last activity\nand the number of items waiting for approval.\n\nThe whole card is clickable through the \"stretched link\" pattern: the agent name inside the\nheading is the only link (or button), and a pseudo-element extends its hit area over the\ncard. The accessible name is just the agent name; status, description and footer are the\ndescription. There is no nested interactive element and no `role=\"button\"` on a `div`.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "Name of the agent. It is the accessible name of the card's single link or button."
        },
        {
          "name": "area",
          "type": "string",
          "required": false,
          "description": "Short caption under the name (the area the agent works in)."
        },
        {
          "name": "icon",
          "type": "LucideIcon",
          "required": false,
          "description": "Icon of the agent: any icon of the curated set (for example `SearchIcon`) or another Lucide icon. Decorative."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "One or two sentences about what the agent does. Part of the accessible description."
        },
        {
          "name": "status",
          "type": "'testing' | 'active' | 'paused' | 'soon'",
          "required": false,
          "default": "'active'",
          "description": "Lifecycle of the agent. `testing`, `active` and `paused` are available. `soon` is not\ninteractive: dashed border, no link, no pending count. Default `active`.",
          "values": ["testing", "active", "paused", "soon"]
        },
        {
          "name": "pending",
          "type": "number",
          "required": false,
          "default": "0",
          "description": "Number of items waiting for approval. Shown (accent number plus text) only when above zero and the agent is available."
        },
        {
          "name": "lastActivity",
          "type": "ReactNode",
          "required": false,
          "description": "Text about the last activity (\"Last reply sent at 08:40\"). Shown in the footer."
        },
        {
          "name": "href",
          "type": "string",
          "required": false,
          "description": "Destination of the card. Renders a real link (router-aware through `DSProvider`)."
        },
        {
          "name": "onOpen",
          "type": "() => void",
          "required": false,
          "description": "Called when the card is activated. Without `href` it renders a real button."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "3",
          "description": "Heading level of the agent name (`h1` to `h6`). Default `3`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "messages",
          "type": "Partial<{ statusTesting: string; statusActive: string; statusPaused: string; statusSoon: string; notConfigured: string; pending: (n: number) => string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "AgentCard",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "statusTesting",
            "kind": "string",
            "default": "In testing",
            "description": "Status badge text."
          },
          {
            "key": "statusActive",
            "kind": "string",
            "default": "Active"
          },
          {
            "key": "statusPaused",
            "kind": "string",
            "default": "Paused"
          },
          {
            "key": "statusSoon",
            "kind": "string",
            "default": "Coming soon"
          },
          {
            "key": "notConfigured",
            "kind": "string",
            "default": "Not set up yet",
            "description": "Footer text of a card that is not available yet and has no last-activity text."
          },
          {
            "key": "pending",
            "kind": "function",
            "default": "to approve",
            "signature": "(n: number)",
            "description": "Text after the (separately styled) pending number: \"4 to approve\"."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-agent-card-gap",
        "--ds-agent-card-icon-box-size",
        "--ds-agent-card-min-height",
        "--ds-agent-card-padding",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-heading",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-body",
        "--ds-lh-heading",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line",
        "--ds-line-strong",
        "--ds-line-subtle",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-radius-sm",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Patterns/AgentCard",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-agent-card--default",
            "hasPlay": true
          },
          {
            "name": "Testing",
            "id": "patterns-agent-card--testing",
            "hasPlay": false
          },
          {
            "name": "Paused",
            "id": "patterns-agent-card--paused",
            "hasPlay": false
          },
          {
            "name": "ComingSoon",
            "id": "patterns-agent-card--coming-soon",
            "hasPlay": true
          },
          {
            "name": "AsLink",
            "id": "patterns-agent-card--as-link",
            "hasPlay": false
          },
          {
            "name": "NothingPending",
            "id": "patterns-agent-card--nothing-pending",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-agent-card--long-text",
            "hasPlay": false
          },
          {
            "name": "WithoutIcon",
            "id": "patterns-agent-card--without-icon",
            "hasPlay": false
          },
          {
            "name": "Grid",
            "id": "patterns-agent-card--grid",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-agent-card--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/patterns/AgentCard/AgentCard.md",
        "summary": "A card that represents one AI agent: icon, name, status, description and a footer with the last activity and the number of items waiting for approval. The whole card opens the agent. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/agents/AgentCard` (TODO `CMP-AGT-01`).",
        "whenToUse": [
          "A grid of agents (or automations) on an overview page, each leading to its own screen.",
          "To make \"what needs me\" visible: the accent number is the only call for attention on the card."
        ],
        "whenNotToUse": [
          "A generic clickable card of any other content: build it with `Card` and the same stretched-link technique.",
          "Several actions per card: this card has exactly one (open). Put other actions on the agent's own screen."
        ],
        "keyboard": [
          "One tab stop per available card; `Enter` activates a link or button, `Space` also a button."
        ],
        "examples": [
          "<AgentCard\n  name=\"Marketplace agent\"\n  area=\"Marketplace\"\n  icon={ShoppingBag}\n  status=\"active\"\n  pending={4}\n  description=\"Answers buyer questions and reviews. You approve before it sends.\"\n  lastActivity=\"Last reply sent at 08:40\"\n  href=\"/agents/marketplace\"\n/>\n<AgentCard name=\"Chat agent\" status=\"soon\" description=\"Will draft orders from the conversation.\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A grid of agents (or automations) on an overview page, each leading to its own screen.\n- To make \"what needs me\" visible: the accent number is the only call for attention on the card."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A generic clickable card of any other content: build it with `Card` and the same stretched-link technique.\n- Several actions per card: this card has exactly one (open). Put other actions on the agent's own screen."
          },
          {
            "id": "pattern-decision-stretched-link",
            "heading": "Pattern decision: stretched link",
            "content": "The card needs to be fully clickable, show a status badge and still have a clean accessible name. Three options were weighed:\n\n1. Wrap the card in a link or button: the accessible name becomes the concatenation of every text inside (name, caption, status, description, footer), and block content inside a `<button>` is invalid HTML.\n2. `div role=\"button\"` (the handoff): no native semantics, and a badge nested in a button.\n3. **Stretched link (chosen)**: the agent name, inside the heading, is the only real link or button. A `::after` pseudo-element on it covers the card, so a click anywhere activates it. The accessible name is just the agent name; the status, description and footer are the accessible description (`aria-describedby`). No nested interactive elements.\n\nThe focus ring is drawn around the whole card (`:has()` on the focused link), hover brightens the card border."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (card, data-status)\n  header\n    icon box (decorative)\n    h{headingLevel} > a | button   <- the only interactive element, stretched over the card\n    area caption\n    Badge (status)\n  p description\n  footer\n    last activity text\n    pending: accent number + \"to approve\"\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                                          | Default    | Notes                                                                                      |\n| -------------- | --------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------ |\n| `name`         | `string`                                      | required   | Accessible name of the link or button.                                                     |\n| `area`         | `string`                                      |            | Caption under the name.                                                                    |\n| `icon`         | `IconComponent`                               |            | Any Lucide icon (curated set or passed in). Decorative.                                    |\n| `description`  | `ReactNode`                                   |            | One or two sentences.                                                                      |\n| `status`       | `'testing' \\| 'active' \\| 'paused' \\| 'soon'` | `'active'` | Text from messages. `soon` is not interactive (dashed).                                    |\n| `pending`      | `number`                                      | `0`        | Shown only above zero and when available.                                                  |\n| `lastActivity` | `ReactNode`                                   |            | Footer text. `soon` without it shows \"Not set up yet\".                                     |\n| `href`         | `string`                                      |            | Renders a real `<a>`; router-aware through `DSProvider`.                                   |\n| `onOpen`       | `() => void`                                  |            | Press handler. Without `href` the element is a `<button>`.                                 |\n| `headingLevel` | `1`-`6`                                       | `3`        | Level of the name heading.                                                                 |\n| `messages`     | partial catalog                               |            | `statusTesting`, `statusActive`, `statusPaused`, `statusSoon`, `notConfigured`, `pending`. |\n\nPlace the cards in a `ul` when they form a list; the grid is yours (for example `repeat(auto-fill, minmax(320px, 1fr))`, 16 px gap)."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- One tab stop per available card; `Enter` activates a link or button, `Space` also a button.\n- The status is text in a `Badge` (color and dot only reinforce it) and is part of the description, so it is read after the name.\n- A `soon` card has no link or button and no tab stop; the heading and the \"Coming soon\" badge say why.\n- Count: \"4 to approve\" is real text in reading order (the number is just styled), pluralized and number-formatted through messages and `Intl`.\n- Contrast: the caption and last-activity text use `--ds-text-3` on the surface (passes 4.5:1 there; do not move the card onto a raised surface). Forced colors: the ring becomes a `Highlight` outline."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Name: the agent's job in two or three words. Description: what it does and that you approve, in one or two sentences.\n- Last activity in the past tense with a time: \"Last reply sent at 08:40\". Do not add decorative metrics."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<AgentCard\n  name=\"Marketplace agent\"\n  area=\"Marketplace\"\n  icon={ShoppingBag}\n  status=\"active\"\n  pending={4}\n  description=\"Answers buyer questions and reviews. You approve before it sends.\"\n  lastActivity=\"Last reply sent at 08:40\"\n  href=\"/agents/marketplace\"\n/>\n<AgentCard name=\"Chat agent\" status=\"soon\" description=\"Will draft orders from the conversation.\" />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A real link or button (stretched link) instead of `div role=\"button\"` with a nested badge.\n- `soon` is not interactive and not focusable.\n- Icon is a Lucide component drawn by `Icon` (the handoff fetched an SVG from a CDN at runtime).\n- Status labels, \"Not set up yet\" and \"to approve\" come from messages (pluralized, localized); the number uses `Intl`.\n- Sizes and spacing are `--ds-agent-card-*` tokens; no `style` prop; no JS hover state."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Card",
            "reason": "A generic clickable card of any other content: build it with `Card` and the same stretched-link technique."
          }
        ],
        "uses": ["Badge", "Icon"],
        "usedBy": []
      }
    },
    {
      "name": "Alert",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Alert } from 'loui-ds';",
      "folder": "src/components/Alert",
      "sourceFile": "src/components/Alert/Alert.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An inline message with a tone, an icon, an optional title, an action and a dismiss button.\nThe icon is decorative and a visually hidden prefix names the tone for screen readers.",
      "props": [
        {
          "name": "tone",
          "type": "'info' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'info'",
          "description": "Kind of message: `info` (neutral, default), `success`, `warning` or `danger`.",
          "values": ["info", "success", "warning", "danger"]
        },
        {
          "name": "title",
          "type": "ReactNode",
          "required": false,
          "description": "Short heading of the alert, in one line: what happened. Plain text, not a document heading."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The detail: why it happened and what to do."
        },
        {
          "name": "action",
          "type": "ReactNode",
          "required": false,
          "description": "A single action next to the text, usually a link or a small `Button` (\"Update card\")."
        },
        {
          "name": "onDismiss",
          "type": "() => void",
          "required": false,
          "description": "Shows a dismiss button that calls this. The alert is removed by the parent."
        },
        {
          "name": "urgent",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "The message interrupts the user's task and must be announced immediately (`role=\"alert\"`,\nassertive). Use it only for failures the user must know about right now."
        },
        {
          "name": "announce",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Whether the alert is a live region (`role=\"status\"`, polite). Default `true`, because most\nalerts appear after something the user did. Set `false` for a static notice that is part\nof the page content from the start: it then has no role and is read in page order.\nIgnored when `urgent`."
        },
        {
          "name": "messages",
          "type": "Partial<{ dismiss: string; info: string; success: string; warning: string; danger: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "tone": ["info", "success", "warning", "danger"]
      },
      "messages": {
        "providerKey": "Alert",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "dismiss",
            "kind": "string",
            "default": "Dismiss alert",
            "description": "Name of the dismiss button."
          },
          {
            "key": "info",
            "kind": "string",
            "default": "Information",
            "description": "Visually hidden prefix that tells screen reader users the kind of alert; color and icon alone cannot."
          },
          {
            "key": "success",
            "kind": "string",
            "default": "Success"
          },
          {
            "key": "warning",
            "kind": "string",
            "default": "Warning"
          },
          {
            "key": "danger",
            "kind": "string",
            "default": "Error"
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-alert-danger-bg",
        "--ds-alert-danger-border",
        "--ds-alert-success-bg",
        "--ds-alert-success-border",
        "--ds-alert-warning-bg",
        "--ds-alert-warning-border",
        "--ds-bg",
        "--ds-danger",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line",
        "--ds-radius-lg",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Alert",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-alert--default",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "components-alert--success",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-alert--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-alert--danger",
            "hasPlay": false
          },
          {
            "name": "Urgent",
            "id": "components-alert--urgent",
            "description": "Urgent alerts use `role=\"alert\"` and interrupt screen readers. Reserve them for failures that need attention now.",
            "hasPlay": false
          },
          {
            "name": "StaticNotice",
            "id": "components-alert--static-notice",
            "description": "A static notice that is part of the page from the start has no live region role.",
            "hasPlay": false
          },
          {
            "name": "TitleOnly",
            "id": "components-alert--title-only",
            "hasPlay": false
          },
          {
            "name": "TextOnly",
            "id": "components-alert--text-only",
            "hasPlay": false
          },
          {
            "name": "WithAction",
            "id": "components-alert--with-action",
            "hasPlay": false
          },
          {
            "name": "Dismissible",
            "id": "components-alert--dismissible",
            "description": "Tab to the dismiss button, press Enter.",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-alert--long-text",
            "hasPlay": false
          },
          {
            "name": "AllTones",
            "id": "components-alert--all-tones",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-alert--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Alert/Alert.md",
        "summary": "An inline message with a tone, an icon, an optional title, one action and an optional dismiss button.",
        "whenToUse": [
          "Explain a state that affects the page or a form: a declined payment, a scheduled maintenance, a failed import.",
          "Tell the user what to do about it, next to the thing it is about."
        ],
        "whenNotToUse": [
          "Feedback about an action that just finished (\"Order #1051 created\"): use `Toast`.",
          "A confirmation that needs a decision: use `Dialog`.",
          "A field error: use the field's error message.",
          "A status of a record (Shipped, Failed): use `Badge`."
        ],
        "keyboard": [],
        "examples": [
          "<Alert title=\"Scheduled maintenance\">Sunday, 02:00 to 03:00 UTC.</Alert>\n\n<Alert tone=\"danger\" title=\"Payment declined\" action={<Link href=\"/billing\">Update card</Link>}>\n  The card ending in 4242 was declined.\n</Alert>\n\n<Alert tone=\"danger\" urgent title=\"Connection lost\" onDismiss={() => setOffline(false)}>\n  Changes are not being saved.\n</Alert>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Explain a state that affects the page or a form: a declined payment, a scheduled maintenance, a failed import.\n- Tell the user what to do about it, next to the thing it is about."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Feedback about an action that just finished (\"Order #1051 created\"): use `Toast`.\n- A confirmation that needs a decision: use `Dialog`.\n- A field error: use the field's error message.\n- A status of a record (Shipped, Failed): use `Badge`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=status | alert | none]  (tinted surface, 1px tinted border, radius lg)\n  Icon (decorative)  info | success | warning (triangle) | danger\n  content\n    VisuallyHidden   \"Information\" | \"Success\" | \"Warning\" | \"Error\"\n    title            optional, 14/22, weight 500\n    children         13/18, text-2\n  actions            action slot · dismiss IconButton\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop        | Type                                           | Default  | Notes                                                                          |\n| ----------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------ |\n| `tone`      | `'info' \\| 'success' \\| 'warning' \\| 'danger'` | `'info'` | `info` is neutral; the others tint border and background                       |\n| `title`     | `ReactNode`                                    |          | What happened, one line                                                        |\n| `children`  | `ReactNode`                                    |          | Why, and what to do                                                            |\n| `action`    | `ReactNode`                                    |          | One link or small `Button`                                                     |\n| `onDismiss` | `() => void`                                   |          | Shows the dismiss button; the parent removes the alert                         |\n| `urgent`    | `boolean`                                      | `false`  | `role=\"alert\"` (assertive). Only for failures that need attention now          |\n| `announce`  | `boolean`                                      | `true`   | `false` removes the live region role for static notices. Ignored when `urgent` |\n| `messages`  | `{ dismiss, info, success, warning, danger }`  |          | Per-instance overrides                                                         |\n\nThe vocabulary uses `info` instead of `neutral` for the plain tone, as in the handoff. Other `div` attributes and `ref` go to the root."
          },
          {
            "id": "accessibility-role-decisions",
            "heading": "Accessibility: role decisions",
            "content": "- **Default `role=\"status\"` (polite).** Most alerts appear after something the user did (submit, import). A status region is announced politely, without stealing focus or interrupting speech. The handoff used `role=\"alert\"` for every `danger` alert, which interrupts for things that are not urgent; urgency is now an explicit `urgent` prop, not a function of the tone.\n- **`urgent` gives `role=\"alert\"` (assertive).** Use it for a failure the user must know right now (lost connection, session expired). Avoid more than one at a time.\n- **`announce={false}` gives no role.** A notice that is part of the page when it loads (a permanent banner, a form-level note) does not need a live region; screen readers read it in page order. A live region that never changes is noise, and many `status` regions on one page are worse.\n- Live regions announce **changes**: for the announcement to work, render the Alert when the event happens and keep the region mounted; do not render it hidden and unhide it.\n- **The tone is not only color and icon.** The icon is `aria-hidden`; a visually hidden prefix (\"Error\", \"Warning\", \"Success\", \"Information\", from messages) names the kind of alert for screen readers.\n- The **dismiss button** is an `IconButton` named \"Dismiss alert\" (messages), reachable by Tab and operated with Enter or Space. After dismissing, the parent should move focus somewhere sensible if the alert held it.\n- Text contrast on the tinted backgrounds is at least 4.5:1 (checked in the browser test with the real computed colors). The 6% and 30% tints are component tokens (`--ds-alert-*`).\n- Forced colors: the browser repaints border and icon with system colors; the icon and the hidden tone prefix still name the tone."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: what happened, sentence case, no exclamation marks (\"Payment declined\").\n- Text: why and what to do (\"The card ending in 4242 was declined. Use another card.\").\n- The action is a verb plus object (\"Update card\"), not \"Click here\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Alert title=\"Scheduled maintenance\">Sunday, 02:00 to 03:00 UTC.</Alert>\n\n<Alert tone=\"danger\" title=\"Payment declined\" action={<Link href=\"/billing\">Update card</Link>}>\n  The card ending in 4242 was declined.\n</Alert>\n\n<Alert tone=\"danger\" urgent title=\"Connection lost\" onDismiss={() => setOffline(false)}>\n  Changes are not being saved.\n</Alert>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Icons are bundled Lucide icons, `aria-hidden`, instead of the text glyphs \"i\", \"check\" and \"!\" that screen readers read aloud. Warning and danger have different icons.\n- `role` is `status` by default, `alert` only when `urgent`, none with `announce={false}`.\n- Dismiss option, visually hidden tone prefix, and `messages` (English and pt-BR).\n- The 6% tint and 30% border are tokens instead of inline `color-mix`; the handoff `--radius` (undefined) is `--ds-radius-lg`.\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Dialog",
            "reason": "A confirmation that needs a decision: use `Dialog`."
          },
          {
            "name": "Badge",
            "reason": "A status of a record (Shipped, Failed): use `Badge`."
          }
        ],
        "uses": ["Icon", "IconButton", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "ApprovalCard",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ApprovalCard } from 'loui-ds/patterns';",
      "folder": "src/patterns/ApprovalCard",
      "sourceFile": "src/patterns/ApprovalCard/ApprovalCard.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A reply waiting for approval: the original message, an editable draft written by an agent\nand two actions, send and discard.\n\nIt is an `article` named by its heading (\"Review buyer_sp\"). Send and discard are secondary\nand ghost buttons on purpose: a screen has many of these cards and only one primary action.\nThey act immediately; the consumer shows an Undo toast instead of asking for confirmation.",
      "props": [
        {
          "name": "message",
          "type": "ReactNode",
          "required": true,
          "description": "The original message the draft answers. Plain text or nodes."
        },
        {
          "name": "kind",
          "type": "'question' | 'review'",
          "required": false,
          "default": "'question'",
          "description": "What is being answered: a `question` or a `review`. Shown as a text badge. Default `question`.",
          "values": ["question", "review"]
        },
        {
          "name": "author",
          "type": "string",
          "required": false,
          "description": "Who wrote it (a username or code). Shown in mono and part of the card's accessible name."
        },
        {
          "name": "itemName",
          "type": "string",
          "required": false,
          "description": "Reference to the item the message is about (a product or order name)."
        },
        {
          "name": "itemColor",
          "type": "string",
          "required": false,
          "description": "Hex color (`#rgb` or `#rrggbb`) shown as a dot before `itemName`. Decorative: `itemName` must name it."
        },
        {
          "name": "when",
          "type": "ReactNode",
          "required": false,
          "description": "When it was written, as display text (\"today, 10:40\")."
        },
        {
          "name": "whenDateTime",
          "type": "string",
          "required": false,
          "description": "Machine-readable date for `when` (an ISO string). Renders a `<time>`."
        },
        {
          "name": "rating",
          "type": "number",
          "required": false,
          "description": "Star rating of a `review` (0 to 5, halves allowed). Shown only for reviews."
        },
        {
          "name": "draft",
          "type": "string",
          "required": false,
          "description": "Controlled value of the draft reply."
        },
        {
          "name": "defaultDraft",
          "type": "string",
          "required": false,
          "description": "Initial value of the draft when uncontrolled."
        },
        {
          "name": "onDraftChange",
          "type": "(draft: string) => void",
          "required": false,
          "description": "Called with the new text whenever the draft is edited."
        },
        {
          "name": "flag",
          "type": "ReactNode",
          "required": false,
          "description": "Warning shown above the actions (for example \"2-star review: read it carefully before\nsending\"). Linked to the card as its description. Reviews of 1 to 3 stars should always have one."
        },
        {
          "name": "onSend",
          "type": "() => void",
          "required": false,
          "description": "Called when the send button is pressed. Do it now and offer Undo with a toast; do not ask to confirm."
        },
        {
          "name": "onDiscard",
          "type": "() => void",
          "required": false,
          "description": "Called when the discard button is pressed. Do it now and offer Undo with a toast."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "3",
          "description": "Heading level of the card heading (`h1` to `h6`). Default `3`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "messages",
          "type": "Partial<{ kindQuestion: string; kindReview: string; messageLabel: string; draftLabel: string; draftHint: string; send: string; discard: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ApprovalCard",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "kindQuestion",
            "kind": "string",
            "default": "Question",
            "description": "Kind badge and the first word of the accessible name."
          },
          {
            "key": "kindReview",
            "kind": "string",
            "default": "Review"
          },
          {
            "key": "messageLabel",
            "kind": "string",
            "default": "Customer message",
            "description": "Visually hidden label before the original message."
          },
          {
            "key": "draftLabel",
            "kind": "string",
            "default": "Agent reply",
            "description": "Label and hint of the editable draft."
          },
          {
            "key": "draftHint",
            "kind": "string",
            "default": "You can edit before sending"
          },
          {
            "key": "send",
            "kind": "string",
            "default": "Send reply",
            "description": "Verb plus object buttons. The card's heading describes which card they belong to."
          },
          {
            "key": "discard",
            "kind": "string",
            "default": "Discard reply"
          }
        ]
      },
      "cssVariables": [
        "--ds-approval-card-body-padding-block",
        "--ds-approval-card-gap",
        "--ds-approval-card-header-min-height",
        "--ds-approval-card-header-padding-block",
        "--ds-approval-card-padding",
        "--ds-bg",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-subtle",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Patterns/ApprovalCard",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-approval-card--default",
            "hasPlay": true
          },
          {
            "name": "Review",
            "id": "patterns-approval-card--review",
            "hasPlay": false
          },
          {
            "name": "LowRatingWithFlag",
            "id": "patterns-approval-card--low-rating-with-flag",
            "hasPlay": false
          },
          {
            "name": "Minimal",
            "id": "patterns-approval-card--minimal",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-approval-card--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-approval-card--right-to-left",
            "hasPlay": false
          },
          {
            "name": "SendWithUndo",
            "id": "patterns-approval-card--send-with-undo",
            "description": "The recipe: act now, then offer Undo in a toast. No confirmation dialog.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ApprovalCard/ApprovalCard.md",
        "summary": "A reply waiting for approval: the original message, an editable draft written by an agent, an optional warning and two actions, send and discard. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/agents/ApprovalCard` (TODO `CMP-AGT-02`).",
        "whenToUse": [
          "An approval queue: replies, messages or changes an agent prepared that go to a customer or touch money.",
          "Any \"review the draft, edit it, send or discard it\" step with many items on one screen."
        ],
        "whenNotToUse": [
          "A single decision with a consequence you cannot take back: use a `Dialog` with a confirmation.",
          "Free chat with an agent: use `ChatPanel`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- An approval queue: replies, messages or changes an agent prepared that go to a customer or touch money.\n- Any \"review the draft, edit it, send or discard it\" step with many items on one screen."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A single decision with a consequence you cannot take back: use a `Dialog` with a confirmation.\n- Free chat with an agent: use `ChatPanel`."
          },
          {
            "id": "principles",
            "heading": "Principles",
            "content": "**Approve before sending.** Anything an agent writes to a customer goes through this card first. The draft is editable (\"You can edit before sending\"). Reviews of 1 to 3 stars always carry a `flag`.\n\n**Undo, don't confirm.** Send and discard act immediately. Do not open a confirmation dialog; show a toast with **Undo** (see the recipe below). A confirmation for every card would train people to click through it.\n\n**Secondary buttons, on purpose.** A screen has many cards and only one primary action (AGENTS.md section 5). Send is `secondary`, Discard is `ghost`. The button text is verb plus object (\"Send reply\", \"Discard reply\"), not a bare \"Send\"."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\narticle (named by the heading)\n  header\n    h{headingLevel}    Badge (Question | Review) + author in mono   <- the accessible name\n    Rating             reviews only\n    item               optional Swatch dot + item name\n    time               when\n  body\n    message            original text, visually hidden label \"Customer message:\"\n    Textarea           label \"Agent reply\", aside \"You can edit before sending\"\n    flag               optional warning (icon + text), the card's description\n    actions            Discard (ghost)                 Send (secondary)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                       | Type                                    | Default      | Notes                                                                                       |\n| ------------------------------------------ | --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |\n| `kind`                                     | `'question' \\| 'review'`                | `'question'` | Badge text from messages.                                                                   |\n| `author`                                   | `string`                                |              | Mono, part of the accessible name.                                                          |\n| `rating`                                   | `number`                                |              | Reviews only; halves allowed.                                                               |\n| `itemName` / `itemColor`                   | `string`                                |              | Item reference; the color is a decorative `Swatch` (hex).                                   |\n| `when` / `whenDateTime`                    | `ReactNode` / `string`                  |              | Display text and ISO date for `<time>`.                                                     |\n| `message`                                  | `ReactNode`                             | required     | The original message.                                                                       |\n| `draft` / `defaultDraft` / `onDraftChange` | `string` / `string` / `(draft) => void` |              | Controlled or uncontrolled draft.                                                           |\n| `flag`                                     | `ReactNode`                             |              | Warning text. Linked with `aria-describedby`.                                               |\n| `onSend` / `onDiscard`                     | `() => void`                            |              | Run the action now and offer Undo.                                                          |\n| `headingLevel`                             | `1`-`6`                                 | `3`          |                                                                                             |\n| `messages`                                 | partial catalog                         |              | `kindQuestion`, `kindReview`, `messageLabel`, `draftLabel`, `draftHint`, `send`, `discard`. |"
          },
          {
            "id": "undo-recipe",
            "heading": "Undo recipe",
            "content": "```tsx\nfunction Queue() {\n  const toast = useToast();\n  function send(reply: Reply) {\n    const previous = reply.state; // save what you need to reverse\n    markSent(reply.id); // 1. do it now\n    toast.success('Reply sent', {\n      // 2. say so, past tense\n      action: { label: 'Undo', onPress: () => restore(reply.id, previous) }, // 3. reverse it\n    });\n  }\n  return replies.map((reply) => (\n    <ApprovalCard\n      key={reply.id}\n      {...reply}\n      onSend={() => send(reply)}\n      onDiscard={() => discard(reply)}\n    />\n  ));\n}\n```\n\nThe real side effect (actually sending to the customer) should be held for the undo window, or undone by a compensating action; see the Toast documentation. After send or discard, remove the card from the queue and move focus to the next card's heading or to the list, so keyboard focus is not lost."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `article` named by its heading through `aria-labelledby` (\"Review buyer_sp\"); with several cards, screen reader users can jump by heading or article.\n- The two buttons are described by the heading (`aria-describedby`), so \"Send reply, Review buyer_sp\" is spoken without making every button name unique.\n- The kind is a text badge, the rating is an image with a text alternative, the flag is text with a decorative icon: nothing relies on color alone. The flag uses `--ds-warning` on the surface (4.5:1 or better).\n- Tab order follows reading order: draft, Discard, Send. The draft is a real labelled `Textarea` (its hint is the field description).\n- The original message is announced after a hidden \"Customer message:\" label."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- The draft is a customer-facing reply: short, factual, no promises about delivery time or discounts, no emoji (AGENTS.md section 5). That is the agent's job; the card only makes it visible and editable.\n- `flag` explains the risk and what to do: \"2-star review: read it carefully before sending.\""
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The `section` has an accessible name (`article` named by a real heading).\n- Strings (kind, labels, buttons) come from messages; the Portuguese originals are the `pt-BR` catalog. Buttons say \"Send reply\" / \"Discard reply\".\n- The item dot uses `Swatch`; spacing uses `--ds-approval-card-*` tokens; the textarea is the design system `Textarea`.\n- The flag is linked as the card's description and has an icon, not only a colored dot.\n- Documented: Send and Discard are secondary/ghost, and undo is a toast."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Dialog",
            "reason": "A single decision with a consequence you cannot take back: use a `Dialog` with a confirmation."
          },
          {
            "name": "ChatPanel",
            "reason": "Free chat with an agent: use `ChatPanel`."
          }
        ],
        "uses": ["Badge", "Button", "Icon", "Rating", "Swatch", "Textarea", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "AppShell",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { AppShell } from 'loui-ds';",
      "folder": "src/components/AppShell",
      "sourceFile": "src/components/AppShell/AppShell.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The application layout: a sidebar, and a main column with a top bar above a scrollable\ncontent region. The sidebar and the content scroll independently; the page itself does not.\n\nLandmarks: the sidebar (`aside`, from `Sidebar`), the top bar (`header`), one `main`\n(`id=\"main\"`), and the optional detail `aside`. A skip link to `main` comes first.\n\nResponsive contract (TOK-010): the shell measures its own width (a container query, so it\nalso works inside a frame). From `--ds-breakpoint-md` (768px) up the sidebar sits in the flow.\nBelow it the sidebar leaves the flow and opens as an off-canvas panel over the content,\ncontrolled by `isNavOpen` and `AppShellNavToggle`; while it is open the rest of the page is\ninert, `Escape` and the scrim close it, and focus returns to the toggle. The shell works from\n320px. It fills its parent: give the parent a height, or it takes the viewport height.",
      "props": [
        {
          "name": "sidebar",
          "type": "ReactNode",
          "required": false,
          "description": "The app sidebar: a `Sidebar` element. Below the md breakpoint it becomes an off-canvas panel."
        },
        {
          "name": "topBar",
          "type": "ReactNode",
          "required": false,
          "description": "The page header: a `TopBar` element. Put an `AppShellNavToggle` in its `children` so narrow\nscreens can open the sidebar."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The page content. It is rendered inside the one `main` landmark."
        },
        {
          "name": "detail",
          "type": "ReactNode",
          "required": false,
          "description": "Optional detail pane at the inline end, `--ds-app-shell-detail-w` (360px) wide, for the\nrecord selected in the main content. It is an `aside` landmark with its own scroll. Below\nthe md breakpoint it moves under the main content. `null`, `undefined` and `false` render\nnothing; a component that renders `null` leaves no empty aside and no reserved column either\n(the empty aside is hidden with `:empty`)."
        },
        {
          "name": "detailLabel",
          "type": "string",
          "required": false,
          "description": "Accessible name of the detail aside. Default \"Details\" from the message catalog."
        },
        {
          "name": "mainId",
          "type": "string",
          "required": false,
          "default": "main",
          "description": "`id` of the `main` element, and the target of the skip link. Default `main`."
        },
        {
          "name": "padded",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Pads the main content with `--ds-app-shell-main-padding`. Turn it off when the content brings its own padding (a `Container` has a gutter). Default `true`."
        },
        {
          "name": "skipLink",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Renders the skip link as the first focusable element. Default `true`; turn it off only if the app has its own."
        },
        {
          "name": "isNavOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled state of the off-canvas sidebar (narrow screens only). Pair with `onNavOpenChange`."
        },
        {
          "name": "defaultNavOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial state of the off-canvas sidebar when uncontrolled. Default `false`."
        },
        {
          "name": "onNavOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the off-canvas sidebar opens or closes (toggle, `Escape`, scrim, close button)."
        },
        {
          "name": "messages",
          "type": "Partial<{ navToggle: string; navClose: string; detail: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "AppShell",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "navToggle",
            "kind": "string",
            "default": "Menu",
            "description": "Text of the button that opens the navigation panel on narrow screens."
          },
          {
            "key": "navClose",
            "kind": "string",
            "default": "Close menu",
            "description": "Text of the button that closes the navigation panel on narrow screens."
          },
          {
            "key": "detail",
            "kind": "string",
            "default": "Details",
            "description": "Accessible name of the detail aside landmark."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-app-shell-detail-w",
        "--ds-app-shell-main-padding",
        "--ds-app-shell-nav-z-index",
        "--ds-bg",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-scrim",
        "--ds-shadow-dialog",
        "--ds-space-1",
        "--ds-space-12",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-text",
        "--ds-toast-clearance"
      ],
      "stories": {
        "title": "Layout/AppShell",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-app-shell--default",
            "description": "Sidebar on the left, top bar and scrolling content on the right. The sidebar and the content scroll apart.",
            "hasPlay": true
          },
          {
            "name": "WithDetail",
            "id": "layout-app-shell--with-detail",
            "description": "The detail aside (360px) holds the record selected in the content.",
            "hasPlay": false
          },
          {
            "name": "WithoutSidebar",
            "id": "layout-app-shell--without-sidebar",
            "description": "Without a sidebar the shell is a header and content.",
            "hasPlay": false
          },
          {
            "name": "Unpadded",
            "id": "layout-app-shell--unpadded",
            "description": "Content without padding, for a table or a page that brings its own container.",
            "hasPlay": false
          },
          {
            "name": "LongContent",
            "id": "layout-app-shell--long-content",
            "description": "Content taller than the viewport scrolls inside `main`; the top bar stays put.",
            "hasPlay": false
          },
          {
            "name": "NarrowClosed",
            "id": "layout-app-shell--narrow-closed",
            "description": "Narrow screens: the sidebar leaves the flow; the Menu button opens it as a panel.",
            "hasPlay": true
          },
          {
            "name": "NarrowOpen",
            "id": "layout-app-shell--narrow-open",
            "description": "Open: focus moves into the panel, the content is inert, Escape or the scrim closes it.",
            "hasPlay": true
          },
          {
            "name": "Narrow320",
            "id": "layout-app-shell--narrow320",
            "description": "The smallest supported width.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-app-shell--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/AppShell/AppShell.md",
        "summary": "The application layout: a sidebar, and a main column with a top bar above a scrollable content region. It includes the skip link and the landmarks.",
        "whenToUse": [
          "The frame of every screen of an app: `Sidebar` + `TopBar` + content (+ optional detail pane)."
        ],
        "whenNotToUse": [
          "A marketing page or a single-column document: use `Container` and let the page scroll.",
          "Inside another AppShell."
        ],
        "keyboard": [],
        "examples": [
          "<AppShell\n  sidebar={<Sidebar workspace={workspace} sections={sections} value={route} onChange={go} />}\n  topBar={\n    <TopBar title=\"Orders\" actions={<Button variant=\"primary\">New order</Button>}>\n      <AppShellNavToggle />\n    </TopBar>\n  }\n  detail={selected ? <OrderDetail order={selected} /> : undefined}\n>\n  <OrdersTable />\n</AppShell>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The frame of every screen of an app: `Sidebar` + `TopBar` + content (+ optional detail pane)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A marketing page or a single-column document: use `Container` and let the page scroll.\n- Inside another AppShell."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root  (container, fills the parent, no page scroll)\n  SkipLink                       first focusable element\n  div.nav                        wraps the Sidebar (aside); off-canvas below md\n  div.scrim                      only when the panel is open\n  div.column\n    TopBar                       header\n    div.body\n      main#main                  scrolls on its own\n      aside \"Details\"            optional, 360px, scrolls on its own\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                             | Type                              | Default   | Notes                                                               |\n| ------------------------------------------------ | --------------------------------- | --------- | ------------------------------------------------------------------- |\n| `sidebar`                                        | `ReactNode`                       |           | A `Sidebar`                                                         |\n| `topBar`                                         | `ReactNode`                       |           | A `TopBar`; put `<AppShellNavToggle />` in its `children`           |\n| `children`                                       | `ReactNode`                       |           | The page, inside the one `main`                                     |\n| `detail`, `detailLabel`                          | `ReactNode`, `string`             | \"Details\" | Optional aside at the inline end (`--ds-app-shell-detail-w`, 360px) |\n| `mainId`                                         | `string`                          | `'main'`  | `id` of `main` and the skip link target                             |\n| `padded`                                         | `boolean`                         | `true`    | Pads `main` (24px); turn off for content with its own padding       |\n| `skipLink`                                       | `boolean`                         | `true`    |                                                                     |\n| `isNavOpen`, `defaultNavOpen`, `onNavOpenChange` |                                   | `false`   | Off-canvas sidebar state (below md)                                 |\n| `messages`                                       | `{ navToggle, navClose, detail }` |           | Per-instance overrides                                              |\n\nThe shell is `100dvh` tall (capped by its parent's height): give the parent a height to embed it."
          },
          {
            "id": "responsive-decision-tok-010",
            "heading": "Responsive decision (TOK-010)",
            "content": "**The DS guarantees every component works from 320px wide, and AppShell collapses its sidebar into an off-canvas panel below `--ds-breakpoint-md` (768px).**\n\nWhy built in rather than left to apps: a sidebar hidden by an app without a way back strands keyboard and touch users, and every product would build the same panel. It is kept simple: it reuses the same `Sidebar` element (no second copy, no lost state) and CSS does the layout.\n\n- The shell measures **its own width** with a container query, so it behaves the same in a frame, a preview or a test. The 768px in the CSS is `--ds-breakpoint-md`; CSS cannot read variables inside a query, so `AppShell.test.tsx` asserts the two agree at 767/768px.\n- From md up the sidebar sits in the flow. Below, it leaves the flow; `AppShellNavToggle` (visible only below md) opens it as a panel over a scrim.\n- While open: focus moves into the panel, the rest of the page is `inert`, `Escape`, the scrim or the \"Close menu\" button close it, and focus returns to the toggle. It is not a `Dialog`: it keeps the sidebar's own landmarks.\n- An empty detail leaves nothing behind: `detail` set to `null`, `undefined` or `false` renders no aside, and a component that renders `null` leaves an empty aside that is hidden (`:empty`), so no landmark and no 360px column stay. Prefer `detail={selected ? <Detail /> : undefined}`.\n- The detail pane moves **under** the main content (half the height at most, own scroll) below md.\n- Breakpoint tokens: `--ds-breakpoint-sm|md|lg|xl` (480 / 768 / 1024 / 1280 px); content: `--ds-content-max`, `--ds-content-gutter`; `--ds-container-sm|md|lg`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Landmarks: `aside` (Sidebar, \"Sidebar\"), `header` (TopBar), **one** `main`, optional `aside` (\"Details\"). Each `nav` in the sidebar is named by its caption.\n- The skip link is the first stop and moves focus to `main`.\n- `main` and the detail aside scroll independently. While they overflow they become keyboard stops (`tabindex=\"0\"`) so the arrow keys scroll them; with nothing to scroll there is no extra tab stop.\n- The closed panel is `visibility: hidden` and `inert`, so it is out of the tab order and the accessibility tree.\n- The panel slide respects `prefers-reduced-motion`; the layout mirrors in RTL.\n- `AppShellNavToggle` reports `aria-expanded` and `aria-controls`. It is an `IconButton` (menu icon) named \"Menu\" (`pt-BR` \"Menu\"; pass a string child to rename it); the close button is an `IconButton` (x icon) named \"Close menu\" / \"Fechar menu\", which sits on the scrim beside the panel and never wraps."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- One `h1` per page: `TopBar` renders it from `title`.\n- One primary action per screen, at the end of the top bar."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<AppShell\n  sidebar={<Sidebar workspace={workspace} sections={sections} value={route} onChange={go} />}\n  topBar={\n    <TopBar title=\"Orders\" actions={<Button variant=\"primary\">New order</Button>}>\n      <AppShellNavToggle />\n    </TopBar>\n  }\n  detail={selected ? <OrderDetail order={selected} /> : undefined}\n>\n  <OrdersTable />\n</AppShell>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff assumed a 1180px minimum width and a fixed 240px sidebar. The shell now works from 320px, and the sidebar width stays a token.\n- The skip link, the single `main` and the independent scrolling were left to each screen; the shell provides them."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Container",
            "reason": "A marketing page or a single-column document: use `Container` and let the page scroll."
          }
        ],
        "uses": ["IconButton", "SkipLink"],
        "usedBy": []
      }
    },
    {
      "name": "AppShellNavToggle",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { AppShellNavToggle } from 'loui-ds';",
      "folder": "src/components/AppShell",
      "sourceFile": "src/components/AppShell/AppShell.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The icon button (menu icon) that opens the off-canvas sidebar. Put it in the `TopBar` (as its `children`).\nIt is only visible below the md breakpoint, and reports `aria-expanded`. It must be rendered\ninside `AppShell`.",
      "props": [
        {
          "name": "children",
          "type": "string",
          "required": false,
          "description": "Accessible name of the icon button. Default \"Menu\" from the message catalog."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the wrapper."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-app-shell-detail-w",
        "--ds-app-shell-main-padding",
        "--ds-app-shell-nav-z-index",
        "--ds-bg",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-scrim",
        "--ds-shadow-dialog",
        "--ds-space-1",
        "--ds-space-12",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-text",
        "--ds-toast-clearance"
      ],
      "stories": {
        "title": "Layout/AppShell",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-app-shell--default",
            "description": "Sidebar on the left, top bar and scrolling content on the right. The sidebar and the content scroll apart.",
            "hasPlay": true
          },
          {
            "name": "WithDetail",
            "id": "layout-app-shell--with-detail",
            "description": "The detail aside (360px) holds the record selected in the content.",
            "hasPlay": false
          },
          {
            "name": "WithoutSidebar",
            "id": "layout-app-shell--without-sidebar",
            "description": "Without a sidebar the shell is a header and content.",
            "hasPlay": false
          },
          {
            "name": "Unpadded",
            "id": "layout-app-shell--unpadded",
            "description": "Content without padding, for a table or a page that brings its own container.",
            "hasPlay": false
          },
          {
            "name": "LongContent",
            "id": "layout-app-shell--long-content",
            "description": "Content taller than the viewport scrolls inside `main`; the top bar stays put.",
            "hasPlay": false
          },
          {
            "name": "NarrowClosed",
            "id": "layout-app-shell--narrow-closed",
            "description": "Narrow screens: the sidebar leaves the flow; the Menu button opens it as a panel.",
            "hasPlay": true
          },
          {
            "name": "NarrowOpen",
            "id": "layout-app-shell--narrow-open",
            "description": "Open: focus moves into the panel, the content is inert, Escape or the scrim closes it.",
            "hasPlay": true
          },
          {
            "name": "Narrow320",
            "id": "layout-app-shell--narrow320",
            "description": "The smallest supported width.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-app-shell--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/AppShell/AppShell.md",
        "summary": "The application layout: a sidebar, and a main column with a top bar above a scrollable content region. It includes the skip link and the landmarks.",
        "whenToUse": [
          "The frame of every screen of an app: `Sidebar` + `TopBar` + content (+ optional detail pane)."
        ],
        "whenNotToUse": [
          "A marketing page or a single-column document: use `Container` and let the page scroll.",
          "Inside another AppShell."
        ],
        "keyboard": [],
        "examples": [
          "<AppShell\n  sidebar={<Sidebar workspace={workspace} sections={sections} value={route} onChange={go} />}\n  topBar={\n    <TopBar title=\"Orders\" actions={<Button variant=\"primary\">New order</Button>}>\n      <AppShellNavToggle />\n    </TopBar>\n  }\n  detail={selected ? <OrderDetail order={selected} /> : undefined}\n>\n  <OrdersTable />\n</AppShell>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The frame of every screen of an app: `Sidebar` + `TopBar` + content (+ optional detail pane)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A marketing page or a single-column document: use `Container` and let the page scroll.\n- Inside another AppShell."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root  (container, fills the parent, no page scroll)\n  SkipLink                       first focusable element\n  div.nav                        wraps the Sidebar (aside); off-canvas below md\n  div.scrim                      only when the panel is open\n  div.column\n    TopBar                       header\n    div.body\n      main#main                  scrolls on its own\n      aside \"Details\"            optional, 360px, scrolls on its own\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                             | Type                              | Default   | Notes                                                               |\n| ------------------------------------------------ | --------------------------------- | --------- | ------------------------------------------------------------------- |\n| `sidebar`                                        | `ReactNode`                       |           | A `Sidebar`                                                         |\n| `topBar`                                         | `ReactNode`                       |           | A `TopBar`; put `<AppShellNavToggle />` in its `children`           |\n| `children`                                       | `ReactNode`                       |           | The page, inside the one `main`                                     |\n| `detail`, `detailLabel`                          | `ReactNode`, `string`             | \"Details\" | Optional aside at the inline end (`--ds-app-shell-detail-w`, 360px) |\n| `mainId`                                         | `string`                          | `'main'`  | `id` of `main` and the skip link target                             |\n| `padded`                                         | `boolean`                         | `true`    | Pads `main` (24px); turn off for content with its own padding       |\n| `skipLink`                                       | `boolean`                         | `true`    |                                                                     |\n| `isNavOpen`, `defaultNavOpen`, `onNavOpenChange` |                                   | `false`   | Off-canvas sidebar state (below md)                                 |\n| `messages`                                       | `{ navToggle, navClose, detail }` |           | Per-instance overrides                                              |\n\nThe shell is `100dvh` tall (capped by its parent's height): give the parent a height to embed it."
          },
          {
            "id": "responsive-decision-tok-010",
            "heading": "Responsive decision (TOK-010)",
            "content": "**The DS guarantees every component works from 320px wide, and AppShell collapses its sidebar into an off-canvas panel below `--ds-breakpoint-md` (768px).**\n\nWhy built in rather than left to apps: a sidebar hidden by an app without a way back strands keyboard and touch users, and every product would build the same panel. It is kept simple: it reuses the same `Sidebar` element (no second copy, no lost state) and CSS does the layout.\n\n- The shell measures **its own width** with a container query, so it behaves the same in a frame, a preview or a test. The 768px in the CSS is `--ds-breakpoint-md`; CSS cannot read variables inside a query, so `AppShell.test.tsx` asserts the two agree at 767/768px.\n- From md up the sidebar sits in the flow. Below, it leaves the flow; `AppShellNavToggle` (visible only below md) opens it as a panel over a scrim.\n- While open: focus moves into the panel, the rest of the page is `inert`, `Escape`, the scrim or the \"Close menu\" button close it, and focus returns to the toggle. It is not a `Dialog`: it keeps the sidebar's own landmarks.\n- An empty detail leaves nothing behind: `detail` set to `null`, `undefined` or `false` renders no aside, and a component that renders `null` leaves an empty aside that is hidden (`:empty`), so no landmark and no 360px column stay. Prefer `detail={selected ? <Detail /> : undefined}`.\n- The detail pane moves **under** the main content (half the height at most, own scroll) below md.\n- Breakpoint tokens: `--ds-breakpoint-sm|md|lg|xl` (480 / 768 / 1024 / 1280 px); content: `--ds-content-max`, `--ds-content-gutter`; `--ds-container-sm|md|lg`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Landmarks: `aside` (Sidebar, \"Sidebar\"), `header` (TopBar), **one** `main`, optional `aside` (\"Details\"). Each `nav` in the sidebar is named by its caption.\n- The skip link is the first stop and moves focus to `main`.\n- `main` and the detail aside scroll independently. While they overflow they become keyboard stops (`tabindex=\"0\"`) so the arrow keys scroll them; with nothing to scroll there is no extra tab stop.\n- The closed panel is `visibility: hidden` and `inert`, so it is out of the tab order and the accessibility tree.\n- The panel slide respects `prefers-reduced-motion`; the layout mirrors in RTL.\n- `AppShellNavToggle` reports `aria-expanded` and `aria-controls`. It is an `IconButton` (menu icon) named \"Menu\" (`pt-BR` \"Menu\"; pass a string child to rename it); the close button is an `IconButton` (x icon) named \"Close menu\" / \"Fechar menu\", which sits on the scrim beside the panel and never wraps."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- One `h1` per page: `TopBar` renders it from `title`.\n- One primary action per screen, at the end of the top bar."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<AppShell\n  sidebar={<Sidebar workspace={workspace} sections={sections} value={route} onChange={go} />}\n  topBar={\n    <TopBar title=\"Orders\" actions={<Button variant=\"primary\">New order</Button>}>\n      <AppShellNavToggle />\n    </TopBar>\n  }\n  detail={selected ? <OrderDetail order={selected} /> : undefined}\n>\n  <OrdersTable />\n</AppShell>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff assumed a 1180px minimum width and a fixed 240px sidebar. The shell now works from 320px, and the sidebar width stays a token.\n- The skip link, the single `main` and the independent scrolling were left to each screen; the shell provides them."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Container",
            "reason": "A marketing page or a single-column document: use `Container` and let the page scroll."
          }
        ],
        "uses": ["IconButton", "SkipLink"],
        "usedBy": []
      }
    },
    {
      "name": "Avatar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Avatar } from 'loui-ds';",
      "folder": "src/components/Avatar",
      "sourceFile": "src/components/Avatar/Avatar.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A circular photo or initials of a person, in grays (never a color per person). The `name`\ndrives the initials and the accessible name; a photo that fails to load falls back to the\ninitials.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "The person's full name. Drives the initials and the accessible name (`alt` of the photo).\nOptional: still used to compute the initials."
        },
        {
          "name": "decorative",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Hides the avatar from assistive technology (`alt=\"\"`). Use it when the name is shown next to it."
        },
        {
          "name": "src",
          "type": "string",
          "required": false,
          "description": "URL of the photo. If it fails to load, the initials are shown instead."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "`sm` 24px, `md` 28px (default), `lg` 40px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "ring",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Draws a 2px ring in the surface color, to separate overlapping avatars. Default `false`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-avatar-initials-ratio",
        "--ds-avatar-ring-width",
        "--ds-avatar-size-lg",
        "--ds-avatar-size-md",
        "--ds-avatar-size-sm",
        "--ds-font-sans",
        "--ds-line",
        "--ds-line-strong",
        "--ds-radius-pill",
        "--ds-surface",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Avatar",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-avatar--default",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-avatar--sizes",
            "hasPlay": false
          },
          {
            "name": "WithPhoto",
            "id": "components-avatar--with-photo",
            "hasPlay": false
          },
          {
            "name": "BrokenPhoto",
            "id": "components-avatar--broken-photo",
            "hasPlay": false
          },
          {
            "name": "Decorative",
            "id": "components-avatar--decorative",
            "hasPlay": false
          },
          {
            "name": "Shades",
            "id": "components-avatar--shades",
            "hasPlay": false
          },
          {
            "name": "Stacked",
            "id": "components-avatar--stacked",
            "hasPlay": false
          },
          {
            "name": "LongName",
            "id": "components-avatar--long-name",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-avatar--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Avatar/Avatar.md",
        "summary": "A circular photo or the initials of a person, in grays. There is no color per person.",
        "whenToUse": [
          "Identify who owns, created or is assigned to something.",
          "Next to a name in a list, a comment or a menu."
        ],
        "whenNotToUse": [
          "For a company logo or a product thumbnail: use an image.",
          "For a status: use `Badge`."
        ],
        "keyboard": [],
        "examples": [
          "<Avatar name=\"Ada Lovelace\" />\n<Avatar name=\"Ada Lovelace\" src=\"/photos/ada.jpg\" size=\"lg\" />\n<Avatar decorative name=\"Ada Lovelace\" /> <span>Ada Lovelace</span>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Identify who owns, created or is assigned to something.\n- Next to a name in a list, a comment or a menu."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a company logo or a product thumbnail: use an image.\n- For a status: use `Badge`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan.root (circle, role=\"img\" + aria-label when it initials)\n  img | span[aria-hidden] initials\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                   | Default | Notes                                                                                  |\n| ------------ | ---------------------- | ------- | -------------------------------------------------------------------------------------- |\n| `name`       | `string`               | -       | Required unless `decorative`. Drives the initials and the accessible name              |\n| `src`        | `string`               |         | Photo URL. A load error falls back to the initials                                     |\n| `size`       | `'sm' \\| 'md' \\| 'lg'` | `'md'`  | 24, 28 and 40 px (`--ds-avatar-size-*`)                                                |\n| `ring`       | `boolean`              | `false` | 2px surface-colored ring, to separate overlapping avatars                              |\n| `decorative` | `boolean`              | `false` | Hides the avatar from assistive technology (`alt=\"\"`). Use it next to the visible name |\n\nThe type makes `name` required when the avatar is not decorative. Other `span` attributes and `ref` go to the root."
          },
          {
            "id": "initials",
            "heading": "Initials",
            "content": "`getInitials(name, locale)` (also exported) takes the first letter of the first and of the last word: \"Ana Maria da Silva\" is `AS`; a single word gives one letter. It uses `Intl.Segmenter` (combined characters stay whole) and `toLocaleUpperCase` with the `DSProvider` locale (`i` becomes `İ` in Turkish). The gray shade is derived from the name, so one person always looks the same."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- With initials the root is `role=\"img\"` named by `name`; the letters are `aria-hidden` so they are not read twice.\n- With a photo the `<img>` has `alt={name}`. A decorative avatar has `alt=\"\"` and no role.\n- The handoff set `alt` to the initials (\"AL\"), which is meaningless to a screen reader; the full name is used now.\n- If the image cannot be loaded the initials appear and the accessible name stays the same.\n- Avatars are not interactive. Put one inside a button or link that has its own name and mark it `decorative`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Pass the full display name, not the initials.\n- Never pass an email or an id as `name`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Avatar name=\"Ada Lovelace\" />\n<Avatar name=\"Ada Lovelace\" src=\"/photos/ada.jpg\" size=\"lg\" />\n<Avatar decorative name=\"Ada Lovelace\" /> <span>Ada Lovelace</span>\n```\n\nStacked avatars: wrap them in a flex container with `role=\"group\"` and an `aria-label`, give each `ring`, and overlap with a negative inline margin. A dedicated `AvatarGroup` is not part of this version."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `name` replaces `initials`; initials are computed and locale-aware.\n- `alt` is the person's name, or empty when `decorative`.\n- Image error fallback.\n- `size` is `sm | md | lg` (tokens) instead of a number; `tone` and `ring` color props are gone (shade is derived from the name, ring is a boolean).\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Badge",
            "reason": "For a status: use `Badge`."
          }
        ],
        "uses": [],
        "usedBy": ["Sidebar"]
      }
    },
    {
      "name": "Badge",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Badge } from 'loui-ds';",
      "folder": "src/components/Badge",
      "sourceFile": "src/components/Badge/Badge.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A pill with a short status label. The text always states the status; the tone and the\ndot only reinforce it.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The status text. Required: color and the dot never carry the meaning alone."
        },
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Meaning of the badge: `accent` open or in progress, `success` done, `warning` pending,\n`danger` failed, `neutral` everything else (default).",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "dot",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Shows the small decorative dot before the text. It is hidden from assistive technology. Default `true`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-badge-dot-size",
        "--ds-badge-height",
        "--ds-badge-padding-inline",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-radius-pill",
        "--ds-space-2",
        "--ds-success",
        "--ds-success-soft",
        "--ds-surface-hover",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Badge",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-badge--default",
            "hasPlay": false
          },
          {
            "name": "Accent",
            "id": "components-badge--accent",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "components-badge--success",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-badge--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-badge--danger",
            "hasPlay": false
          },
          {
            "name": "WithoutDot",
            "id": "components-badge--without-dot",
            "hasPlay": false
          },
          {
            "name": "AllTones",
            "id": "components-badge--all-tones",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-badge--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-badge--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Badge/Badge.md",
        "summary": "A pill with a short status label. The text always states the status; the tone and the dot only reinforce it.",
        "whenToUse": [
          "Show the state of a record in a list, a table row or a card header: Draft, In progress, Shipped, Failed.",
          "Keep the text short (one to three words, sentence case)."
        ],
        "whenNotToUse": [
          "For a count or a filter value: use `Tag`.",
          "For a message the user must read or act on: use `Alert`.",
          "For an action: use `Button`. A Badge is never interactive."
        ],
        "keyboard": [],
        "examples": [
          "<Badge tone=\"success\">Shipped</Badge>\n<Badge tone=\"danger\">Failed</Badge>\n<Badge dot={false}>Draft</Badge>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Show the state of a record in a list, a table row or a card header: Draft, In progress, Shipped, Failed.\n- Keep the text short (one to three words, sentence case)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a count or a filter value: use `Tag`.\n- For a message the user must read or act on: use `Alert`.\n- For an action: use `Button`. A Badge is never interactive."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\n[ dot ] Text\n```\n\n- `dot` (optional, decorative, `aria-hidden`).\n- `text`: required, the status."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop        | Type                                                          | Default     | Notes                                                                            |\n| ----------- | ------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------- |\n| `tone`      | `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'neutral'` | `accent` open or in progress, `success` done, `warning` pending, `danger` failed |\n| `dot`       | `boolean`                                                     | `true`      | Decorative dot before the text                                                   |\n| `children`  | `ReactNode`                                                   | required    | The status text                                                                  |\n| `className` | `string`                                                      |             | Merged with the component class                                                  |\n\nOther `span` attributes and `ref` are passed to the root element."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A Badge is plain text in a `span`: there is no role and no keyboard behavior.\n- The dot is `aria-hidden`; the tone is never the only carrier of meaning, because the text names the status (WCAG 1.4.1).\n- Text on every soft tint keeps at least 4.5:1 contrast (checked in the browser test by compositing the real computed colors).\n- In forced-colors mode the Badge gets a border and the dot takes the text color."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Sentence case, no punctuation, no emoji: \"In progress\", not \"IN PROGRESS!\".\n- Name the state, not the action: \"Shipped\", not \"Ship\".\n- If the status can be unknown, say so with text (\"Unknown\"), do not omit the Badge."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Badge tone=\"success\">Shipped</Badge>\n<Badge tone=\"danger\">Failed</Badge>\n<Badge dot={false}>Draft</Badge>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Tones use only the semantic tokens (`--ds-*-soft`). The handoff built the danger tint with a different `color-mix` (14%) than the token (10%); the token is now the single source.\n- Height, dot size and padding are component tokens (`--ds-badge-*`).\n- The dot is explicitly decorative; `children` is required.\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Tag",
            "reason": "For a count or a filter value: use `Tag`."
          },
          {
            "name": "Alert",
            "reason": "For a message the user must read or act on: use `Alert`."
          },
          {
            "name": "Button",
            "reason": "For an action: use `Button`. A Badge is never interactive."
          }
        ],
        "uses": [],
        "usedBy": ["AgentCard", "ApprovalCard"]
      }
    },
    {
      "name": "Banner",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Banner } from 'loui-ds';",
      "folder": "src/components/Banner",
      "sourceFile": "src/components/Banner/Banner.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A persistent, full-width message for a whole page or section: maintenance, a limited account,\nan offline mode. It stays until the condition ends or the user dismisses it.\nThe icon is decorative and a visually hidden prefix names the tone for screen readers.",
      "props": [
        {
          "name": "tone",
          "type": "'info' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'info'",
          "description": "Kind of message: `info` (neutral, default), `success`, `warning` or `danger`.",
          "values": ["info", "success", "warning", "danger"]
        },
        {
          "name": "title",
          "type": "ReactNode",
          "required": false,
          "description": "Short heading in one line: what is going on. Plain text, not a document heading."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The detail: why, and what to do. Keep it to one or two sentences."
        },
        {
          "name": "action",
          "type": "ReactNode",
          "required": false,
          "description": "A single action next to the text, usually a link or a small `Button` (\"Update card\")."
        },
        {
          "name": "onDismiss",
          "type": "() => void",
          "required": false,
          "description": "Shows a dismiss button that calls this. The banner is removed by the parent."
        },
        {
          "name": "urgent",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "The banner appears while the user is working and interrupts the task, so it must be announced\nimmediately (`role=\"alert\"`, assertive). Only for a failure the user must know about right\nnow. A banner that is on the page from the start never needs it."
        },
        {
          "name": "announce",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Whether the banner is a live region (`role=\"status\"`, polite). Default `false`, because a\nbanner is usually part of the page when it loads and a live region that never changes is\nnoise. Set `true` when the banner is added to the page after load (a lost connection that\nis restored, a session that is about to expire). Ignored when `urgent`."
        },
        {
          "name": "messages",
          "type": "Partial<{ dismiss: string; info: string; success: string; warning: string; danger: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "tone": ["info", "success", "warning", "danger"]
      },
      "messages": {
        "providerKey": "Banner",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "dismiss",
            "kind": "string",
            "default": "Dismiss banner",
            "description": "Name of the dismiss button."
          },
          {
            "key": "info",
            "kind": "string",
            "default": "Information",
            "description": "Visually hidden prefix that tells screen reader users the kind of banner; color and icon alone cannot."
          },
          {
            "key": "success",
            "kind": "string",
            "default": "Success"
          },
          {
            "key": "warning",
            "kind": "string",
            "default": "Warning"
          },
          {
            "key": "danger",
            "kind": "string",
            "default": "Error"
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-banner-content-basis",
        "--ds-banner-danger-bg",
        "--ds-banner-danger-border",
        "--ds-banner-success-bg",
        "--ds-banner-success-border",
        "--ds-banner-warning-bg",
        "--ds-banner-warning-border",
        "--ds-bg",
        "--ds-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Banner",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-banner--default",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "components-banner--success",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-banner--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-banner--danger",
            "hasPlay": false
          },
          {
            "name": "Urgent",
            "id": "components-banner--urgent",
            "description": "Urgent banners use `role=\"alert\"` and interrupt screen readers. Use them only when they appear while the user is working.",
            "hasPlay": false
          },
          {
            "name": "Announced",
            "id": "components-banner--announced",
            "description": "A banner added after the page loaded is a polite live region.",
            "hasPlay": false
          },
          {
            "name": "TitleOnly",
            "id": "components-banner--title-only",
            "hasPlay": false
          },
          {
            "name": "TextOnly",
            "id": "components-banner--text-only",
            "hasPlay": false
          },
          {
            "name": "Dismissible",
            "id": "components-banner--dismissible",
            "description": "Tab to the dismiss button, press Enter.",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-banner--long-text",
            "hasPlay": false
          },
          {
            "name": "AllTones",
            "id": "components-banner--all-tones",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-banner--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Banner/Banner.md",
        "summary": "A persistent, full-width message for a whole page or section: maintenance, a limited account, an offline mode. It has a tone, an icon, an optional title, one action and an optional dismiss button.",
        "whenToUse": [
          "A condition that affects the whole page or area and lasts until it is resolved or dismissed: scheduled maintenance, a trial ending, a read-only mode.",
          "Place it at the top of the page or section it applies to, above the content."
        ],
        "whenNotToUse": [
          "Feedback about something that just happened and goes away by itself (\"Order #1051 created\"): use `Toast`.",
          "A message about one piece of content, inside it (a form, a card): use `Alert`.",
          "A field error: use the field's error message.",
          "A confirmation that needs a decision: use `Dialog`.",
          "The status of a record: use `Badge`."
        ],
        "keyboard": [],
        "examples": [
          "<Banner title=\"Scheduled maintenance\">Sunday, 02:00 to 03:00 UTC.</Banner>\n\n<Banner\n  tone=\"warning\"\n  title=\"Trial ends in 3 days\"\n  action={<Button size=\"sm\">Choose plan</Button>}\n  onDismiss={() => setHidden(true)}\n>\n  Choose a plan to keep your data.\n</Banner>\n\n<Banner tone=\"danger\" urgent title=\"Connection lost\">\n  Changes are not being saved.\n</Banner>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A condition that affects the whole page or area and lasts until it is resolved or dismissed: scheduled maintenance, a trial ending, a read-only mode.\n- Place it at the top of the page or section it applies to, above the content."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Feedback about something that just happened and goes away by itself (\"Order #1051 created\"): use `Toast`.\n- A message about one piece of content, inside it (a form, a card): use `Alert`.\n- A field error: use the field's error message.\n- A confirmation that needs a decision: use `Dialog`.\n- The status of a record: use `Badge`.\n\n### Banner, Alert and Toast\n\n|             | Banner                                             | Alert                                   | Toast                                |\n| ----------- | -------------------------------------------------- | --------------------------------------- | ------------------------------------ |\n| Scope       | Page or section                                    | Inline, next to the content it explains | The app, in a corner                 |\n| Lifetime    | Persistent, until resolved or dismissed            | Persistent while relevant               | Transient, auto-dismisses (pausable) |\n| Shape       | Full width, flat, top and bottom border only       | Rounded card                            | Elevated overlay                     |\n| Live region | None by default (it is on the page from the start) | Polite status by default                | Always announced                     |"
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=alert | status | none]  (tinted surface, 1px tinted top and bottom border, full width)\n  Icon (decorative)  info | success | warning (triangle) | danger\n  content\n    VisuallyHidden   \"Information\" | \"Success\" | \"Warning\" | \"Error\"\n    title            optional, 14/22, weight 500\n    children         text-2, on the same line as the title when it fits\n  actions            action slot · dismiss IconButton\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop        | Type                                           | Default  | Notes                                                                 |\n| ----------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------- |\n| `tone`      | `'info' \\| 'success' \\| 'warning' \\| 'danger'` | `'info'` | `info` is neutral; the others tint border and background              |\n| `title`     | `ReactNode`                                    |          | What is going on, one line                                            |\n| `children`  | `ReactNode`                                    |          | Why, and what to do                                                   |\n| `action`    | `ReactNode`                                    |          | One link or small `Button`                                            |\n| `onDismiss` | `() => void`                                   |          | Shows the dismiss button; the parent removes the banner               |\n| `urgent`    | `boolean`                                      | `false`  | `role=\"alert\"` (assertive). Only when it appears while the user works |\n| `announce`  | `boolean`                                      | `false`  | `role=\"status\"` (polite), for a banner added after the page loaded    |\n| `messages`  | `{ dismiss, info, success, warning, danger }`  |          | Per-instance overrides                                                |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "accessibility-role-decisions",
            "heading": "Accessibility: role decisions",
            "content": "- **No role by default.** A banner is normally part of the page when it loads; screen readers read it in page order. A live region that never changes is noise.\n- **`announce` gives `role=\"status\"` (polite).** Use it when the banner is inserted after load (connection restored, session about to expire). Render the banner when the event happens and keep it mounted; do not render it hidden and unhide it.\n- **`urgent` gives `role=\"alert\"` (assertive).** Use it only for a `danger` or `warning` banner that appears dynamically and needs attention now (connection lost, session expired). A `danger` or `warning` banner that is on the page from the start must not be `urgent`: the tone does not imply urgency.\n- **The tone is not only color and icon.** The icon is `aria-hidden`; a visually hidden prefix (\"Error\", \"Warning\", \"Success\", \"Information\", from messages) names the kind of banner.\n- The **dismiss button** is an `IconButton` named \"Dismiss banner\" (messages), reachable by Tab and operated with Enter or Space. After dismissing, move focus somewhere sensible if the banner held it.\n- Text contrast on the tinted backgrounds is at least 4.5:1 (checked in the browser test with the real computed colors). The tints are component tokens (`--ds-banner-*`).\n- Reflow: the text and the actions wrap; at 320 px the action moves below the text."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: what is going on, sentence case, no exclamation marks (\"Trial ends in 3 days\").\n- Text: why and what to do, one or two sentences.\n- The action is a verb plus object (\"Choose plan\"), not \"Click here\".\n- Do not stack more than one banner per page area."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Banner title=\"Scheduled maintenance\">Sunday, 02:00 to 03:00 UTC.</Banner>\n\n<Banner\n  tone=\"warning\"\n  title=\"Trial ends in 3 days\"\n  action={<Button size=\"sm\">Choose plan</Button>}\n  onDismiss={() => setHidden(true)}\n>\n  Choose a plan to keep your data.\n</Banner>\n\n<Banner tone=\"danger\" urgent title=\"Connection lost\">\n  Changes are not being saved.\n</Banner>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Alert",
            "reason": "A message about one piece of content, inside it (a form, a card): use `Alert`."
          },
          {
            "name": "Dialog",
            "reason": "A confirmation that needs a decision: use `Dialog`."
          },
          {
            "name": "Badge",
            "reason": "The status of a record: use `Badge`."
          }
        ],
        "uses": ["Icon", "IconButton", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Breadcrumb",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Breadcrumb } from 'loui-ds';",
      "folder": "src/components/Breadcrumb",
      "sourceFile": "src/components/Breadcrumb/Breadcrumb.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The path to the current page as an ordered list of links inside a `nav` landmark. The\nseparators are decorative (`aria-hidden`), so a screen reader hears a list of N items. Long\nlabels are truncated with an ellipsis (the current page keeps the most room); collapsing the\nmiddle of a long trail into a menu is not supported yet.",
      "props": [
        {
          "name": "items",
          "type": "readonly BreadcrumbItem[]",
          "required": true,
          "description": "The trail from the root to the current page. The last item is the current page: it gets\n`aria-current=\"page\"` and is not a link."
        },
        {
          "name": "messages",
          "type": "Partial<{ label: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Breadcrumb",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "label",
            "kind": "string",
            "default": "Breadcrumb",
            "description": "Accessible name of the breadcrumb navigation landmark."
          }
        ]
      },
      "cssVariables": [
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-radius-sm",
        "--ds-space-2",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Breadcrumb",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-breadcrumb--default",
            "hasPlay": true
          },
          {
            "name": "SingleItem",
            "id": "components-breadcrumb--single-item",
            "hasPlay": true
          },
          {
            "name": "KeyboardNavigation",
            "id": "components-breadcrumb--keyboard-navigation",
            "description": "Tab visits each ancestor link in order; the current page is plain text.",
            "hasPlay": true
          },
          {
            "name": "LongTrail",
            "id": "components-breadcrumb--long-trail",
            "hasPlay": false
          },
          {
            "name": "RouterNavigation",
            "id": "components-breadcrumb--router-navigation",
            "hasPlay": true
          },
          {
            "name": "Portuguese",
            "id": "components-breadcrumb--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-breadcrumb--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-breadcrumb--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Breadcrumb/Breadcrumb.md",
        "summary": "The path from the root to the current page, as an ordered list of links inside a `nav` landmark. Built on React Aria `Breadcrumbs` and `Breadcrumb`.\n\n- Handoff source: `components/layout/Breadcrumb.jsx` (TODO `CMP-LAY-03`).",
        "whenToUse": [
          "Deep hierarchies (Orders > Northwind Traders > ORD-1048) where people need to go up a level.",
          "Inside the `TopBar` instead of a title."
        ],
        "whenNotToUse": [
          "For the steps of a process: use a stepper (`StatusSteps`).",
          "For the main navigation: use `Sidebar` and `NavItem`.",
          "For a flat app with two levels: a page title and a \"Back\" link are enough."
        ],
        "keyboard": [],
        "examples": [
          "<Breadcrumb\n  items={[\n    { label: 'Orders', href: '/orders' },\n    { label: 'Northwind Traders', href: '/customers/northwind' },\n    { label: 'ORD-1048' },\n  ]}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Deep hierarchies (Orders > Northwind Traders > ORD-1048) where people need to go up a level.\n- Inside the `TopBar` instead of a title."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For the steps of a process: use a stepper (`StatusSteps`).\n- For the main navigation: use `Sidebar` and `NavItem`.\n- For a flat app with two levels: a page title and a \"Back\" link are enough."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                                 | Notes                                                                      |\n| ------------ | ------------------------------------ | -------------------------------------------------------------------------- |\n| `items`      | `{ label: string; href?: string }[]` | Root first. The last item is the current page: plain text, `href` ignored. |\n| `aria-label` | `string`                             | Overrides the landmark name (\"Breadcrumb\" from the catalog).               |\n| `messages`   | `{ label?: string }`                 | Per-instance message override (ADR-0005).                                  |\n\n`ref` points to the `<nav>`; `className` and other `nav` attributes pass through. Links go through the router set on `DSProvider`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `nav` named \"Breadcrumb\" containing an `ol`: a screen reader announces a list and its length.\n- The current page is `aria-current=\"page\"` and is not a link.\n- Separators are decorative icons with `aria-hidden`, so they are never read. They mirror in right-to-left locales.\n- Tab visits each ancestor link in order. Link text is `--ds-text-2` and turns ink with an underline on hover.\n- Truncation: ancestors shrink first (three times faster than the current page) and show an ellipsis; labels are not wrapped. Give the full text to users some other way when it matters (the page title).\n- Collapsing the middle of a long trail into a menu (\"...\") is not supported yet; it needs the `Menu` component (P3)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are the page titles, short, in sentence case. The last label matches the page `h1`.\n- Do not include \"Home\" unless it is a real destination people use."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Breadcrumb\n  items={[\n    { label: 'Orders', href: '/orders' },\n    { label: 'Northwind Traders', href: '/customers/northwind' },\n    { label: 'ORD-1048' },\n  ]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ordered list inside a named `nav` (the handoff was a flex of loose links and `/` spans).\n- Separators are icons, hidden from assistive technology; the current page uses `aria-current` and is not a link.\n- Real routed links (`href` through `DSProvider`) instead of `href=\"#\"` plus `onClick`. Hover color is CSS.\n- The landmark name comes from the message catalog (English and pt-BR)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "StatusSteps",
            "reason": "For the steps of a process: use a stepper (`StatusSteps`)."
          },
          {
            "name": "Sidebar",
            "reason": "For the main navigation: use `Sidebar` and `NavItem`."
          },
          {
            "name": "NavItem",
            "reason": "For the main navigation: use `Sidebar` and `NavItem`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": ["TopBar"]
      }
    },
    {
      "name": "Button",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Button } from 'loui-ds';",
      "folder": "src/components/Button",
      "sourceFile": "src/components/Button/Button.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An action button built on React Aria's `Button`.\n\nUse one `primary` button per screen. While `isPending` the button keeps its width and its\nlabel (dimmed), shows a spinner before the label (over the `leading` icon when there is one), stays focusable, sets `aria-disabled` and ignores presses, so it can be explained\nby a tooltip and focus is never lost.",
      "props": [
        {
          "name": "variant",
          "type": "'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-solid'",
          "required": false,
          "default": "'secondary'",
          "description": "Visual style.\n- `primary`: the single main action of the screen (light ink on dark). One per screen.\n- `secondary`: default for other actions.\n- `ghost`: tertiary, low emphasis actions.\n- `danger`: destructive action that is not yet confirmed.\n- `danger-solid`: the confirm button of a destructive dialog.",
          "values": ["primary", "secondary", "ghost", "danger", "danger-solid"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Height token: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Stretch to the width of the container."
        },
        {
          "name": "leading",
          "type": "ReactNode",
          "required": false,
          "description": "Node before the label, usually `<Icon size=\"sm\" />`. Decorative; the label names the button."
        },
        {
          "name": "trailing",
          "type": "ReactNode",
          "required": false,
          "description": "Node after the label, for example a `Kbd` shortcut hint."
        },
        {
          "name": "shortcut",
          "type": "readonly string[]",
          "required": false,
          "description": "Keyboard shortcut of the action, as `Kbd` keys (`['Mod', 'K']`). Renders the hint after the\nlabel (before `trailing`), hidden from assistive technology, and sets `aria-keyshortcuts` so\nthe shortcut is announced once and is not part of the accessible name. Bind the key itself\nwith `useHotkeys`."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Label: a short verb plus object in sentence case (\"Create order\")."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLButtonElement>",
          "required": false,
          "description": "Ref to the underlying `<button>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaBaseButtonProps",
          "domGeneric": false,
          "count": 16,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-current",
              "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"",
              "required": false
            },
            {
              "name": "aria-disabled",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-expanded",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "aria-pressed",
              "type": "boolean | \"true\" | \"false\" | \"mixed\"",
              "required": false
            },
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "formAction",
              "type": "string | ((formData: FormData) => void | Promise<void>)",
              "required": false
            },
            {
              "name": "formEncType",
              "type": "string",
              "required": false
            },
            {
              "name": "formMethod",
              "type": "string",
              "required": false
            },
            {
              "name": "formNoValidate",
              "type": "boolean",
              "required": false
            },
            {
              "name": "formTarget",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            },
            {
              "name": "preventFocusOnPress",
              "type": "boolean",
              "required": false
            },
            {
              "name": "type",
              "type": "'button' | 'submit' | 'reset'",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isPending",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"button\", ButtonRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {
        "variant": ["primary", "secondary", "ghost", "danger", "danger-solid"],
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-button-danger-solid-hover-brightness",
        "--ds-button-hover-brightness",
        "--ds-button-pending-label-opacity",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-danger-line",
        "--ds-danger-soft",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-icon-md",
        "--ds-icon-sm",
        "--ds-lh-body",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-primary-bg",
        "--ds-primary-fg",
        "--ds-primary-fg-muted",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-space-5",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-inverse",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Button",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-button--default",
            "hasPlay": false
          },
          {
            "name": "Primary",
            "id": "components-button--primary",
            "description": "Use one `primary` button per screen, at the top right of the view.",
            "hasPlay": false
          },
          {
            "name": "Secondary",
            "id": "components-button--secondary",
            "hasPlay": false
          },
          {
            "name": "Ghost",
            "id": "components-button--ghost",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-button--danger",
            "hasPlay": false
          },
          {
            "name": "DangerSolid",
            "id": "components-button--danger-solid",
            "description": "The confirm button of a destructive dialog.",
            "hasPlay": false
          },
          {
            "name": "Variants",
            "id": "components-button--variants",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-button--sizes",
            "hasPlay": false
          },
          {
            "name": "WithLeadingIcon",
            "id": "components-button--with-leading-icon",
            "hasPlay": false
          },
          {
            "name": "WithTrailingShortcut",
            "id": "components-button--with-trailing-shortcut",
            "hasPlay": false
          },
          {
            "name": "FullWidth",
            "id": "components-button--full-width",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-button--disabled",
            "hasPlay": true
          },
          {
            "name": "Pending",
            "id": "components-button--pending",
            "description": "The button keeps its width and its (dimmed) label, shows a spinner, stays focusable; presses are ignored.",
            "hasPlay": true
          },
          {
            "name": "PendingVariants",
            "id": "components-button--pending-variants",
            "hasPlay": false
          },
          {
            "name": "PendingWithLeadingAndSizes",
            "id": "components-button--pending-with-leading-and-sizes",
            "description": "Pending with a leading icon (the spinner replaces the icon in place) and in every size.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-button--long-text",
            "description": "Long labels wrap instead of overflowing their container.",
            "hasPlay": false
          },
          {
            "name": "Rtl",
            "id": "components-button--rtl",
            "description": "Right-to-left layout: icons and labels swap sides through logical properties.",
            "hasPlay": false
          },
          {
            "name": "KeyboardActivation",
            "id": "components-button--keyboard-activation",
            "description": "Keyboard: Tab focuses, Enter and Space press.",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-button--focused",
            "description": "Focus state shown through a play function.",
            "hasPlay": true
          },
          {
            "name": "Hovered",
            "id": "components-button--hovered",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Button/Button.md",
        "summary": "The control for an action. Built on React Aria's `Button`, styled with tokens and `data-*` state attributes.\n\n- Handoff source: `components/forms/Button` (CMP-FRM-01).",
        "whenToUse": [
          "To run an action in place: \"Create order\", \"Mark as shipped\".",
          "`danger` for a destructive action that opens a confirmation, `danger-solid` for the confirm button inside that dialog (initial focus stays on Cancel).",
          "`ghost` for low-emphasis actions such as \"View all\" in a card header."
        ],
        "whenNotToUse": [
          "To navigate to another page: use `Link` (styled as text) so the browser keeps link semantics.",
          "For an icon-only action: use `IconButton`, which requires an accessible name.",
          "For a two-state toggle: use a toggle component."
        ],
        "keyboard": ["Keys: `Tab` focuses, `Enter` and `Space` press (RAC). One tab stop."],
        "examples": [
          "<Button variant=\"primary\" onPress={save}>Save changes</Button>\n\n<Button leading={<Icon icon={PlusIcon} size=\"sm\" />}>Create order</Button>\n\n<Button variant=\"primary\" isPending={isSaving} onPress={save}>\n  Save changes\n</Button>\n\n<Button trailing={<span className=\"ds-mono\">⌘K</span>}>Search</Button>"
        ],
        "sections": [
          {
            "id": "one-primary-per-screen",
            "heading": "One primary per screen",
            "content": "Use **one `primary` button per screen**, at the top right of the view, ideally with a keyboard shortcut. Primary is \"ink for action\": light on dark, never the accent color. Every other action is `secondary` or `ghost`. A second primary button means the screen has two main jobs: split the screen or demote one action."
          },
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- To run an action in place: \"Create order\", \"Mark as shipped\".\n- `danger` for a destructive action that opens a confirmation, `danger-solid` for the confirm button inside that dialog (initial focus stays on Cancel).\n- `ghost` for low-emphasis actions such as \"View all\" in a card header."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To navigate to another page: use `Link` (styled as text) so the browser keeps link semantics.\n- For an icon-only action: use `IconButton`, which requires an accessible name.\n- For a two-state toggle: use a toggle component."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "`[leading] label [trailing]` inside one native `<button>`. When pending, the label stays visible, dimmed with `--ds-button-pending-label-opacity` (0.65), and a `Spinner` takes the leading position: it sits over the `leading` icon when there is one, otherwise before the label, using the button padding (the content is pulled outward by half of spinner plus gap on each side), so the width never changes. The dimmed label is a busy state, not a disabled one; the label keeps meaning, so users still see what the action is doing (for example \"Connecting\")."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                                                                | Default       | Notes                                                                                                   |\n| ------------ | ------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- |\n| `variant`    | `'primary' \\| 'secondary' \\| 'ghost' \\| 'danger' \\| 'danger-solid'` | `'secondary'` | See above.                                                                                              |\n| `size`       | `'sm' \\| 'md' \\| 'lg'`                                              | `'md'`        | 32, 40, 48 px (`--ds-control-h-*`). Taller if the label wraps.                                          |\n| `fullWidth`  | `boolean`                                                           | `false`       | Fills the container.                                                                                    |\n| `leading`    | `ReactNode`                                                         | none          | Usually `<Icon size=\"sm\" />`. Decorative.                                                               |\n| `trailing`   | `ReactNode`                                                         | none          | For example a shortcut hint in mono.                                                                    |\n| `shortcut`   | `readonly string[]`                                                 | none          | Shortcut keys (`['Mod', 'K']`): renders a `Kbd` hint, sets `aria-keyshortcuts`; not part of the name.   |\n| `isPending`  | `boolean`                                                           | `false`       | Busy: dimmed label plus spinner, `aria-disabled`, presses and form submission ignored, stays focusable. |\n| `isDisabled` | `boolean`                                                           | `false`       | Native `disabled`: not focusable, 35% opacity.                                                          |\n| `onPress`    | `(e: PressEvent) => void`                                           | none          | React Aria press event (mouse, touch, keyboard, screen reader). Prefer it over `onClick`.               |\n| `type`       | `'button' \\| 'submit' \\| 'reset'`                                   | `'button'`    | Stays `button` so a button never submits a form by accident.                                            |\n| `className`  | `string`                                                            | none          | Merged with the component class.                                                                        |\n\nOther React Aria `Button` props (`form*`, `autoFocus`, `aria-*`, `onHoverStart`, ...) and `ref` pass through. `style` and render-prop children are not accepted."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A `Kbd` inside a button (through `shortcut` or `trailing`) is hidden from assistive technology, so the accessible name is only the label (\"Settings\", not \"Settings Escape\"). `shortcut` also sets `aria-keyshortcuts` (`Control+K`, `Meta+K` on Apple).\n\n- A real `<button>`; the name is its visible label.\n- Keys: `Tab` focuses, `Enter` and `Space` press (RAC). One tab stop.\n- Focus: accent border plus a 3px ring through `data-focus-visible`, a transparent outline that forced-colors mode repaints as `Highlight`. Mouse clicks do not show the ring.\n- Pending: the button stays in the tab order (focus is not lost while waiting) with `aria-disabled=\"true\"` (React Aria does not set `aria-busy`). React Aria announces the busy state when the focused button becomes pending and again when it finishes. The spinner is named \"Loading\" (pt-BR \"Carregando\", from the `Spinner` messages) and sits inside the button so it is part of the button's content.\n- Disabled: removed from the tab order. Explain _why_ in nearby text rather than relying on a disabled button alone.\n- Targets: at least 32px high, above the 24px minimum of WCAG 2.5.8.\n- Contrast: label colors meet 4.5:1 on their backgrounds in every variant; disabled controls are exempt.\n- Reduced motion: the only transition is a 150ms background color change, which the global reduced-motion rule neutralizes. The pending spinner turns into a static ring."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Verb plus object, sentence case: \"Create order\", \"Retry charge\". Never a bare \"OK\" or \"Send\".\n- Destructive buttons name the action: \"Delete order\".\n- No exclamation marks, no emoji, no decorative ellipsis. Use \"...\" only when the action opens something that needs more input (\"Move to...\")."
          },
          {
            "id": "states",
            "heading": "States",
            "content": "Hover (one surface step lighter; primary dims with `--ds-button-hover-brightness`), focus-visible, disabled, pending. Hover, focus and pending are CSS (`data-hovered`, `data-focus-visible`, `data-pending`), never React state. There is no press animation."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Button variant=\"primary\" onPress={save}>Save changes</Button>\n\n<Button leading={<Icon icon={PlusIcon} size=\"sm\" />}>Create order</Button>\n\n<Button variant=\"primary\" isPending={isSaving} onPress={save}>\n  Save changes\n</Button>\n\n<Button trailing={<span className=\"ds-mono\">⌘K</span>}>Search</Button>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Hover is CSS, not JS state.\n- `isPending` replaces the \"disabled plus spinner in leading\" recipe: width-stable, focusable, announced, and it ignores presses.\n- Sizes are 32 / 40 / 48 px. The handoff `Button.prompt.md` said 28 / 32 / 40; the code and the tokens win.\n- `onPress` (React Aria) is the primary event; `disabled` is now `isDisabled`.\n- `style` was removed; the primary hover brightness (0.88) and the danger-solid one (0.9) are component tokens.\n- Wrapping instead of `white-space: nowrap`, so long labels never overflow."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Link",
            "reason": "To navigate to another page: use `Link` (styled as text) so the browser keeps link semantics."
          },
          {
            "name": "IconButton",
            "reason": "For an icon-only action: use `IconButton`, which requires an accessible name."
          }
        ],
        "uses": ["Kbd", "Spinner"],
        "usedBy": [
          "ApprovalCard",
          "ChatPanel",
          "ConfirmDialog",
          "DataTable",
          "DatePicker",
          "FileUpload",
          "InlineConfirm",
          "ResponseFeedback",
          "ShortcutsHelp",
          "ToastRegion",
          "Tour"
        ]
      }
    },
    {
      "name": "Calendar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Calendar } from 'loui-ds';",
      "folder": "src/components/DatePicker",
      "sourceFile": "src/components/DatePicker/Calendar.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A month calendar (React Aria `Calendar`, an ARIA grid). The value is a `YYYY-MM-DD` string.\nMonths, weekday names and the first day of the week follow the `DSProvider` locale.\n\nKeys: arrows move by day and week, `PageUp` / `PageDown` by month, `Shift+PageUp` /\n`Shift+PageDown` by year, `Home` / `End` to the start / end of the week, `Enter` / `Space`\nselect. Each day button is named with its full date (\"Wednesday, October 28, 2026\"); today is\nringed and named \"Today\".",
      "props": [
        {
          "name": "min",
          "type": "string",
          "required": false,
          "description": "Earliest selectable date (`YYYY-MM-DD`). Earlier days are disabled."
        },
        {
          "name": "max",
          "type": "string",
          "required": false,
          "description": "Latest selectable date (`YYYY-MM-DD`). Later days are disabled."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the whole calendar: days cannot be focused or selected."
        },
        {
          "name": "isReadOnly",
          "type": "boolean",
          "required": false,
          "description": "The calendar can be navigated and focused but the value cannot change."
        },
        {
          "name": "autoFocus",
          "type": "boolean",
          "required": false,
          "description": "Moves focus into the grid on mount."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "messages",
          "type": "Partial<{ previousMonth: string; nextMonth: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `previousMonth`, `nextMonth`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": ""
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the calendar grid. Required unless `aria-labelledby` is given.\nAccessible name of the calendar grid."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the calendar. Required unless `aria-label` is given.\nId of the element that names the calendar."
        },
        {
          "name": "value",
          "type": "string | null",
          "required": false,
          "description": "The selected date as `YYYY-MM-DD`, or `null` for none. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "string | null",
          "required": false,
          "description": "The initially selected date when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((value: string) => void)",
          "required": false,
          "description": "Called with the picked date as `YYYY-MM-DD`."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "Calendar",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "today",
            "kind": "string",
            "default": "Today",
            "description": "Footer button that picks today's date."
          },
          {
            "key": "clear",
            "kind": "string",
            "default": "Clear",
            "description": "Footer button that empties the field."
          },
          {
            "key": "openCalendar",
            "kind": "string",
            "default": "Open calendar",
            "description": "Accessible name of the button that opens the calendar."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-calendar-cell-size",
        "--ds-calendar-today-ring-width",
        "--ds-calendar-weekday-height",
        "--ds-field-control-padding-inline",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-text-inverse",
        "--ds-weight-medium",
        "--ds-weight-semibold",
        "--ds-z-dropdown"
      ],
      "stories": {
        "title": "Components/DatePicker",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-date-picker--default",
            "hasPlay": true
          },
          {
            "name": "CalendarKeyboard",
            "id": "components-date-picker--calendar-keyboard",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-date-picker--open",
            "description": "The calendar open under the field, for the visual review (TEST-020).",
            "hasPlay": true
          },
          {
            "name": "PickDate",
            "id": "components-date-picker--pick-date",
            "description": "Pointer path: open the calendar, click a day, the popover closes and the field shows the date. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-date-picker--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "WithValue",
            "id": "components-date-picker--with-value",
            "hasPlay": false
          },
          {
            "name": "MinAndMax",
            "id": "components-date-picker--min-and-max",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-date-picker--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-date-picker--invalid",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-date-picker--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-date-picker--read-only",
            "hasPlay": false
          },
          {
            "name": "Brazilian",
            "id": "components-date-picker--brazilian",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-date-picker--sizes",
            "hasPlay": false
          },
          {
            "name": "StandaloneCalendar",
            "id": "components-date-picker--standalone-calendar",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-date-picker--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/DatePicker/DatePicker.md",
        "summary": "`DatePicker` is a date field (typed segments) with a calendar popover and \"Today\" / \"Clear\" buttons. `Calendar` is the standalone month grid. Built on React Aria `DatePicker`, `DateInput`, `DateSegment`, `Calendar`, `CalendarGrid`, `CalendarCell`, `Popover`. Task: CMP-INP-02.",
        "whenToUse": [
          "A single calendar date: delivery, due date, birthday.",
          "`Calendar` alone for inline planners and for composing your own picker."
        ],
        "whenNotToUse": [
          "A date range: not supported yet (range picker is P2).",
          "A time of day or a timestamp: the value has no time and no time zone.",
          "Dates years away from now: typing the segments is faster than the grid."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A single calendar date: delivery, due date, birthday.\n- `Calendar` alone for inline planners and for composing your own picker."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A date range: not supported yet (range picker is P2).\n- A time of day or a timestamp: the value has no time and no time zone.\n- Dates years away from now: typing the segments is faster than the grid."
          },
          {
            "id": "value-contract",
            "heading": "Value contract",
            "content": "`value`, `defaultValue`, `onChange`, `min` and `max` are **ISO strings `YYYY-MM-DD`** (the `ISODate` type of `src/utils/date`), never `Date`. Conversion to React Aria's `CalendarDate` happens inside the component (`@internationalized/date`, see below), so the day cannot shift with the viewer's time zone. Invalid strings are treated as \"no date\". `onChange` receives a string only once every segment is filled, or `null` from the Clear button; an incomplete date is not reported. The form receives the ISO string under `name`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                                                 | Notes                                                                                                                           |\n| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`                                                           | One is required.                                                                                                                |\n| `value` / `defaultValue` / `onChange`                                                                | ISO string or `null`.                                                                                                           |\n| `min`, `max`                                                                                         | ISO strings. Outside days are disabled in the grid; typed dates outside are invalid. \"Today\" is disabled when today is outside. |\n| `description`, `errorMessage`, `size`, `name`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | As in `Select`.                                                                                                                 |\n| `fullWidth`                                                                                          | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.                              |\n| `messages`                                                                                           | `today`, `clear`, `openCalendar`, `previousMonth`, `nextMonth`, `requiredMark`.                                                 |\n\n`Calendar` takes `value`, `defaultValue`, `onChange(iso)`, `min`, `max`, `isDisabled`, `isReadOnly`, `autoFocus`, `messages`, and requires `aria-label` or `aria-labelledby`."
          },
          {
            "id": "locale",
            "heading": "Locale",
            "content": "Segment order and separators, placeholders (`dd/mm/aaaa`), month names, weekday letters and the first day of the week come from the `DSProvider` locale through React Aria; there are no hard-coded names. Only the button texts are in the message catalog (`Calendar` key for the month buttons, `DatePicker` for the rest)."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key (calendar grid)               | Behavior                                                      |\n| --------------------------------- | ------------------------------------------------------------- |\n| Arrows                            | Previous / next day, previous / next week                     |\n| `PageUp` / `PageDown`             | Previous / next month                                         |\n| `Shift+PageUp` / `Shift+PageDown` | Previous / next year                                          |\n| `Home` / `End`                    | First / last day of the **month** (React Aria; APG says week) |\n| `Enter` / `Space`                 | Select the focused day                                        |\n| `Escape`                          | Close the popover, focus returns to the button                |\n\nIn the field: digits fill the segment and move to the next; arrows change a segment's value; `Backspace` clears a segment; `Tab` moves between segments. Each day is named with its full date (\"Wednesday, October 28, 2026\", \"Today, ...\"), so the grid is usable without sight. Today has a ring (`--ds-text-3`), the selected day an accent fill with `--ds-text-inverse`; disabled days are dimmed and `aria-disabled`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the date's role (\"Delivery date\"). Errors say how to fix: \"Choose a date after today.\""
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<DatePicker\n  label=\"Delivery date\"\n  name=\"delivery\"\n  min=\"2026-10-30\"\n  value={date}\n  onChange={setDate}\n/>\n```"
          },
          {
            "id": "dependency-note",
            "heading": "Dependency note",
            "content": "The component imports `@internationalized/date`, which React Aria already depends on. It is not a direct dependency of this package yet, so under pnpm's strict layout it must be added to `package.json` (`pnpm add @internationalized/date@^3.12.4`, already in the lockfile as a transitive dependency)."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Typed segments with locale-aware placeholders instead of a button showing a `toLocaleDateString` string; calendar is an ARIA grid with full keyboard navigation (the handoff used plain buttons).\n- Month and weekday names come from `Intl` through the locale, not hard-coded Portuguese arrays; the week start follows the locale.\n- Day buttons have full-date names; `min` / `max` are enforced for typing too; today is a ring, the footer buttons (\"Today\", \"Clear\") come from messages.\n- `Date` objects and `T12:00:00` tricks are gone; the value is an ISO string end to end."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "Card",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Card } from 'loui-ds';",
      "folder": "src/components/Card",
      "sourceFile": "src/components/Card/Card.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A bordered surface that groups related content. With a `title` it is a labelled region\n(`section` named by its heading).",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": false,
          "description": "Title shown as an uppercase mono caption. It is a real heading (see `headingLevel`) and\nnames the card region through `aria-labelledby`."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2",
          "description": "Heading level of the title (`h1` to `h6`). Pick the level that fits the page outline. Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "actions",
          "type": "ReactNode",
          "required": false,
          "description": "Controls placed at the end of the header, next to the title (buttons, a menu)."
        },
        {
          "name": "variant",
          "type": "'default' | 'raised'",
          "required": false,
          "default": "'default'",
          "description": "`default` is a flat surface with a border. `raised` uses the raised surface, a stronger\nborder and the overlay shadow: use it only for a card that floats above the page (a\npinned panel, a detail popover shell), never for cards that sit in the normal flow.",
          "values": ["default", "raised"]
        },
        {
          "name": "padding",
          "type": "'md' | 'none'",
          "required": false,
          "default": "'md'",
          "description": "`md` applies the card padding token; `none` removes it (for full-bleed content such as a table). Default `md`.",
          "values": ["md", "none"]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "variant": ["default", "raised"]
      },
      "cssVariables": [
        "--ds-card-gap",
        "--ds-card-padding",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-lh-caption",
        "--ds-line",
        "--ds-line-strong",
        "--ds-radius-lg",
        "--ds-shadow-overlay",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Card",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-card--default",
            "hasPlay": false
          },
          {
            "name": "WithActions",
            "id": "components-card--with-actions",
            "hasPlay": false
          },
          {
            "name": "Raised",
            "id": "components-card--raised",
            "hasPlay": false
          },
          {
            "name": "WithoutTitle",
            "id": "components-card--without-title",
            "hasPlay": false
          },
          {
            "name": "HeadingLevel3",
            "id": "components-card--heading-level3",
            "hasPlay": false
          },
          {
            "name": "NoPadding",
            "id": "components-card--no-padding",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-card--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-card--right-to-left",
            "hasPlay": false
          },
          {
            "name": "PaperReceipt",
            "id": "components-card--paper-receipt",
            "description": "The paper scope (`data-theme=\"paper\"`): the only light surface, for receipt and PDF previews.\nSuccess, warning and danger use their darker paper twins (TOK-012), so badges, alerts and\ntinted buttons keep 4.5:1 on the paper.",
            "hasPlay": false
          },
          {
            "name": "ReceiptPreview",
            "id": "components-card--receipt-preview",
            "description": "The entry point of the paper scope: put `data-theme=\"paper\"` on the `Card` itself. The card\ntakes the light paper surface, border and text, and nested `Text`, `Heading` and `Divider`\n(solid and dashed) follow. This is an example composition, not a receipt component.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Card/Card.md",
        "summary": "A bordered surface that groups related content. With a `title` it is a labelled region.",
        "whenToUse": [
          "Group a set of related fields, numbers or a list under one caption.",
          "Put page sections side by side in a grid."
        ],
        "whenNotToUse": [
          "To wrap a whole page: use the page layout.",
          "For content that interrupts the user: use `Dialog` or `Alert`.",
          "Do not nest a Card inside a Card; use a divider instead.",
          "For a card that is itself one click target (a settings category tile): use `CardLink`. For a dashed \"create new\" tile: use `AddCard`. `Card` is never interactive (ADR-0011); a card with inner buttons stays a `Card`."
        ],
        "keyboard": [],
        "examples": [
          "<Card title=\"Storage\" actions={<Button size=\"sm\">Manage</Button>}>\n  <Progress value={62} label=\"Used\" meta=\"6.2 / 10 GB\" />\n</Card>\n\n<Card title=\"Details\" headingLevel={3}>…</Card>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Group a set of related fields, numbers or a list under one caption.\n- Put page sections side by side in a grid."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To wrap a whole page: use the page layout.\n- For content that interrupts the user: use `Dialog` or `Alert`.\n- Do not nest a Card inside a Card; use a divider instead.\n- For a card that is itself one click target (a settings category tile): use `CardLink`. For a dashed \"create new\" tile: use `AddCard`. `Card` is never interactive (ADR-0011); a card with inner buttons stays a `Card`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nsection (surface + 1px line, radius lg, padding)\n  header (optional)\n    h{headingLevel}  mono caption, uppercase by CSS\n    actions          optional controls\n  children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                         | Default     | Notes                                                                    |\n| -------------- | ---------------------------- | ----------- | ------------------------------------------------------------------------ |\n| `title`        | `ReactNode`                  |             | Rendered as a heading styled as a mono caption                           |\n| `headingLevel` | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | `2`         | Pick the level that fits the page outline                                |\n| `actions`      | `ReactNode`                  |             | End of the header                                                        |\n| `variant`      | `'default' \\| 'raised'`      | `'default'` | `raised` only for a card that floats above the page                      |\n| `padding`      | `'md' \\| 'none'`             | `'md'`      | `none` for full-bleed content such as a table (the header keeps padding) |\n\nOther `section` attributes (including `aria-label` and `aria-labelledby`) and `ref` go to the root."
          },
          {
            "id": "elevation",
            "heading": "Elevation",
            "content": "Borders carry hierarchy, shadows are for overlays (AGENTS.md section 5). A `raised` card gets the raised surface, a stronger border **and** `--ds-shadow-overlay`, so use it only when the card really floats (a pinned side panel, the shell of a custom popover). A card in the normal flow stays `default`."
          },
          {
            "id": "paper-scope",
            "heading": "Paper scope",
            "content": "Put `data-theme=\"paper\"` on the `Card` to render the only light surface in the system, for receipt and PDF previews (ADR-0007). The card reads semantic tokens (`--ds-surface`, `--ds-line`, `--ds-text`), which the `[data-theme='paper']` scope remaps on the element itself, so the background, border and text switch with no extra prop. Inside it, `Text`, `Heading` and `Divider` (solid and dashed) follow the same remap; use `<Divider variant=\"dashed\" />` for the total line.\n\n```tsx\n<Card data-theme=\"paper\" title=\"Receipt #1051\">\n  <Text mono>$1,240.00</Text>\n  <Divider variant=\"dashed\" />\n  <Text mono>Total $1,240.00</Text>\n</Card>\n```\n\nPaper-safe components (axe contrast tested in the paper scope): `Card`, `Text`, `Heading`, `Divider`, plus `Badge`, `Alert` and `Button` (see the `PaperReceipt` story). A receipt or PDF component is product domain and is not part of the design system; compose these instead."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The root is a `section`. With a `title`, the heading id is wired to `aria-labelledby`, so screen readers expose a named `region` landmark. Without a title or a label it is a plain generic container (no landmark), which is correct for decorative grouping.\n- Give a title-less card an `aria-label` when it should be a landmark.\n- The heading level is yours to choose: keep levels sequential (a Card inside a page with an `h1` usually uses `2`; inside another section use `3`).\n- The caption uses `--ds-text-2` (the handoff used `--ds-text-3`, which drops below 4.5:1 on the raised surface)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Captions are sentence case in the source (\"Storage\"); uppercase is applied by CSS only.\n- Keep captions to one to three words."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Card title=\"Storage\" actions={<Button size=\"sm\">Manage</Button>}>\n  <Progress value={62} label=\"Used\" meta=\"6.2 / 10 GB\" />\n</Card>\n\n<Card title=\"Details\" headingLevel={3}>…</Card>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The title is a real heading with `aria-labelledby`, not a `span`.\n- `raised` is the `variant` prop; the shadow applies only to it, and it is documented as floating only.\n- Padding and gap are component tokens (`--ds-card-padding`, `--ds-card-gap`); `padding` is `'md' | 'none'` instead of a number.\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Dialog",
            "reason": "For content that interrupts the user: use `Dialog` or `Alert`."
          },
          {
            "name": "Alert",
            "reason": "For content that interrupts the user: use `Dialog` or `Alert`."
          },
          {
            "name": "CardLink",
            "reason": "For a card that is itself one click target (a settings category tile): use `CardLink`. For a dashed \"create new\" tile: use `AddCard`. `Card` is never interactive (ADR-0011); a card with inner buttons stays a `Card`."
          },
          {
            "name": "AddCard",
            "reason": "For a card that is itself one click target (a settings category tile): use `CardLink`. For a dashed \"create new\" tile: use `AddCard`. `Card` is never interactive (ADR-0011); a card with inner buttons stays a `Card`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "CardLink",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { CardLink } from 'loui-ds';",
      "folder": "src/components/CardLink",
      "sourceFile": "src/components/CardLink/CardLink.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A card whose whole surface is one link or button. The title is the real interactive\nelement (an `<a>` with `href`, a `<button>` with `onPress`) inside a heading, and a\nstretched `::after` makes the full card its hit area. The title is the accessible name; the\ndescription and meta are the accessible description.\n\nUse it for navigation tiles (a settings category grid). A card that needs inner buttons is a\n`Card` with a `Button` (ADR-0011).",
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "description": "Title of the card. It is the accessible name of the link or button, and a real heading\n(see `headingLevel`). Keep it to a few words (\"Billing\")."
        },
        {
          "name": "href",
          "type": "string",
          "required": false,
          "description": "Destination. Renders a real `<a>`; uses the router set on `DSProvider` for client-side navigation."
        },
        {
          "name": "target",
          "type": "string",
          "required": false,
          "description": "Link target. `_blank` adds `rel=\"noopener noreferrer\"` and a hidden \"opens in a new tab\" note."
        },
        {
          "name": "rel",
          "type": "string",
          "required": false,
          "description": "Link relationship, merged with `noopener noreferrer` for `_blank`."
        },
        {
          "name": "onPress",
          "type": "((event: PressEvent) => void) | ((event: PressEvent) => void)",
          "required": false,
          "description": "Fired when the link is pressed, before navigation.\nAction run when the card is pressed. Renders a real `<button type=\"button\">`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Supporting text. It is the accessible description (`aria-describedby`), not part of the\nname. Plain text only: the whole card is one interactive element, so never put links,\nbuttons or form controls here."
        },
        {
          "name": "meta",
          "type": "ReactNode",
          "required": false,
          "description": "Small mono detail under the text (\"3 members\", \"ORD-1048\"). Same rule as `description`: no interactive content."
        },
        {
          "name": "leading",
          "type": "ReactNode",
          "required": false,
          "description": "Icon or avatar before the text. Decorative: the title names the card."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "description": "Heading level of the title (`h1` to `h6`). Pick the level that fits the page outline. Default `3`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the card: it is dimmed, skipped by `Tab` and ignores presses."
        },
        {
          "name": "messages",
          "type": "Partial<{ opensInNewTab: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 167
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "CardLink",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "opensInNewTab",
            "kind": "string",
            "default": "(opens in a new tab)"
          }
        ]
      },
      "cssVariables": [
        "--ds-card-link-gap",
        "--ds-card-link-text-gap",
        "--ds-card-padding",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-body",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-body",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-surface",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/CardLink",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-card-link--default",
            "description": "The title is the link; its name is only the title, the description is the accessible description.",
            "hasPlay": true
          },
          {
            "name": "WithLeadingAndMeta",
            "id": "components-card-link--with-leading-and-meta",
            "hasPlay": false
          },
          {
            "name": "AsButton",
            "id": "components-card-link--as-button",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-card-link--long-text",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-card-link--disabled",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-card-link--focused",
            "description": "Keyboard focus draws the ring on the whole card outline.",
            "hasPlay": true
          },
          {
            "name": "ExternalLink",
            "id": "components-card-link--external-link",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-card-link--right-to-left",
            "hasPlay": false
          },
          {
            "name": "InGrid",
            "id": "components-card-link--in-grid",
            "description": "Next to plain `Card`s and an `AddCard`; every tile stretches to the row height.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/CardLink/CardLink.md",
        "summary": "A card whose whole surface is one link or button. Decision: [ADR-0011](../../../docs/adr/0011-interactive-cards.md).",
        "whenToUse": [
          "A grid of destinations, such as a settings category grid: each tile opens a page.",
          "A tile that runs one action (`onPress`), such as \"Import data\"."
        ],
        "whenNotToUse": [
          "A card that contains its own buttons, a menu or form controls: use `Card` with a `Button` (a card can't be interactive and contain interactive content).",
          "A \"create new\" tile: use `AddCard`.",
          "A plain group of content: use `Card`.",
          "A list row: use `ActionList` or a table."
        ],
        "keyboard": [
          "Keyboard: `Tab` reaches it once; `Enter` activates a link or a button, `Space` also activates a button (AGENTS.md section 8)."
        ],
        "examples": [
          "<Grid minItemWidth=\"md\">\n  <CardLink\n    leading={<Icon icon={UsersIcon} />}\n    title=\"Team\"\n    description=\"Members, roles and invitations.\"\n    href=\"/settings/team\"\n  />\n  <CardLink title=\"Import data\" description=\"Upload a CSV file.\" onPress={openImport} />\n  <AddCard href=\"/settings/new\">Create category</AddCard>\n</Grid>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A grid of destinations, such as a settings category grid: each tile opens a page.\n- A tile that runs one action (`onPress`), such as \"Import data\"."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A card that contains its own buttons, a menu or form controls: use `Card` with a `Button` (a card can't be interactive and contain interactive content).\n- A \"create new\" tile: use `AddCard`.\n- A plain group of content: use `Card`.\n- A list row: use `ActionList` or a table."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (surface + 1px line, radius lg, padding, hover = raised surface)\n  leading            optional icon or avatar, decorative\n  div\n    h{headingLevel}\n      a | button     the title; its ::after stretches over the whole card\n    p                description (aria-describedby)\n    p                meta, mono (aria-describedby)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                         | Default | Notes                                                                     |\n| -------------- | ---------------------------- | ------- | ------------------------------------------------------------------------- |\n| `title`        | `string`                     |         | Required. The accessible name.                                            |\n| `description`  | `ReactNode`                  |         | Accessible description. Plain text, no interactive content.               |\n| `meta`         | `ReactNode`                  |         | Small mono detail. Accessible description. No interactive content.        |\n| `leading`      | `ReactNode`                  |         | Decorative icon or avatar.                                                |\n| `href`         | `string`                     |         | Renders an `<a>`. Router support through `DSProvider`.                    |\n| `target`/`rel` | `string`                     |         | With `href`. `_blank` adds `rel=\"noopener noreferrer\"` and a hidden note. |\n| `onPress`      | `(e: PressEvent) => void`    |         | Required without `href` (renders a `<button>`); optional with `href`.     |\n| `isDisabled`   | `boolean`                    | `false` | Dimmed, dashed, skipped by `Tab`.                                         |\n| `headingLevel` | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | `3`     | Keeps the page outline.                                                   |\n| `messages`     | `{ opensInNewTab?: string }` |         | Per-instance message override (ADR-0005).                                 |\n| `className`    | `string`                     |         | On the root.                                                              |\n\nOther `div` attributes and `ref` go to the root. There is no `children` and no `actions`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- One real `<a>` or `<button>`, in a heading, so the card shows up in the heading outline. The name is the title only; the description and meta are announced as the description.\n- Keyboard: `Tab` reaches it once; `Enter` activates a link or a button, `Space` also activates a button (AGENTS.md section 8).\n- Focus ring on the whole card outline, kept in `forced-colors`. Hover is one surface step lighter. Transitions are neutralized by the global reduced-motion rule.\n- The target is the whole card (well above 24 px).\n- Trade-off: the stretched overlay stops text selection by dragging inside the card."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Grid minItemWidth=\"md\">\n  <CardLink\n    leading={<Icon icon={UsersIcon} />}\n    title=\"Team\"\n    description=\"Members, roles and invitations.\"\n    href=\"/settings/team\"\n  />\n  <CardLink title=\"Import data\" description=\"Upload a CSV file.\" onPress={openImport} />\n  <AddCard href=\"/settings/new\">Create category</AddCard>\n</Grid>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Card",
            "reason": "A card that contains its own buttons, a menu or form controls: use `Card` with a `Button` (a card can't be interactive and contain interactive content)."
          },
          {
            "name": "Button",
            "reason": "A card that contains its own buttons, a menu or form controls: use `Card` with a `Button` (a card can't be interactive and contain interactive content)."
          },
          {
            "name": "AddCard",
            "reason": "A \"create new\" tile: use `AddCard`."
          },
          {
            "name": "ActionList",
            "reason": "A list row: use `ActionList` or a table."
          }
        ],
        "uses": ["VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Chart",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Chart } from 'loui-ds';",
      "folder": "src/components/Chart",
      "sourceFile": "src/components/Chart/Chart.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A responsive bar or line chart for one series, drawn in SVG.\n\nIt is a single focusable image: `Tab` focuses it, the left and right arrow keys (and Home and\nEnd) move between data points, and each point is announced politely (\"Tue: 58. 2 of 7\") and\nshown in a label above it. Pointer users get the same label on hover. A visually hidden\n`<table>` with a caption carries the same data for assistive technology.\n\nLimits: one series, a value axis that always includes zero, categories spaced evenly, no\nzoom, no stacked or grouped bars, no legend. The axis is not mirrored in right-to-left\nlanguages (time reads left to right in charts).",
      "props": [
        {
          "name": "data",
          "type": "readonly ChartDatum[]",
          "required": true,
          "description": "The data, in display order (usually time). The chart does not sort."
        },
        {
          "name": "type",
          "type": "'line' | 'bar'",
          "required": false,
          "default": "'bar'",
          "description": "`bar` (default) or `line`. One series per chart.",
          "values": ["line", "bar"]
        },
        {
          "name": "format",
          "type": "(value: number) => string",
          "required": false,
          "description": "Formats values on the axis, in the tooltip, in the announcement and in the data table.\nReceives a number and returns text; use `formatCurrency` or `formatNumber` with the active\nlocale. Default: `formatNumber` (compact on the axis, full in the tooltip)."
        },
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'accent'",
          "description": "Color of the active point and of the line: `accent` (default), `neutral`, `success`, `warning` or `danger`.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "highlight",
          "type": "Readonly<Record<number, string>>",
          "required": false,
          "description": "Marks points by index with short text, for example `{ 29: 'Today', 12: 'Best day' }`. The\nmarked point is drawn in the strong ink color, and its text is printed above it and read\nby screen readers, so the highlight never depends on color. Keep the texts short."
        },
        {
          "name": "notes",
          "type": "Readonly<Record<number, string>>",
          "required": false,
          "description": "Long text about a point by index, for example `{ 12: 'Free shipping started this day.' }`.\nShown under the value in the label of the active point, in the \"Note\" column of the hidden\ntable and in the announcement of the point. Points with a note get a small diamond marker\nabove them (a shape, not a color). Unlike `highlight`, the text is not printed on the\nchart, so it may be a full sentence."
        },
        {
          "name": "referenceLines",
          "type": "readonly ChartReferenceLine[]",
          "required": false,
          "description": "Dashed lines across the plot, each with its label printed next to the line, where it does\nnot cover bars or other texts (see `labelPosition`). `average` and `target` differ by dash\npattern and by name. Use `chartAverage(values)` for the average. The lines are listed in the description and in the last rows of the hidden\ntable. A value outside the data range widens the axis."
        },
        {
          "name": "tickEvery",
          "type": "number",
          "required": false,
          "description": "Show the label of every nth category (`tickEvery={7}` shows 1st, 8th, 15th...). Default:\nautomatic, labels are skipped only when they would collide. Values below 1 or not finite\nmean automatic. Points still get a keyboard stop and a table row."
        },
        {
          "name": "height",
          "type": "string | number",
          "required": false,
          "description": "Height of the chart (a number is px, or any CSS length). Default `--ds-chart-height` (200px). The width is always 100% of the container."
        },
        {
          "name": "description",
          "type": "string",
          "required": false,
          "description": "Longer description of what the chart shows. Read after the name, together with the keyboard instructions."
        },
        {
          "name": "categoryLabel",
          "type": "string",
          "required": false,
          "description": "Header of the category column in the hidden data table. Default: the \"Category\" message."
        },
        {
          "name": "valueLabel",
          "type": "string",
          "required": false,
          "description": "Header of the value column in the hidden data table (for example \"Sales\"). Default: the \"Value\" message."
        },
        {
          "name": "messages",
          "type": "Partial<{ instructions: (count: number, formatted: string) => string; point: (label: string, value: string, position: string, count: string, note?: string) => string; tableCaption: (name?: string) => string; ... 7 more ...; empty: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the chart: what it shows (\"Sales, last 30 days\"). Required unless `aria-labelledby` is set."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the chart (a visible heading)."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "messages": {
        "providerKey": "Chart",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "instructions",
            "kind": "function",
            "default": "{formatted} data points. Use the left and right arrow keys to move between points, Home and End for the first and last.",
            "signature": "(count: number, formatted: string)",
            "description": "Instructions linked to the chart with `aria-describedby`. `count` is the number of data\npoints; `formatted` is the same number formatted for the locale."
          },
          {
            "key": "point",
            "kind": "function",
            "default": "{label}: {value}, {note?}. {position} of {count}",
            "signature": "(label: string, value: string, position: string, count: string, note?: string)",
            "description": "Announced (polite) when the user moves to a point. `value` is already formatted;\n`note` is the highlight text of the point, if any (\"Today\")."
          },
          {
            "key": "tableCaption",
            "kind": "function",
            "default": "Data for {name?}",
            "signature": "(name?: string)",
            "description": "Caption of the visually hidden data table. `name` is the accessible name of the chart, if it has a text one."
          },
          {
            "key": "category",
            "kind": "string",
            "default": "Category",
            "description": "Header of the category column of the data table, unless `categoryLabel` is given."
          },
          {
            "key": "value",
            "kind": "string",
            "default": "Value",
            "description": "Header of the value column of the data table, unless `valueLabel` is given."
          },
          {
            "key": "note",
            "kind": "string",
            "default": "Note",
            "description": "Header of the note column of the data table (shown when a point is highlighted)."
          },
          {
            "key": "average",
            "kind": "string",
            "default": "Average",
            "description": "Name of the `average` reference line kind (dash pattern: long dashes)."
          },
          {
            "key": "target",
            "kind": "string",
            "default": "Target",
            "description": "Name of the `target` reference line kind (dash pattern: dash and dot)."
          },
          {
            "key": "referenceLine",
            "kind": "function",
            "default": "{kind}, {label}",
            "signature": "(kind: string, label: string)",
            "description": "Read form of one reference line, used in the description and as the row header in the\ndata table. `kind` is the \"Average\" / \"Target\" message; `label` is the author's text."
          },
          {
            "key": "referenceSummary",
            "kind": "function",
            "default": "Reference lines: {items}.",
            "signature": "(items: string)",
            "description": "Sentence added to the description. `items` is the joined list of \"kind, label: value\"."
          },
          {
            "key": "empty",
            "kind": "string",
            "default": "No data to show",
            "description": "Text shown inside the plot when there is no data."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-chart-area-opacity",
        "--ds-chart-bar-radius",
        "--ds-chart-color",
        "--ds-chart-grid-dash",
        "--ds-chart-grid-gap",
        "--ds-chart-height",
        "--ds-chart-outline-w",
        "--ds-chart-point-r",
        "--ds-chart-ref-average-dash",
        "--ds-chart-ref-average-gap",
        "--ds-chart-ref-target-dash",
        "--ds-chart-ref-target-dot",
        "--ds-chart-ref-target-gap",
        "--ds-chart-ref-w",
        "--ds-chart-stroke-w",
        "--ds-chart-tooltip-max-w",
        "--ds-chart-tooltip-padding-block",
        "--ds-chart-tooltip-padding-inline",
        "--ds-chart-tooltip-x",
        "--ds-chart-tooltip-y",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-line-subtle",
        "--ds-primary-bg",
        "--ds-primary-fg",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Chart",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-chart--default",
            "hasPlay": true
          },
          {
            "name": "Line",
            "id": "components-chart--line",
            "hasPlay": false
          },
          {
            "name": "Highlight",
            "id": "components-chart--highlight",
            "description": "`highlight` marks points by index with text (\"Today\", \"Best day\"). The point is drawn in the\nstrong ink color with an outline, the text is printed above it and read by screen readers.",
            "hasPlay": true
          },
          {
            "name": "LineHighlight",
            "id": "components-chart--line-highlight",
            "hasPlay": false
          },
          {
            "name": "NegativeValues",
            "id": "components-chart--negative-values",
            "description": "The axis always includes zero and bars hang below it for negative values.",
            "hasPlay": false
          },
          {
            "name": "Notes",
            "id": "components-chart--notes",
            "description": "`notes` attach long text to a point by index. A small diamond marks the point; the text shows\nunder the value in the label of the active point, in the \"Note\" column of the hidden table and\nin the announcement.",
            "hasPlay": true
          },
          {
            "name": "LongNote",
            "id": "components-chart--long-note",
            "description": "A long note wraps inside the label instead of stretching it.",
            "hasPlay": true
          },
          {
            "name": "AverageLine",
            "id": "components-chart--average-line",
            "description": "An average line is drawn with long dashes; use `chartAverage(values)` to compute it.",
            "hasPlay": true
          },
          {
            "name": "TargetLine",
            "id": "components-chart--target-line",
            "description": "A target line is drawn with dash and dot. A target above the data widens the axis.",
            "hasPlay": true
          },
          {
            "name": "AverageAndTarget",
            "id": "components-chart--average-and-target",
            "hasPlay": true
          },
          {
            "name": "TickEvery",
            "id": "components-chart--tick-every",
            "description": "`tickEvery` shows every nth category label, whatever the width.",
            "hasPlay": true
          },
          {
            "name": "ReferenceLabelAvoidsHighlight",
            "id": "components-chart--reference-label-avoids-highlight",
            "description": "GAP-057: a tall highlighted last bar (\"Today\") and an average line. The average text moves\naway from the end of the line when it would cover the highlight or a bar: to the start, below\nthe line, or (when bars cross the line everywhere) outside the plot on the end side.",
            "hasPlay": true
          },
          {
            "name": "ReferenceLabelOutside",
            "id": "components-chart--reference-label-outside",
            "description": "Ninety days: bars cross the average almost everywhere, so its text is printed outside the plot.",
            "hasPlay": false
          },
          {
            "name": "ReferenceLabelPinned",
            "id": "components-chart--reference-label-pinned",
            "description": "`labelPosition: 'end'` pins the text to the end of the line (above it, or below when above is taken), even over bars.",
            "hasPlay": false
          },
          {
            "name": "ReferenceLabelTopRow",
            "id": "components-chart--reference-label-top-row",
            "description": "GAP-057 (0.7.4): the ERP report in an 800 px window, where the chart box is about 470 px\nwide. The long average text fits neither end of the line nor the end margin, so it goes to a\nrow above the plot after a short sample of the line, clear of \"Melhor\" and \"Hoje\".",
            "hasPlay": false
          },
          {
            "name": "ReferenceLabelReport800",
            "id": "components-chart--reference-label-report800",
            "description": "The same report at 800 px: the text fits in the end margin, level with its line.",
            "hasPlay": false
          },
          {
            "name": "AnnotatedRtl",
            "id": "components-chart--annotated-rtl",
            "description": "Notes, reference lines and a highlight together; the chart itself is not mirrored.",
            "hasPlay": false
          },
          {
            "name": "SmallValues",
            "id": "components-chart--small-values",
            "description": "Tiny values still get a readable, round axis.",
            "hasPlay": false
          },
          {
            "name": "AllZero",
            "id": "components-chart--all-zero",
            "hasPlay": false
          },
          {
            "name": "SinglePoint",
            "id": "components-chart--single-point",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-chart--empty",
            "hasPlay": true
          },
          {
            "name": "Tones",
            "id": "components-chart--tones",
            "hasPlay": false
          },
          {
            "name": "CustomHeight",
            "id": "components-chart--custom-height",
            "hasPlay": false
          },
          {
            "name": "ManyPoints",
            "id": "components-chart--many-points",
            "description": "With many points, category labels are skipped so they never overlap; the table keeps all of them.",
            "hasPlay": false
          },
          {
            "name": "KeyboardNavigation",
            "id": "components-chart--keyboard-navigation",
            "description": "Tab focuses the chart; the arrow keys, Home and End move between points; each point is\nannounced politely and shown in a label. Escape clears it.",
            "hasPlay": true
          },
          {
            "name": "Hover",
            "id": "components-chart--hover",
            "description": "Hover shows the same label as the keyboard.",
            "hasPlay": true
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-chart--portuguese-brazil",
            "description": "Instructions, announcements, the table caption and number formats follow the provider locale.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Chart/Chart.md",
        "summary": "A small responsive bar or line chart for one series, drawn in SVG with no chart library. Grid and axis are quiet (dashed lines, mono labels); the data is reachable by keyboard and by a hidden table.",
        "whenToUse": [
          "A trend or a comparison across a handful to a few dozen categories: sales per day, orders per week.",
          "Next to the number it explains (a `StatCard`)."
        ],
        "whenNotToUse": [
          "Several series, stacked or grouped bars, zoom, brushing, pie charts: not supported (see limits).",
          "Parts of a whole: use `ProportionBar`.",
          "Rankings with names: use `RankList`.",
          "Exact values the user must read or compare: use `DataTable` (the chart is for shape)."
        ],
        "keyboard": [
          "Keyboard: `Tab` focuses the chart (nothing announced yet). Left/Right (also Up/Down) move to the previous/next point, Home/End to the first/last, Escape clears the active point. The first Right arrow goes to the first point, the first Left arrow to the last. Movement stops at the ends."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A trend or a comparison across a handful to a few dozen categories: sales per day, orders per week.\n- Next to the number it explains (a `StatCard`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Several series, stacked or grouped bars, zoom, brushing, pie charts: not supported (see limits).\n- Parts of a whole: use `ProportionBar`.\n- Rankings with names: use `RankList`.\n- Exact values the user must read or compare: use `DataTable` (the chart is for shape)."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  div[role=img][tabindex=0]   named by aria-label / aria-labelledby, described by the instructions\n    svg[aria-hidden]          grid + tick labels, bars or line + area, reference lines + labels, category labels, highlight texts, note markers\n    span                      value label of the active point (aria-hidden)\n  span                        visually hidden description + keyboard instructions\n  span[role=status]           visually hidden polite live region: \"Tue: 58. 2 of 7\"\n  table (visually hidden)     caption, headers, one row per point, one footer row per reference line\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                          | Type                                       | Default   | Notes                                                                                                                                                                           |\n| ----------------------------- | ------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `data`                        | `{ label, value }[]`                       | -         | In display order; the chart never sorts                                                                                                                                         |\n| `aria-label`                  | `string`                                   | -         | Required (or `aria-labelledby`): what the chart shows                                                                                                                           |\n| `type`                        | `bar \\| line`                              | `bar`     |                                                                                                                                                                                 |\n| `format`                      | `(value) => string`                        | `Intl`    | Axis, label, announcement and table. Default: compact on the axis, full elsewhere                                                                                               |\n| `tone`                        | `ChartTone`                                | `accent`  | Color of the active bar and of the line                                                                                                                                         |\n| `highlight`                   | `Record<index, string>`                    |           | `{ 29: 'Today', 12: 'Best day' }`: ink fill + outline + text above + announced                                                                                                  |\n| `notes`                       | `Record<index, string>`                    |           | Long text per point: in the label of the active point, the table and the announcement; diamond marker on the point                                                              |\n| `referenceLines`              | `{ value, label, kind, labelPosition? }[]` |           | Dashed lines across the plot (`kind`: `average` or `target`); the axis widens to include them. `labelPosition`: `auto` (default), `end` or `start`, see \"Reference line labels\" |\n| `tickEvery`                   | `number`                                   | automatic | Show every nth category label. Below 1 or not finite means automatic                                                                                                            |\n| `height`                      | `number \\| string`                         | `200px`   | Width is always 100% of the container                                                                                                                                           |\n| `description`                 | `string`                                   |           | Longer description, read before the keyboard instructions                                                                                                                       |\n| `categoryLabel`, `valueLabel` | `string`                                   |           | Headers of the hidden table (\"Day\", \"Sales\")                                                                                                                                    |\n| `messages`                    | `Partial<messages>`                        |           | `instructions`, `point`, `tableCaption`, `category`, `value`, `note`, `average`, `target`, `referenceLine`, `referenceSummary`, `empty`                                         |\n\n`chartAverage(values)` is exported: the mean of numbers (or of `{ value }` data), ignoring non-finite entries, `0` when empty. Use it for the `average` line: `referenceLines={[{ kind: 'average', label: 'Average', value: chartAverage(data) }]}`.\n\n`niceScale(min, max, tickCount?)` is exported: a pure helper that returns `{ min, max, step, ticks }` for a round axis. It always includes zero, handles all-zero data (0 to 1), tiny values (`0.00003`), huge values, negatives and non-finite input."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The plot is one `role=\"img\"` with an accessible name and a description; the SVG inside is hidden. The visually hidden `<table>` with a `<caption>` is the text alternative and holds every value (and the highlight texts in a \"Note\" column).\n- Notes: a point with a `notes` entry gets a small diamond above it (a shape, not a color). Its text is shown under the value in the label of the active point (wrapping, up to `--ds-chart-tooltip-max-w`), read in the announcement after the highlight (\"Tue: 58, Today. Free shipping started. 2 of 7\") and written in the \"Note\" column of the table. When a point has both, the cell reads \"Today. Free shipping started.\"; the table gets the Note column as soon as any point has a highlight or a note.\n- Reference lines: dashed across the plot with the label and the formatted value as text at the end side of the line (\"Goal 120\"). `average` uses long dashes, `target` dash and dot, so the kind never depends on color; the line and the text use `text-2`. Screen readers get them in the description (\"Reference lines: Average, Weekly mean: 51.5 and Target, Goal: 120.\") and in the footer rows of the table (\"Average, Weekly mean\" / \"51.5\"). If the label already starts with the kind name as a whole word (\"Average per open day\", pt-BR \"Média por dia aberto\"; compared ignoring case and accents in the active locale), only the label is read, so the kind is never said twice. Where the text is printed never changes what is read (see \"Reference line labels\").\n- Axis labels: by default category labels are skipped automatically when they would collide; `tickEvery` forces every nth label (1 shows all, even if they touch). The keyboard and the table always reach every point.\n- Keyboard: `Tab` focuses the chart (nothing announced yet). Left/Right (also Up/Down) move to the previous/next point, Home/End to the first/last, Escape clears the active point. The first Right arrow goes to the first point, the first Left arrow to the last. Movement stops at the ends.\n- The active point is announced politely (\"Tue: 58, Today. 2 of 7\") and drawn as a label above it; hover shows the same label. The label is also in the SVG as text, never color-only.\n- Highlights always carry text and a stronger shape (outline on bars, a ring marker on lines), so they do not depend on color. Active bars get a tone fill and outline.\n- Bars use `line-control` (3:1 against the surface); tick labels use `text-3` (above 4.5:1 on `bg` and `surface`).\n- Reduced motion: color transitions only apply when motion is allowed. Forced colors: bars, lines and labels switch to system colors (`GrayText`, `CanvasText`, `Highlight`).\n- The chart does not mirror in right-to-left languages (time still runs left to right); the label position uses physical coordinates on purpose."
          },
          {
            "id": "reference-line-labels",
            "heading": "Reference line labels",
            "content": "The text of a reference line (\"Average $1.8K\") is placed where it does not cover the bars, the line, a highlight text (\"Today\"), a note marker or the text of another reference line. With `labelPosition: 'auto'` (the default) the chart tries, in order:\n\n1. the end of the line, above it (the usual place, used whenever it is clear);\n2. the start of the line, above it;\n3. the end, then the start, below the line;\n4. outside the plot, on the end side, level with its line: the plot gives up the width of the text. This happens when bars cross the line at both ends, as in a 90-day report whose last bar is the tallest. Texts of nearby lines are moved apart there. Skipped when the margin would take more than 40% of the chart width, would hide category labels that all fit before, or would touch a highlight text that reaches past the last bar;\n5. in a row above the plot, aligned to the end side, after a short sample of the line in its dash pattern (like a legend): the plot gives up the height of the row. Nothing else is drawn there, so the text never covers anything. Several texts sit side by side and wrap onto more rows. This is the usual result for a long label in a narrow chart, such as \"Média por dia aberto R$ 1.611,98\" in a 470 px report.\n\nWith `auto` the text never covers a bar, the line, a point, a highlight text or another reference text.\n\n`labelPosition: 'end'` or `'start'` pins the text to that end of the line: above it, or below it when above is taken, and never outside. Use it only when the position must stay fixed across data, since the text may then cover bars.\n\nThe accessible name, the description and the hidden table are the same wherever the text is printed. The placement uses estimated text boxes (caption mono); the browser tests measure the real boxes at 470, 800 and 1366 px with 30 and 90 points."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Reference line labels: either a short name that adds to the kind (\"Goal\", \"Weekly mean\"; read as \"Target, Goal\") or a full name that starts with the kind (\"Average per open day\"; read as written). Do not put the kind in the middle or the end of the label (\"Daily average\" is read \"Average, Daily average\"). The visible text is always the label alone.\n- Name says what and when: \"Sales, last 30 days\". Labels are short (\"Mon\", \"28\") because they are printed under the plot.\n- Highlight texts are one or two words (\"Today\", \"Best day\").\n- Format money with `formatCurrency` (integer minor units) and the active locale; never hand-build strings."
          },
          {
            "id": "limits",
            "heading": "Limits",
            "content": "- One series per chart. For two series use two charts side by side.\n- Categories are spaced evenly (no time scale); labels are skipped when crowded unless `tickEvery` is set (the table and the keyboard still reach every point).\n- Reference lines are horizontal only. Their labels avoid marks (see \"Reference line labels\"); only a pinned `labelPosition` can cross bars (the text carries a background-colored halo).\n- The value axis always includes zero, and reference line values (a target far above the data, for example) widen it. Up to about 100 ticks are generated at most (usually 4 to 7).\n- Geometry (padding, bar width, label spacing) lives in `chartLayout.ts` as SVG units; every color, stroke, radius and font is a token."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Chart\n  aria-label=\"Sales, last 30 days\"\n  type=\"bar\"\n  data={days.map((d) => ({ label: d.day, value: d.totalMinor }))}\n  format={(v) => formatCurrency(v, 'USD', locale)}\n  highlight={{ 29: 'Today', [bestIndex]: 'Best day' }}\n  notes={{ [bestIndex]: 'Free shipping campaign started this day.' }}\n  referenceLines={[\n    { kind: 'average', label: 'Average', value: chartAverage(days.map((d) => d.totalMinor)) },\n    { kind: 'target', label: 'Goal', value: 500000 },\n  ]}\n  tickEvery={7}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The SVG has an accessible name, description and a visually hidden data table.\n- Focusable with arrow-key traversal and polite announcements instead of a mouse-only tooltip.\n- Negative values, all-zero data and tiny maxima work (nice-number axis instead of `log10`).\n- `highlight` is `index -> text` (was `index -> color`): the highlight has a text and shape cue. `color` is now `tone`.\n- Bars use `line-control` instead of `line-strong` so they reach 3:1.\n- Responsive label skipping is tested; `height` can be any CSS length."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ProportionBar",
            "reason": "Parts of a whole: use `ProportionBar`."
          },
          {
            "name": "RankList",
            "reason": "Rankings with names: use `RankList`."
          },
          {
            "name": "DataTable",
            "reason": "Exact values the user must read or compare: use `DataTable` (the chart is for shape)."
          }
        ],
        "uses": ["VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "ChatPanel",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ChatPanel } from 'loui-ds/patterns';",
      "folder": "src/patterns/ChatPanel",
      "sourceFile": "src/patterns/ChatPanel/ChatPanel.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A chat side panel for talking to an agent: header with Clear, a message log, suggested prompts\nwhen empty, and a composer. Built for streaming replies.\n\nLive-region approach: the message list is `role=\"log\"` (a navigable transcript) with\n`aria-live=\"off\"`, so partial text never reaches a screen reader token by token. A separate\nvisually hidden `role=\"status\"` region announces \"Thinking…\", \"The assistant is responding\",\nand then the full reply once, when it is complete.\n\nMessage content is plain text. Pass `renderContent` to render sanitized markdown.",
      "props": [
        {
          "name": "items",
          "type": "readonly ChatMessage[]",
          "required": true,
          "description": "The conversation, oldest first. Update `content` and `status` of the last item as a reply streams in."
        },
        {
          "name": "title",
          "type": "ReactNode",
          "required": false,
          "description": "Heading of the panel. Defaults to the \"Chat with the agent\" message."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2",
          "description": "Heading level of the title (`h1` to `h6`). Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "busy",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Waiting for the first part of a reply: shows a spinner with \"Thinking…\" and disables sending."
        },
        {
          "name": "busyLabel",
          "type": "string",
          "required": false,
          "description": "Text shown next to the busy indicator and announced while waiting (\"Searching orders…\").\nDefaults to the \"Thinking…\" message."
        },
        {
          "name": "busyIndicator",
          "type": "ReactNode",
          "required": false,
          "description": "Replaces the default `Spinner` of the busy row. Decorative: the busy text is `busyLabel`.\nPass `null` to show the text only."
        },
        {
          "name": "getKey",
          "type": "(message: ChatMessage, index: number) => string",
          "required": false,
          "description": "Key of a message, used as the React key and to tell history from news. Must be unique and\nstable for the same message across renders. Default: `message.id` when present, otherwise\nthe role plus the position among messages of the same role (`user-0`, `assistant-1`), which\nbreaks if you insert or remove messages before the end. Pass `getKey` (or give each message\nan `id`) whenever the list is not append-only."
        },
        {
          "name": "intro",
          "type": "ReactNode",
          "required": false,
          "description": "Short text shown while the conversation is empty."
        },
        {
          "name": "prompts",
          "type": "readonly string[]",
          "required": false,
          "default": "[]",
          "description": "Suggested prompts shown while the conversation is empty. Pressing one sends it."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Placeholder of the composer. Never a substitute for its label."
        },
        {
          "name": "onSend",
          "type": "(text: string) => void",
          "required": false,
          "description": "Called with the trimmed text when the user sends (Enter, or the send button) or presses a suggested prompt."
        },
        {
          "name": "onStop",
          "type": "() => void",
          "required": false,
          "description": "Called when the user stops a reply that is being generated. Without it the button is disabled meanwhile."
        },
        {
          "name": "onRetry",
          "type": "(message: ChatMessage) => void",
          "required": false,
          "description": "Called with the failed message when the user presses Retry. Without it, failed messages have no Retry."
        },
        {
          "name": "onClear",
          "type": "() => void",
          "required": false,
          "description": "Called when the user clears the conversation. Without it there is no Clear button."
        },
        {
          "name": "renderContent",
          "type": "(message: ChatMessage) => ReactNode",
          "required": false,
          "description": "Renders the content of a message. Use it to plug in a markdown renderer. Whatever it returns\nis rendered as-is, so it must be **sanitized**: the design system never uses\n`dangerouslySetInnerHTML` and ships no markdown library. Without it the content is plain\ntext with line breaks preserved."
        },
        {
          "name": "sendVariant",
          "type": "'primary' | 'secondary'",
          "required": false,
          "default": "'secondary'",
          "description": "Visual weight of the send button: `secondary` (default) for a chat that sits beside the\npage's main task, `primary` when the chat is the main task of the page. One primary per screen.",
          "values": ["primary", "secondary"]
        },
        {
          "name": "messages",
          "type": "Partial<{ title: string; clear: string; transcript: string; composerLabel: string; placeholder: string; send: string; stop: string; thinking: string; responding: string; you: string; assistant: string; ... 5 more ...; errorSaid: (text: string) => string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ChatPanel",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "title",
            "kind": "string",
            "default": "Chat with the agent",
            "description": "Default heading of the panel."
          },
          {
            "key": "clear",
            "kind": "string",
            "default": "Clear conversation",
            "description": "Clear button in the header."
          },
          {
            "key": "transcript",
            "kind": "string",
            "default": "Conversation",
            "description": "Accessible name of the message list (`role=\"log\"`)."
          },
          {
            "key": "composerLabel",
            "kind": "string",
            "default": "Message",
            "description": "Accessible name of the composer field."
          },
          {
            "key": "placeholder",
            "kind": "string",
            "default": "Ask or request something",
            "description": "Default placeholder of the composer."
          },
          {
            "key": "send",
            "kind": "string",
            "default": "Send message",
            "description": "Composer button while it can send, and while the assistant is working (it stops)."
          },
          {
            "key": "stop",
            "kind": "string",
            "default": "Stop generating"
          },
          {
            "key": "thinking",
            "kind": "string",
            "default": "Thinking…",
            "description": "Visible busy text and the announcement while waiting for the first part of a reply."
          },
          {
            "key": "responding",
            "kind": "string",
            "default": "The assistant is responding",
            "description": "Announcement when a reply starts streaming. The text itself is announced once, when complete."
          },
          {
            "key": "you",
            "kind": "string",
            "default": "You",
            "description": "Visually hidden author prefix of each message."
          },
          {
            "key": "assistant",
            "kind": "string",
            "default": "Assistant"
          },
          {
            "key": "error",
            "kind": "string",
            "default": "Error"
          },
          {
            "key": "retry",
            "kind": "string",
            "default": "Retry",
            "description": "Retry button of a failed reply."
          },
          {
            "key": "jumpToLatest",
            "kind": "string",
            "default": "Jump to latest",
            "description": "Shown while the list is scrolled away from the latest message."
          },
          {
            "key": "suggestions",
            "kind": "string",
            "default": "Suggested prompts",
            "description": "Accessible name of the group of suggested prompts."
          },
          {
            "key": "assistantSaid",
            "kind": "function",
            "default": "Assistant: {text}",
            "signature": "(text: string)",
            "description": "Announcements made when a reply is complete or failed."
          },
          {
            "key": "errorSaid",
            "kind": "function",
            "default": "Error: {text}",
            "signature": "(text: string)"
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-chat-panel-bubble-max-ratio",
        "--ds-chat-panel-composer-padding",
        "--ds-chat-panel-gap",
        "--ds-chat-panel-header-height",
        "--ds-chat-panel-padding-block",
        "--ds-chat-panel-padding-inline",
        "--ds-danger",
        "--ds-danger-line",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-label",
        "--ds-lh-body",
        "--ds-lh-label",
        "--ds-line",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Patterns/ChatPanel",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Empty",
            "id": "patterns-chat-panel--empty",
            "hasPlay": true
          },
          {
            "name": "Conversation",
            "id": "patterns-chat-panel--conversation",
            "hasPlay": false
          },
          {
            "name": "Busy",
            "id": "patterns-chat-panel--busy",
            "hasPlay": false
          },
          {
            "name": "WithoutIds",
            "id": "patterns-chat-panel--without-ids",
            "description": "Messages without ids (`{ role, content }`): keys come from the role and position, or from `getKey`.",
            "hasPlay": false
          },
          {
            "name": "CustomBusyLabel",
            "id": "patterns-chat-panel--custom-busy-label",
            "description": "A busy row with the consumer's own text instead of \"Thinking…\".",
            "hasPlay": false
          },
          {
            "name": "Streaming",
            "id": "patterns-chat-panel--streaming",
            "hasPlay": false
          },
          {
            "name": "ErrorWithRetry",
            "id": "patterns-chat-panel--error-with-retry",
            "hasPlay": false
          },
          {
            "name": "PrimarySend",
            "id": "patterns-chat-panel--primary-send",
            "hasPlay": false
          },
          {
            "name": "LongConversation",
            "id": "patterns-chat-panel--long-conversation",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-chat-panel--long-text",
            "hasPlay": false
          },
          {
            "name": "CustomRenderer",
            "id": "patterns-chat-panel--custom-renderer",
            "description": "Content is plain text by default. Plug a sanitized renderer in with `renderContent`.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-chat-panel--right-to-left",
            "hasPlay": false
          },
          {
            "name": "InteractiveStreaming",
            "id": "patterns-chat-panel--interactive-streaming",
            "hasPlay": true
          },
          {
            "name": "InteractiveError",
            "id": "patterns-chat-panel--interactive-error",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ChatPanel/ChatPanel.md",
        "summary": "A chat panel for talking to an agent: header with a Clear action, a message log, suggested prompts when empty, and a composer. It supports streaming replies, Stop and Retry. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/agents/ChatPanel` (TODO `CMP-AGT-03`; groundwork for `AI-013`).",
        "whenToUse": [
          "A side panel where the owner tests an agent, asks why it did something or asks for an adjustment.",
          "Any short, owner-facing conversation with an assistant."
        ],
        "whenNotToUse": [
          "Customer-facing conversations: the chat is for the owner. Anything that goes to a customer must pass through an approval queue (`ApprovalCard`), never straight from the chat.",
          "A one-shot prompt with a single answer: use a form.",
          "Rich documents as replies: render those in the page and keep the chat short."
        ],
        "keyboard": [],
        "examples": [
          "function AgentChat() {\n  const [items, setItems] = useState<ChatMessage[]>([]);\n  const [busy, setBusy] = useState(false);\n  // onSend: append the user message, set busy, then append an assistant message with\n  // status 'streaming' and grow its content; set status 'complete' (or 'error') at the end.\n  return (\n    <ChatPanel\n      items={items}\n      busy={busy}\n      intro=\"Test replies or ask for changes.\"\n      prompts={['Summarize today’s questions']}\n      onSend={ask}\n      onStop={abort}\n      onRetry={retry}\n      onClear={() => setItems([])}\n    />\n  );\n}"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A side panel where the owner tests an agent, asks why it did something or asks for an adjustment.\n- Any short, owner-facing conversation with an assistant."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Customer-facing conversations: the chat is for the owner. Anything that goes to a customer must pass through an approval queue (`ApprovalCard`), never straight from the chat.\n- A one-shot prompt with a single answer: use a form.\n- Rich documents as replies: render those in the page and keep the chat short."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nsection (named by the heading)\n  header          h{headingLevel} title + Clear (ghost, only with messages and onClear)\n  viewport\n    div[role=log][aria-live=off][tabindex=0]\n      intro + suggested prompts (group)    only while empty\n      message rows (user right, assistant left, error with Retry)\n      \"Thinking…\" row                      while busy or a reply has no text yet\n    Jump to latest                         only when scrolled away from the end\n  div[role=status][aria-live=polite]       visually hidden announcer\n  composer        Textarea (Enter sends) + one button: Send message | Stop generating\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                     | Type                         | Default                                                  | Notes                                                                                      |\n| ---------------------------------------- | ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| `items`                                  | `ChatMessage[]`              | required                                                 | The conversation, oldest first. (Named `items` because `messages` overrides the catalog.)  |\n| `busy`                                   | `boolean`                    | `false`                                                  | Waiting for the first part of a reply.                                                     |\n| `busyLabel` / `busyIndicator`            | `string` / `ReactNode`       | \"Thinking…\" / `Spinner`                                  | Text of the busy row (also announced) and a replacement for its spinner (`null` for none). |\n| `getKey`                                 | `(message, index) => string` | `id`, else role + position                               | React key and history detection. Pass it when messages are inserted or removed mid-list.   |\n| `onSend`                                 | `(text: string) => void`     |                                                          | Trimmed text. Called from Enter, the button and the suggested prompts.                     |\n| `onStop`                                 | `() => void`                 |                                                          | Stop generating. The button is disabled while working without it.                          |\n| `onRetry`                                | `(message) => void`          |                                                          | Shows Retry on failed messages.                                                            |\n| `onClear`                                | `() => void`                 |                                                          | Shows Clear when there are messages.                                                       |\n| `intro` / `prompts`                      | `ReactNode` / `string[]`     |                                                          | Empty state.                                                                               |\n| `renderContent`                          | `(message) => ReactNode`     | plain text                                               | Plug in a sanitized renderer (see below).                                                  |\n| `sendVariant`                            | `'secondary' \\| 'primary'`   | `'secondary'`                                            | See \"Send button\".                                                                         |\n| `title` / `headingLevel` / `placeholder` |                              | \"Chat with the agent\" / `2` / \"Ask or request something\" |                                                                                            |\n| `messages`                               | partial catalog              |                                                          | Every string, including the announcements.                                                 |\n\n`ChatMessage`: `{ id?: string; role: 'user' | 'assistant'; content: string; status?: 'complete' | 'streaming' | 'error' }`. `id` is optional so `{ role, content }` objects work as they are. Without it the key is the role plus the position among messages of the same role (`user-0`, `assistant-1`), which is stable while the list only grows. If you insert, remove or reorder messages, give each one an `id` or pass `getKey`; keys must be unique and stable across renders. Never use the array index."
          },
          {
            "id": "streaming-and-live-regions-research-notes",
            "heading": "Streaming and live regions (research notes)",
            "content": "Sources: practitioner write-ups on accessible streaming chat UIs (azukiazusa.dev, tianpan.co), an IDE chat-panel pull request (TrixtyAI/ide#172) and the WAI-ARIA `log` role. There is no formal standard for streamed AI output; this is current practice, not tested with every screen reader.\n\n- `role=\"log\"` is the right role for a chat transcript (implicit polite live region, history in reading order). But a streamed reply rewrites one node many times, and a live region then reads it in fragments (or repeats it).\n- Common practice (and what chat products do): keep the streaming text **out** of the live region, announce a short status while it generates, and announce the result once when complete.\n\nWhat `ChatPanel` does:\n\n1. The list is `role=\"log\"` with `aria-live=\"off\"`: a labelled, focusable (`tabindex=0`, so it can be scrolled with the keyboard) transcript that screen reader users can read in order, without automatic announcements. It sets `aria-busy` while a reply is being produced.\n2. A separate visually hidden `role=\"status\"` (polite, atomic) announces: **\"Thinking…\"** while `busy`, **\"The assistant is responding\"** while a message is `streaming` (one announcement, not one per token), and **\"Assistant: <full reply>\"** once when the message becomes `complete` (or **\"Error: …\"** when it fails).\n3. The user's own messages are never announced (they just typed them). Messages present on first render are history and are not announced.\n4. The author (\"You\", \"Assistant\", \"Error\") is a visually hidden prefix of every message, since alignment and color are the only visual cue.\n\nNot done: a \"regenerate\" action (only Retry for failed replies), and chunked announcements for very long replies."
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- **Composer.** Enter sends, Shift+Enter inserts a new line. Enter is ignored while an **IME composition** is active (`isComposing`, and `keyCode 229` for Safari), so committing a CJK candidate never sends. Whitespace-only text is not sent. The field is not disabled while the assistant works, so the user can prepare the next message; sending is disabled meanwhile.\n- **Send / Stop.** One button changes its label and action (Send message, Stop generating), so keyboard focus is not lost when a reply starts. With no `onStop` it is disabled while working.\n- **Scroll anchoring.** The list follows new content only while the user is within a small distance of the bottom. If they scroll up to read, nothing moves, and \"Jump to latest\" appears. Sending a message returns to the bottom. No smooth scrolling is used, so `prefers-reduced-motion` needs no special handling; the spinner is already static under reduced motion.\n- **Suggested prompts** send immediately. The chat is owner-facing and nothing leaves it."
          },
          {
            "id": "send-button-secondary-by-default",
            "heading": "Send button: secondary by default",
            "content": "The panel normally sits beside the page's main task (an agent screen), and the screen already has its one primary action, so Send is `secondary`. When the chat is the main task of the page, pass `sendVariant=\"primary\"`. One primary per screen."
          },
          {
            "id": "content-policy-plain-text-by-default",
            "heading": "Content policy: plain text by default",
            "content": "Messages render as **plain text with line breaks**. The design system never uses `dangerouslySetInnerHTML` and ships no markdown library. To show markdown, pass `renderContent` and return **React elements** from a renderer that sanitizes its input (a markdown-to-React library that does not allow raw HTML, with an allow-list of elements and URL schemes). `renderContent` runs for every message, including while it streams, so it must tolerate incomplete markdown. The story \"CustomRenderer\" shows the shape."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Region named by its heading; composer named \"Message\"; prompts in a group named \"Suggested prompts\"; Clear and Retry are real buttons.\n- Keyboard: Tab order is header (Clear), the log (scroll with arrows and Page keys), Jump to latest when shown, composer, the Send/Stop button. Prompts are buttons in the log while empty.\n- Error messages use text (\"Error:\" prefix, hidden), a danger border and danger text: color is never the only cue.\n- Forced colors: the borders and focus use system colors through the shared tokens (outlines are transparent, not removed)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title and buttons are sentence case, verb plus object: \"Clear conversation\", \"Send message\", \"Stop generating\".\n- Errors say how to fix: \"The agent could not answer. Check your connection and try again.\"\n- The agent never sends anything to a customer from this chat."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nfunction AgentChat() {\n  const [items, setItems] = useState<ChatMessage[]>([]);\n  const [busy, setBusy] = useState(false);\n  // onSend: append the user message, set busy, then append an assistant message with\n  // status 'streaming' and grow its content; set status 'complete' (or 'error') at the end.\n  return (\n    <ChatPanel\n      items={items}\n      busy={busy}\n      intro=\"Test replies or ask for changes.\"\n      prompts={['Summarize today’s questions']}\n      onSend={ask}\n      onStop={abort}\n      onRetry={retry}\n      onClear={() => setItems([])}\n    />\n  );\n}\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Messages are keyed by stable `id` instead of the array index; `status` adds streaming and error states.\n- Enter ignores IME composition; Stop, Retry, scroll anchoring and Jump to latest are new.\n- The log no longer announces every change: the live-region design above replaces `aria-live=\"polite\"` on the list.\n- Strings are in the catalog (including `pt-BR`); the composer is the design system `Textarea`; the Send button is the design system `Button` (secondary by default instead of a hand-made primary).\n- `renderContent` is the markdown hook; no HTML injection."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ApprovalCard",
            "reason": "Customer-facing conversations: the chat is for the owner. Anything that goes to a customer must pass through an approval queue (`ApprovalCard`), never straight from the chat."
          }
        ],
        "uses": ["Button", "Spinner", "Textarea", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Checkbox",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Checkbox } from 'loui-ds';",
      "folder": "src/components/Checkbox",
      "sourceFile": "src/components/Checkbox/Checkbox.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A checkbox with a native `<input type=\"checkbox\">` (so `name` / `value` take part in form\nsubmission), an 18px box and an optional description. The whole label row is the click target.\n\nState props follow React Aria: `isSelected`, `defaultSelected` and `onChange(isSelected)`.\n`isIndeterminate` is visual and semantic (\"mixed\") until the user toggles it.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Secondary text under the label. Linked with `aria-describedby`, not part of the name."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, for example \"Accept the terms to continue\". Shown while the checkbox is invalid\nand the error is displayed (after a submit attempt with the default native validation).\nInside a `CheckboxGroup` put the error on the group instead."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`. Use `inputRef` for the `<input>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaToggleProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "CheckboxFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "inputRef",
              "type": "Ref<HTMLInputElement | null>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "CheckboxProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isIndeterminate",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", CheckboxFieldRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleStateOptions",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(isSelected: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: boolean) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-checkbox-box-size",
        "--ds-checkbox-radius",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-choice-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-5",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-3",
        "--ds-text-inverse"
      ],
      "stories": {
        "title": "Components/Checkbox",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-checkbox--default",
            "hasPlay": true
          },
          {
            "name": "Checked",
            "id": "components-checkbox--checked",
            "hasPlay": true
          },
          {
            "name": "Indeterminate",
            "id": "components-checkbox--indeterminate",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-checkbox--with-description",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-checkbox--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-checkbox--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-checkbox--disabled",
            "hasPlay": false
          },
          {
            "name": "ReadOnly",
            "id": "components-checkbox--read-only",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-checkbox--long-label",
            "hasPlay": false
          },
          {
            "name": "WithoutVisibleLabel",
            "id": "components-checkbox--without-visible-label",
            "hasPlay": false
          },
          {
            "name": "InAGroup",
            "id": "components-checkbox--in-agroup",
            "hasPlay": true
          },
          {
            "name": "GroupHorizontal",
            "id": "components-checkbox--group-horizontal",
            "hasPlay": false
          },
          {
            "name": "GroupInvalid",
            "id": "components-checkbox--group-invalid",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Checkbox/Checkbox.md",
        "summary": "A checkbox with a native `<input type=\"checkbox\">`, an 18px box and an optional description, plus a labelled group for several related checkboxes. Built on React Aria `CheckboxField` + `CheckboxButton` (the non-deprecated pair that supports a description and an error outside the label) and `CheckboxGroup`. Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": [
          "Independent yes/no choices, agreement (\"I accept the terms\"), selecting rows, picking several items from a list (`CheckboxGroup`).",
          "`isIndeterminate` for a parent that summarizes partly selected children."
        ],
        "whenNotToUse": [
          "An on/off setting that applies at once: `Switch`.",
          "One choice from several: `RadioGroup`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Independent yes/no choices, agreement (\"I accept the terms\"), selecting rows, picking several items from a list (`CheckboxGroup`).\n- `isIndeterminate` for a parent that summarizes partly selected children."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- An on/off setting that applies at once: `Switch`.\n- One choice from several: `RadioGroup`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                      | Notes                                                                                            |\n| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |\n| `label`                                                   | Visible label. Without it pass `aria-label` or `aria-labelledby` (type error otherwise).         |\n| `description`                                             | Secondary text under the label, linked by `aria-describedby`, not part of the name.              |\n| `errorMessage`                                            | Error for a single checkbox (for example a required agreement). In a group, set it on the group. |\n| `isSelected` / `defaultSelected` / `onChange(isSelected)` | React Aria naming for controlled and uncontrolled use.                                           |\n| `isIndeterminate`                                         | Shows a minus; the native input is `indeterminate` until toggled.                                |\n| `name`, `value`                                           | Submitted with the form while checked (`value` defaults to `on`).                                |\n| `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly`     | Standard states.                                                                                 |\n| `inputRef` / `ref`                                        | `inputRef` is the `<input>`, `ref` the field root.                                               |\n\n`CheckboxGroup` takes `label`, `description`, `errorMessage`, `orientation` (`vertical` default, `horizontal`), `name`, `value`/`defaultValue`/`onChange(values: string[])`, `isRequired`, `isInvalid`, `isDisabled`. Each child `Checkbox` needs a `value`."
          },
          {
            "id": "look",
            "heading": "Look",
            "content": "- 18px box, radius from `--ds-checkbox-radius` (5px), `--ds-line-control` border (3:1 or better in every state), accent fill with a `--ds-text-inverse` check when selected, minus when indeterminate.\n- Hover brightens the border; it never changes the fill.\n- The whole row is the click target and is at least 24px tall (WCAG 2.5.8).\n- A disabled group dims once as a whole; disabled single checkboxes use `--ds-opacity-disabled`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Space toggles the focused checkbox. Tab moves between checkboxes; each checkbox is a tab stop.\n- Name comes from the label only; the description is `aria-describedby`.\n- Group: `role=\"group\"` named by the label, with the group description and error.\n- Forced colors: borders and the check glyph use system colors; the glyph (check or minus) carries the state."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Checkbox label=\"I accept the terms\" name=\"terms\" isRequired errorMessage=\"Accept the terms to continue.\" />\n\n<CheckboxGroup label=\"Notify me about\" name=\"topics\" defaultValue={['orders']}>\n  <Checkbox value=\"orders\" label=\"New orders\" />\n  <Checkbox value=\"stock\" label=\"Low stock\" description=\"When a product drops below its minimum.\" />\n</CheckboxGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A native input replaces `div role=\"checkbox\"`; `name` and `value` take part in forms.\n- Accessible name is required by the types; the label row is the click target.\n- New: indeterminate, group, required, invalid, read-only, error text.\n- Radius is a component token (the handoff hard-coded 5px); the fill is accent as in the handoff.\n- Controlled props follow React Aria (`isSelected`, `onChange`) instead of `checked`/`onChange(checked)`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Switch",
            "reason": "An on/off setting that applies at once: `Switch`."
          },
          {
            "name": "RadioGroup",
            "reason": "One choice from several: `RadioGroup`."
          }
        ],
        "uses": ["FieldMessages", "Icon", "RequiredMark"],
        "usedBy": ["ActionList", "DataTable"]
      }
    },
    {
      "name": "CheckboxGroup",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { CheckboxGroup } from 'loui-ds';",
      "folder": "src/components/Checkbox",
      "sourceFile": "src/components/Checkbox/CheckboxGroup.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A labelled `role=\"group\"` of checkboxes that share one value (`string[]`), one `name`, and\none description and error. `value`, `defaultValue` and `onChange(values)` follow React Aria.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The `Checkbox` elements of the group. Each needs a `value`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the label. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text for the group, shown while it is invalid and the error is displayed."
        },
        {
          "name": "orientation",
          "type": "'vertical' | 'horizontal'",
          "required": false,
          "default": "'vertical'",
          "description": "Layout of the checkboxes. Default `vertical`.",
          "values": ["vertical", "horizontal"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the group root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the group root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", CheckboxGroupRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: string[]) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "string[]",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: string[]) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "string[]",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-checkbox-box-size",
        "--ds-checkbox-radius",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-choice-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-5",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-3",
        "--ds-text-inverse"
      ],
      "stories": {
        "title": "Components/Checkbox",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-checkbox--default",
            "hasPlay": true
          },
          {
            "name": "Checked",
            "id": "components-checkbox--checked",
            "hasPlay": true
          },
          {
            "name": "Indeterminate",
            "id": "components-checkbox--indeterminate",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-checkbox--with-description",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-checkbox--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-checkbox--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-checkbox--disabled",
            "hasPlay": false
          },
          {
            "name": "ReadOnly",
            "id": "components-checkbox--read-only",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-checkbox--long-label",
            "hasPlay": false
          },
          {
            "name": "WithoutVisibleLabel",
            "id": "components-checkbox--without-visible-label",
            "hasPlay": false
          },
          {
            "name": "InAGroup",
            "id": "components-checkbox--in-agroup",
            "hasPlay": true
          },
          {
            "name": "GroupHorizontal",
            "id": "components-checkbox--group-horizontal",
            "hasPlay": false
          },
          {
            "name": "GroupInvalid",
            "id": "components-checkbox--group-invalid",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Checkbox/Checkbox.md",
        "summary": "A checkbox with a native `<input type=\"checkbox\">`, an 18px box and an optional description, plus a labelled group for several related checkboxes. Built on React Aria `CheckboxField` + `CheckboxButton` (the non-deprecated pair that supports a description and an error outside the label) and `CheckboxGroup`. Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": [
          "Independent yes/no choices, agreement (\"I accept the terms\"), selecting rows, picking several items from a list (`CheckboxGroup`).",
          "`isIndeterminate` for a parent that summarizes partly selected children."
        ],
        "whenNotToUse": [
          "An on/off setting that applies at once: `Switch`.",
          "One choice from several: `RadioGroup`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Independent yes/no choices, agreement (\"I accept the terms\"), selecting rows, picking several items from a list (`CheckboxGroup`).\n- `isIndeterminate` for a parent that summarizes partly selected children."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- An on/off setting that applies at once: `Switch`.\n- One choice from several: `RadioGroup`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                      | Notes                                                                                            |\n| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |\n| `label`                                                   | Visible label. Without it pass `aria-label` or `aria-labelledby` (type error otherwise).         |\n| `description`                                             | Secondary text under the label, linked by `aria-describedby`, not part of the name.              |\n| `errorMessage`                                            | Error for a single checkbox (for example a required agreement). In a group, set it on the group. |\n| `isSelected` / `defaultSelected` / `onChange(isSelected)` | React Aria naming for controlled and uncontrolled use.                                           |\n| `isIndeterminate`                                         | Shows a minus; the native input is `indeterminate` until toggled.                                |\n| `name`, `value`                                           | Submitted with the form while checked (`value` defaults to `on`).                                |\n| `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly`     | Standard states.                                                                                 |\n| `inputRef` / `ref`                                        | `inputRef` is the `<input>`, `ref` the field root.                                               |\n\n`CheckboxGroup` takes `label`, `description`, `errorMessage`, `orientation` (`vertical` default, `horizontal`), `name`, `value`/`defaultValue`/`onChange(values: string[])`, `isRequired`, `isInvalid`, `isDisabled`. Each child `Checkbox` needs a `value`."
          },
          {
            "id": "look",
            "heading": "Look",
            "content": "- 18px box, radius from `--ds-checkbox-radius` (5px), `--ds-line-control` border (3:1 or better in every state), accent fill with a `--ds-text-inverse` check when selected, minus when indeterminate.\n- Hover brightens the border; it never changes the fill.\n- The whole row is the click target and is at least 24px tall (WCAG 2.5.8).\n- A disabled group dims once as a whole; disabled single checkboxes use `--ds-opacity-disabled`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Space toggles the focused checkbox. Tab moves between checkboxes; each checkbox is a tab stop.\n- Name comes from the label only; the description is `aria-describedby`.\n- Group: `role=\"group\"` named by the label, with the group description and error.\n- Forced colors: borders and the check glyph use system colors; the glyph (check or minus) carries the state."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Checkbox label=\"I accept the terms\" name=\"terms\" isRequired errorMessage=\"Accept the terms to continue.\" />\n\n<CheckboxGroup label=\"Notify me about\" name=\"topics\" defaultValue={['orders']}>\n  <Checkbox value=\"orders\" label=\"New orders\" />\n  <Checkbox value=\"stock\" label=\"Low stock\" description=\"When a product drops below its minimum.\" />\n</CheckboxGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A native input replaces `div role=\"checkbox\"`; `name` and `value` take part in forms.\n- Accessible name is required by the types; the label row is the click target.\n- New: indeterminate, group, required, invalid, read-only, error text.\n- Radius is a component token (the handoff hard-coded 5px); the fill is accent as in the handoff.\n- Controlled props follow React Aria (`isSelected`, `onChange`) instead of `checked`/`onChange(checked)`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Switch",
            "reason": "An on/off setting that applies at once: `Switch`."
          },
          {
            "name": "RadioGroup",
            "reason": "One choice from several: `RadioGroup`."
          }
        ],
        "uses": ["FieldMessages", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "ChoiceTiles",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ChoiceTiles } from 'loui-ds';",
      "folder": "src/components/ChoiceTiles",
      "sourceFile": "src/components/ChoiceTiles/ChoiceTiles.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Single-choice tiles: a `radiogroup` whose options are drawn as big buttons with a label and an\noptional meta line. The group is one tab stop; arrow keys move focus and select, `Space`\nselects (React Aria roving focus; arrows wrap at the ends like any radio group).\n`value`, `defaultValue` and `onChange(value)` follow React Aria. A group needs a name: pass\n`label`, `aria-label` or `aria-labelledby`.\n\nFor a few short options drawn as one bar, use `SegmentedControl`; for a plain list of\noptions, use `RadioGroup`.",
      "props": [
        {
          "name": "options",
          "type": "readonly ChoiceTilesOption[]",
          "required": true,
          "description": "The tiles, in display order. Two to eight fit well."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the label. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the group is invalid."
        },
        {
          "name": "columns",
          "type": "number",
          "required": false,
          "default": "2",
          "description": "Maximum number of columns. Default `2`. The grid collapses to fewer columns on narrow\ncontainers so a tile never gets narrower than `--ds-choice-tiles-min-width`."
        },
        {
          "name": "size",
          "type": "'md' | 'lg'",
          "required": false,
          "default": "'lg'",
          "description": "Tile height: `md` 40px or `lg` 48px (default).",
          "values": ["md", "lg"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the group root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the group root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", RadioGroupRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: string) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "string | null",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: string) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["md", "lg"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-bg",
        "--ds-choice-tiles-columns",
        "--ds-choice-tiles-gap",
        "--ds-choice-tiles-height-lg",
        "--ds-choice-tiles-height-md",
        "--ds-choice-tiles-min-width",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/ChoiceTiles",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-choice-tiles--default",
            "hasPlay": true
          },
          {
            "name": "Selected",
            "id": "components-choice-tiles--selected",
            "hasPlay": false
          },
          {
            "name": "Medium",
            "id": "components-choice-tiles--medium",
            "hasPlay": false
          },
          {
            "name": "ThreeColumns",
            "id": "components-choice-tiles--three-columns",
            "hasPlay": false
          },
          {
            "name": "WithDetail",
            "id": "components-choice-tiles--with-detail",
            "description": "A second line (`detail`) under the label and meta, exposed as the radio's description.",
            "hasPlay": false
          },
          {
            "name": "WithoutMeta",
            "id": "components-choice-tiles--without-meta",
            "hasPlay": false
          },
          {
            "name": "DisabledTile",
            "id": "components-choice-tiles--disabled-tile",
            "hasPlay": true
          },
          {
            "name": "DisabledGroup",
            "id": "components-choice-tiles--disabled-group",
            "hasPlay": false
          },
          {
            "name": "WithDescription",
            "id": "components-choice-tiles--with-description",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-choice-tiles--invalid",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-choice-tiles--required",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-choice-tiles--long-text",
            "hasPlay": false
          },
          {
            "name": "NarrowContainer",
            "id": "components-choice-tiles--narrow-container",
            "description": "Below roughly two minimum tile widths the grid collapses to one column.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-choice-tiles--right-to-left",
            "hasPlay": false
          },
          {
            "name": "KeyboardSelection",
            "id": "components-choice-tiles--keyboard-selection",
            "description": "One tab stop. Arrows move focus and select, wrapping at the ends.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-choice-tiles--controlled",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-choice-tiles--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ChoiceTiles/ChoiceTiles.md",
        "summary": "Single-choice tiles: a labelled group where the options are drawn as large buttons with a label and an optional meta line. Built on React Aria `RadioGroup` with custom radio buttons, so it is a real `radiogroup` with roving focus.\n\n- Handoff source: `components/erp/ChoiceTiles.jsx` (TODO `CMP-PAT-08`).",
        "whenToUse": [
          "A choice with 2 to 8 short options where each option deserves weight: delivery method, plan, size, payment type.",
          "When a secondary fact helps decide (a price, a duration, a code): use `meta`."
        ],
        "whenNotToUse": [
          "For a view mode or a small filter: use `SegmentedControl`.",
          "For a long list of options or long descriptions: use `RadioGroup` (with descriptions) or `Select`.",
          "For several selections at once: use `CheckboxGroup` or `TagGroup`."
        ],
        "keyboard": [
          "Keyboard: one tab stop; arrows (`←` `↑` previous, `→` `↓` next) move focus and select, wrapping at the ends; `Space` selects the focused tile. Disabled tiles are skipped."
        ],
        "examples": [
          "<ChoiceTiles\n  label=\"Delivery\"\n  columns={2}\n  options={[\n    { value: 'standard', label: 'Standard', meta: '3-5 days' },\n    { value: 'express', label: 'Express', meta: '1 day' },\n  ]}\n  value={method}\n  onChange={setMethod}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A choice with 2 to 8 short options where each option deserves weight: delivery method, plan, size, payment type.\n- When a secondary fact helps decide (a price, a duration, a code): use `meta`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a view mode or a small filter: use `SegmentedControl`.\n- For a long list of options or long descriptions: use `RadioGroup` (with descriptions) or `Select`.\n- For several selections at once: use `CheckboxGroup` or `TagGroup`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=radiogroup] aria-labelledby=label\n  label                      (group label, from `label`)\n  div.grid                   (responsive grid, at most `columns` columns)\n    div > label.tile         (one per option; hidden native radio inside)\n      label text\n      meta                   (optional, mono; text or a short inline node)\n      detail                 (optional second line, body font; the radio's description)\n  description / error\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                                          | Type                   | Default      | Notes                                                         |\n| ------------------------------------------------------------- | ---------------------- | ------------ | ------------------------------------------------------------- |\n| `options`                                                     | `ChoiceTilesOption[]`  | required     | `{ value, label, meta?, detail?, isDisabled? }`.              |\n| `label` / `aria-label` / `aria-labelledby`                    | `ReactNode` / `string` | one required | Names the group. Enforced by the types.                       |\n| `value` / `defaultValue` / `onChange(value)`                  | `string`               | none         | Controlled / uncontrolled selection (React Aria names).       |\n| `columns`                                                     | `number`               | `2`          | Maximum columns. Fewer on narrow containers (min tile 120px). |\n| `size`                                                        | `'md' \\| 'lg'`         | `'lg'`       | Minimum tile height: 40px or 48px.                            |\n| `description`, `errorMessage`                                 | `ReactNode`            | none         | Linked with `aria-describedby`.                               |\n| `isDisabled`, `isRequired`, `isInvalid`, `isReadOnly`, `name` | -                      | -            | Group state, forwarded to React Aria.                         |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Semantics: `radiogroup` of native radio inputs; the tile is the radio's label.\n- Keyboard: one tab stop; arrows (`←` `↑` previous, `→` `↓` next) move focus and select, wrapping at the ends; `Space` selects the focused tile. Disabled tiles are skipped.\n- State is not color alone: the selected tile has a doubled (2px) accent border, a soft fill and ink text, and screen readers get \"checked\".\n- Boundaries: unselected tiles use `--ds-line-control` (3:1) and hover brightens the border instead of changing the fill, so the 3:1 holds in every state. The meta line uses `--ds-text-2` (`--ds-text-3` fails 4.5:1 on the selected fill).\n- Focus: accent border plus ring. Forced colors: the selected border uses `Highlight`.\n\n- `detail` is linked with `aria-describedby`: the radio's name stays the label plus the meta, and the detail is read after it as the description. Use it for a short helpful sentence, not for essential text."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels in sentence case, one to three words. Put the differentiating fact in `meta`, not in the label.\n- Keep the same shape across tiles: avoid `meta` on some tiles only."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<ChoiceTiles\n  label=\"Delivery\"\n  columns={2}\n  options={[\n    { value: 'standard', label: 'Standard', meta: '3-5 days' },\n    { value: 'express', label: 'Express', meta: '1 day' },\n  ]}\n  value={method}\n  onChange={setMethod}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A `div role=\"radiogroup\"` of `button role=\"radio\"` had no roving focus or arrow keys; it is now React Aria `RadioGroup` (one tab stop, arrows, `Space`).\n- The group is named with `label` (rendered and wired with `aria-labelledby`), `aria-label` or `aria-labelledby`; the handoff had no name.\n- Columns are responsive: `columns` is a maximum and the grid collapses by minimum tile width. `size` is `md`/`lg` (tokens).\n- Strings of options are plain props; `onChange` receives the value.\n- Unselected borders are `--ds-line-control` instead of `--ds-line` (3:1), and the selected state adds a thicker border."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "For a view mode or a small filter: use `SegmentedControl`."
          },
          {
            "name": "RadioGroup",
            "reason": "For a long list of options or long descriptions: use `RadioGroup` (with descriptions) or `Select`."
          },
          {
            "name": "Select",
            "reason": "For a long list of options or long descriptions: use `RadioGroup` (with descriptions) or `Select`."
          },
          {
            "name": "CheckboxGroup",
            "reason": "For several selections at once: use `CheckboxGroup` or `TagGroup`."
          },
          {
            "name": "TagGroup",
            "reason": "For several selections at once: use `CheckboxGroup` or `TagGroup`."
          }
        ],
        "uses": ["FieldMessages", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "Citation",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { Citation } from 'loui-ds/patterns';",
      "folder": "src/patterns/SourceList",
      "sourceFile": "src/patterns/SourceList/SourceList.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An inline marker for a claim in an answer. It is a real link (`href=\"#...\"`) that moves\nfocus to the matching source in the `SourceList`, so it works with a keyboard and a screen\nreader: the name is \"Source 2: Title\", the visible text is the number.",
      "props": [
        {
          "name": "number",
          "type": "number",
          "required": true,
          "description": "Position of the source in the list, starting at 1. Shown as the marker text."
        },
        {
          "name": "title",
          "type": "string",
          "required": true,
          "description": "Title of the source. Part of the accessible name (\"Source 2: Title\")."
        },
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "Id of the `SourceList` the source belongs to."
        },
        {
          "name": "sourceId",
          "type": "string",
          "required": true,
          "description": "Id of the source (`Source.id`)."
        },
        {
          "name": "messages",
          "type": "Partial<{ title: string; citation: (number: number, title: string) => string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AnchorHTMLAttributes",
          "domGeneric": true,
          "count": 7
        },
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "SourceList",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "title",
            "kind": "string",
            "default": "Sources",
            "description": "Default heading of the source list."
          },
          {
            "key": "citation",
            "kind": "function",
            "default": "Source 2: {title}",
            "signature": "(number: number, title: string)",
            "description": "Accessible name of a citation marker: \"Source 2: Title\"."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-citation-size",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line-control",
        "--ds-radius-sm",
        "--ds-source-list-gap",
        "--ds-source-list-indent",
        "--ds-source-list-padding",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Patterns/SourceList",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-source-list--default",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-source-list--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-source-list--right-to-left",
            "hasPlay": false
          },
          {
            "name": "WithCitations",
            "id": "patterns-source-list--with-citations",
            "description": "Markers in a sentence jump to the matching source; focus lands on it.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/SourceList/SourceList.md",
        "summary": "The numbered list of sources an answer is based on (`SourceList`) and the inline markers that point to it (`Citation`). Import both from `loui-ds/patterns`.",
        "whenToUse": [
          "An assistant answer that rests on documents, pages or records, and the owner should be able to check them."
        ],
        "whenNotToUse": [
          "Footnotes in long-form documents: use plain links.",
          "A list of results the user searched for: use a table or a list of cards."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- An assistant answer that rests on documents, pages or records, and the owner should be able to check them."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Footnotes in long-form documents: use plain links.\n- A list of results the user searched for: use a table or a list of cards."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\np            ... a claim [2] ...        Citation: <a href=\"#s-source-b\" aria-label=\"Source 2: Title\">2</a>\nsection      named by its heading (\"Sources\")\n  ol\n    li       tabIndex=-1 (focus target)    title (link when it has a url) · domain (mono) · snippet\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`SourceList`\n\n| Prop           | Type            | Default   | Notes                                                                        |\n| -------------- | --------------- | --------- | ---------------------------------------------------------------------------- |\n| `id`           | `string`        | -         | Required. Citations find their source through it.                            |\n| `sources`      | `Source[]`      | -         | `{ id, title, url?, domain?, snippet? }`. The position is the source number. |\n| `title`        | `string`        | \"Sources\" | Heading text.                                                                |\n| `headingLevel` | `1`-`6`         | `3`       |                                                                              |\n| `messages`     | partial catalog |           | `title`, `citation(number, title)`.                                          |\n\n`Citation`: `number` (1-based, the visible text), `title`, `listId`, `sourceId`, plus anchor props. `getSourceElementId(listId, sourceId)` returns the DOM id when you need it."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A citation is a real link (`href=\"#...\"`) named \"Source 2: Title\"; the visible text is the number, which the name contains.\n- Activating it moves focus to the matching list item and scrolls it into view. It does not change the URL hash, so it does not fight your router. Without JavaScript the hash link still works.\n- Source titles with a `url` are links that open in a new tab (with the external-link icon and a screen-reader note).\n- The marker is 24 px square (WCAG 2.5.8)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Keep source order stable while the answer streams; append new sources at the end, so numbers already shown never change.\n- Titles say what the page is, not \"Click here\". Snippets are short, plain text."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<p>\n  Orders move through four statuses\n  <Citation number={1} title=\"Order lifecycle\" listId=\"answer-1\" sourceId=\"lifecycle\" />.\n</p>\n<SourceList\n  id=\"answer-1\"\n  sources={[{ id: 'lifecycle', title: 'Order lifecycle', url: 'https://example.com/orders' }]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "No handoff source: new pattern (TODO `AI-013`)."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["Link"],
        "usedBy": []
      }
    },
    {
      "name": "Code",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Code } from 'loui-ds';",
      "folder": "src/components/Code",
      "sourceFile": "src/components/Code/Code.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Monospace code: inline in a sentence, or as a block (`pre` + `code`). A block that overflows\nbecomes a keyboard stop and a labelled region so it can be scrolled without a mouse.\nIt does not highlight syntax.",
      "props": [
        {
          "name": "variant",
          "type": "'inline' | 'block'",
          "required": false,
          "default": "'inline'",
          "description": "`inline` (default) is a `code` element inside running text. `block` renders a `pre` with a\n`code` inside it, keeps the whitespace, and scrolls sideways instead of wrapping.",
          "values": ["inline", "block"]
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Block only. Accessible name of the block when it scrolls and becomes a region. Defaults to\n\"Code\" from the message catalog; say what the code is when there are several (\"Request body\")."
        },
        {
          "name": "messages",
          "type": "Partial<{ blockLabel: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "variant": ["inline", "block"]
      },
      "messages": {
        "providerKey": "Code",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "blockLabel",
            "kind": "string",
            "default": "Code",
            "description": "Accessible name of a scrollable code block (a labelled region), when the consumer gives none."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-code-inline-padding-block",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-radius-md",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface",
        "--ds-text"
      ],
      "stories": {
        "title": "Typography/Code",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "typography-code--default",
            "hasPlay": false
          },
          {
            "name": "Block",
            "id": "typography-code--block",
            "hasPlay": false
          },
          {
            "name": "BlockScrolls",
            "id": "typography-code--block-scrolls",
            "description": "A block that overflows is a labelled region and a keyboard stop. Press Tab, then the right arrow (the browser test covers the scrolling itself).",
            "hasPlay": true
          },
          {
            "name": "InlineLongToken",
            "id": "typography-code--inline-long-token",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "typography-code--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Code/Code.md",
        "summary": "Monospace code: inline in a sentence, or a block (`pre` + `code`).",
        "whenToUse": [
          "Commands, identifiers, file names and short snippets in documentation or help text.",
          "A block for several lines, such as a request example."
        ],
        "whenNotToUse": [
          "For data values in the UI (IDs, amounts, shortcuts): use `Text mono`, or `Kbd` for a key.",
          "For syntax highlighting or a copy button: not part of this component."
        ],
        "keyboard": [],
        "examples": [
          "<p>Run <Code>pnpm check</Code> before you push.</p>\n\n<Code variant=\"block\" aria-label=\"Request example\">\n  {`curl -X POST https://api.example.com/v1/orders`}\n</Code>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Commands, identifiers, file names and short snippets in documentation or help text.\n- A block for several lines, such as a request example."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For data values in the UI (IDs, amounts, shortcuts): use `Text mono`, or `Kbd` for a key.\n- For syntax highlighting or a copy button: not part of this component."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ncode                 inline\npre > code           block (scrolls sideways, keeps whitespace)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                  | Default    | Notes                                                                     |\n| ------------ | --------------------- | ---------- | ------------------------------------------------------------------------- |\n| `variant`    | `'inline' \\| 'block'` | `'inline'` | Block renders `pre` + `code`                                              |\n| `aria-label` | `string`              | \"Code\"     | Block only: the name of the region while it scrolls. Say what the code is |\n| `messages`   | `{ blockLabel }`      |            | Overrides the default label for this instance                             |\n\nOther `code` attributes and `ref` go to the `code` element."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A block that overflows horizontally gets `tabindex=\"0\"` and `role=\"region\"` with a name, so keyboard users can scroll it (WCAG 2.1.1). A block that fits is a plain `pre`.\n- Inline code wraps anywhere (`overflow-wrap: anywhere`) so a long token does not overflow at 320px.\n- Block text is `--ds-text` on `--ds-surface` (≥ 4.5:1)."
          },
          {
            "id": "internationalization",
            "heading": "Internationalization",
            "content": "`blockLabel` has English and `pt-BR` (\"Código\") strings. Override per instance (`messages`), through `DSProvider`, or with `aria-label`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<p>Run <Code>pnpm check</Code> before you push.</p>\n\n<Code variant=\"block\" aria-label=\"Request example\">\n  {`curl -X POST https://api.example.com/v1/orders`}\n</Code>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff used inline mono spans. Code is a real `code` / `pre`, with a keyboard-accessible scrolling block."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Kbd",
            "reason": "For data values in the UI (IDs, amounts, shortcuts): use `Text mono`, or `Kbd` for a key."
          }
        ],
        "uses": [],
        "usedBy": ["ToolCallStep"]
      }
    },
    {
      "name": "ColorField",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ColorField } from 'loui-ds';",
      "folder": "src/components/ColorField",
      "sourceFile": "src/components/ColorField/ColorField.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A hex colour field: a text input in Geist Mono with a `Swatch` preview of the current colour,\nbuilt on React Aria `ColorField`. Type `#abc` or `aabbcc` and the value is normalised to\n`#AABBCC` on blur or `Enter`; text that is not a colour is reverted on blur. `ArrowUp` /\n`ArrowDown` step the value by one.\n\n`value` / `defaultValue` accept a `#RRGGBB` string or a React Aria `Color`; `onChange`\nreceives a `Color` or `null` when the field is empty. Read the hex with\n`color?.toString('hex')` (always `#RRGGBB`, upper case). The swatch is decorative: the\nhex text next to it names the colour. A picker (area and sliders) is not part of this component.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control, linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check.\nThe text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed (see `Input`).\nDefaults to the catalog message \"Enter a colour as #RRGGBB.\"."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field is empty, for example `#RRGGBB`. Never a substitute for the label."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<input>` element."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ invalid: string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `invalid`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaColorFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isWheelDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ColorFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "onChange",
              "type": "(color: Color | null) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "ColorFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "colorSpace",
              "type": "'rgb' | 'hsl' | 'hsb'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: Color | null) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "defaultValue",
              "type": "string | Color | null",
              "required": false
            },
            {
              "name": "value",
              "type": "string | Color | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "ColorField",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "invalid",
            "kind": "string",
            "default": "Enter a colour as #RRGGBB.",
            "description": "Error shown when the field is invalid and the consumer gave no `errorMessage`."
          }
        ]
      },
      "cssVariables": [
        "--ds-field-control-padding-inline",
        "--ds-font-mono",
        "--ds-space-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/ColorField",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-color-field--default",
            "hasPlay": true
          },
          {
            "name": "WithValue",
            "id": "components-color-field--with-value",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-color-field--empty",
            "hasPlay": false
          },
          {
            "name": "NormalisesOnBlur",
            "id": "components-color-field--normalises-on-blur",
            "hasPlay": true
          },
          {
            "name": "KeyboardContract",
            "id": "components-color-field--keyboard-contract",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-color-field--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-color-field--invalid",
            "hasPlay": false
          },
          {
            "name": "CustomError",
            "id": "components-color-field--custom-error",
            "hasPlay": false
          },
          {
            "name": "DescriptionTone",
            "id": "components-color-field--description-tone",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-color-field--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-color-field--read-only",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-color-field--sizes",
            "hasPlay": false
          },
          {
            "name": "Controlled",
            "id": "components-color-field--controlled",
            "hasPlay": true
          },
          {
            "name": "Brazilian",
            "id": "components-color-field--brazilian",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ColorField/ColorField.md",
        "summary": "A hex colour input with a live `Swatch` preview. Built on React Aria `ColorField` (hex mode), styled like [Input](../Input/Input.md). Task: GAP-024.",
        "whenToUse": [
          "The user types or pastes a colour as a hex code: fabric colour, tag colour, brand colour in a settings form."
        ],
        "whenNotToUse": [
          "Choosing from a fixed palette: a hand-built set of options with `Swatch` and labels.",
          "Exploring colours visually (area, hue slider, eyedropper): a picker is not part of this component yet.",
          "A colour name or code that is not a hex value: `Input`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The user types or pastes a colour as a hex code: fabric colour, tag colour, brand colour in a settings form."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing from a fixed palette: a hand-built set of options with `Swatch` and labels.\n- Exploring colours visually (area, hue slider, eyedropper): a picker is not part of this component yet.\n- A colour name or code that is not a hex value: `Input`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "`[ swatch | #RRGGBB ]` in one box (`sm` 32 / `md` 40 / `lg` 48), then description and error as in `Input`. The text is Geist Mono, shown upper case. The swatch is decorative (`aria-hidden`) and empty (dashed circle) when there is no value."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                          | Notes                                                                                           |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`                    | One is required.                                                                                |\n| `value` / `defaultValue` / `onChange(color)`                  | RAC naming. A `#RRGGBB` string or a `Color`. `onChange` receives a `Color`, or `null` if empty. |\n| `validate`                                                    | Receives the `Color \\| null`; return a message (or messages) to mark the field invalid.         |\n| `size`                                                        | `sm` / `md` (default) / `lg`.                                                                   |\n| `fullWidth`                                                   | In a flex row (`Inline`) the field takes the free space and can shrink. Off by default.         |\n| `description`, `descriptionTone`, `errorMessage`              | As in `Input`. `errorMessage` defaults to the catalog `invalid` message.                        |\n| `placeholder`                                                 | For example `#RRGGBB`. Never a substitute for the label.                                        |\n| `name`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | Standard. `name` submits `#RRGGBB`.                                                             |\n| `messages`                                                    | `invalid`, `requiredMark`.                                                                      |\n\n### Getting the hex\n\n`onChange` gives a React Aria `Color`. The normalised string is `color?.toString('hex')`: always `#RRGGBB`, upper case (`null` when the field is empty). To store the value as a string, keep that string and pass it back as `value`.\n\n```tsx\nconst [hex, setHex] = useState('#C2603E');\n<ColorField label=\"Hex\" value={hex} onChange={(color) => setHex(color?.toString('hex') ?? '')} />;\n```"
          },
          {
            "id": "behaviour",
            "heading": "Behaviour",
            "content": "- Typing is free. On blur or `Enter` the text is parsed and normalised: `#abc` and `abc` become `#AABBCC`.\n- Text that is not a colour is reverted to the last valid value on blur (React Aria). To flag an empty required field or a rule of your own (`validate`, `isInvalid`), the field shows the `invalid` message \"Enter a colour as #RRGGBB.\" unless you pass `errorMessage`; an app-supplied `validate` message wins.\n- An empty field reports `null`; the swatch is then empty."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                     | Behavior                                         |\n| ----------------------- | ------------------------------------------------ |\n| Typing                  | Any text; parsed and normalised on blur or Enter |\n| `ArrowUp` / `ArrowDown` | Step the colour value by 1 (React Aria)          |\n\nThe input is a text input named by its label, with description and error linked by `aria-describedby` and `aria-invalid`. The swatch is `aria-hidden`: the hex text next to it carries the meaning, so colour is never the only carrier. Focus ring on the box, also in forced-colors."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the noun (\"Hex\", \"Fabric colour\"). Errors say how to fix: \"Enter a colour as #RRGGBB.\""
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<ColorField\n  label=\"Hex\"\n  name=\"hex\"\n  defaultValue=\"#C2603E\"\n  description=\"Colour of the fabric as dyed.\"\n/>\n```"
          },
          {
            "id": "design-note-gap-024",
            "heading": "Design note (GAP-024)",
            "content": "Decision: wrap React Aria `ColorField` rather than keep a masked `Input` plus `Swatch` in every app. No new ADR: `react-aria-components` is already the behaviour layer (ADR-0001) and this adds no dependency. React Aria gives hex parsing, `#abc` expansion, stepping and form submission for free; the DS adds the field anatomy, the swatch preview, mono type and messages.\n\nOut of scope, a later task: a colour picker (area, hue and channel sliders, eyedropper) as a popover composed with this field, and channel fields (`channel` prop) for HSL or RGB inputs."
          },
          {
            "id": "differences-from-the-consumer-workaround",
            "heading": "Differences from the consumer workaround",
            "content": "- No hand-built `normalizeHex`, `isHex` or preview row: the field normalises and previews itself.\n- The consumer disabled form validation and showed its own error; here the field uses `FieldError` and the catalog message, in en and pt-BR."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Swatch",
            "reason": "Choosing from a fixed palette: a hand-built set of options with `Swatch` and labels."
          },
          {
            "name": "Input",
            "reason": "A colour name or code that is not a hex value: `Input`."
          }
        ],
        "uses": ["FieldMessages", "Label", "Swatch"],
        "usedBy": []
      }
    },
    {
      "name": "ColorOption",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ColorOption } from 'loui-ds/patterns';",
      "folder": "src/patterns/ColorOption",
      "sourceFile": "src/patterns/ColorOption/ColorOption.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A selectable option that shows a color swatch and its name. It is a toggle button\n(`aria-pressed`), so each option is chosen independently: put several in a\n`role=\"group\"` with an accessible name to pick any number of colors.\n`isSelected` / `defaultSelected` / `onChange(isSelected)` follow React Aria.\n\nWhy a toggle and not a radio: the handoff uses it to pick several colors (a product's\navailable colors). To pick exactly one color, use `ChoiceTiles` or `RadioGroup` instead.",
      "props": [
        {
          "name": "color",
          "type": "string",
          "required": true,
          "description": "The color to show, as `#rgb` or `#rrggbb`."
        },
        {
          "name": "label",
          "type": "string",
          "required": true,
          "description": "Name of the color (\"Navy\"). It is the accessible name of the option and is always visible."
        },
        {
          "name": "mark",
          "type": "ReactNode",
          "required": false,
          "description": "Optional short text after the label (a count, \"Default\", a code). It is part of the\naccessible name. While the option is selected and no `mark` is given, a check is shown\ninstead."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the button."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLButtonElement>",
          "required": false,
          "description": "Ref to the underlying `<button>`."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaBaseButtonProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-disabled",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-expanded",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "preventFocusOnPress",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"button\", ToggleButtonRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "ToggleButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "Key",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ToggleButtonProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(isSelected: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-bg",
        "--ds-color-option-group-columns",
        "--ds-color-option-group-gap",
        "--ds-color-option-group-min-width",
        "--ds-control-h-md",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-3",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Patterns/ColorOption",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-color-option--default",
            "hasPlay": true
          },
          {
            "name": "Selected",
            "id": "patterns-color-option--selected",
            "hasPlay": false
          },
          {
            "name": "WithMark",
            "id": "patterns-color-option--with-mark",
            "hasPlay": false
          },
          {
            "name": "SelectedWithMark",
            "id": "patterns-color-option--selected-with-mark",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "patterns-color-option--disabled",
            "hasPlay": false
          },
          {
            "name": "DisabledSelected",
            "id": "patterns-color-option--disabled-selected",
            "hasPlay": false
          },
          {
            "name": "LongLabel",
            "id": "patterns-color-option--long-label",
            "hasPlay": false
          },
          {
            "name": "LightColor",
            "id": "patterns-color-option--light-color",
            "hasPlay": false
          },
          {
            "name": "DarkColor",
            "id": "patterns-color-option--dark-color",
            "hasPlay": false
          },
          {
            "name": "InAGroup",
            "id": "patterns-color-option--in-agroup",
            "description": "Several options in a named group: each toggles on its own (any number of colors).",
            "hasPlay": true
          },
          {
            "name": "KeyboardToggle",
            "id": "patterns-color-option--keyboard-toggle",
            "description": "Every option is a tab stop; Space and Enter toggle.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "patterns-color-option--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "patterns-color-option--focused",
            "hasPlay": true
          },
          {
            "name": "Default",
            "id": "patterns-color-option-group--default",
            "hasPlay": true
          },
          {
            "name": "Preselected",
            "id": "patterns-color-option-group--preselected",
            "hasPlay": false
          },
          {
            "name": "WithDescription",
            "id": "patterns-color-option-group--with-description",
            "hasPlay": false
          },
          {
            "name": "DisabledItem",
            "id": "patterns-color-option-group--disabled-item",
            "hasPlay": false
          },
          {
            "name": "DisabledGroup",
            "id": "patterns-color-option-group--disabled-group",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "patterns-color-option-group--invalid",
            "hasPlay": true
          },
          {
            "name": "SingleSelection",
            "id": "patterns-color-option-group--single-selection",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "patterns-color-option-group--controlled",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "patterns-color-option-group--long-labels",
            "hasPlay": false
          },
          {
            "name": "SingleColumn",
            "id": "patterns-color-option-group--single-column",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "patterns-color-option-group--portuguese",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-color-option-group--right-to-left",
            "hasPlay": false
          },
          {
            "name": "KeyboardRoving",
            "id": "patterns-color-option-group--keyboard-roving",
            "description": "Arrow keys move between options and stop at the ends; Space toggles.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ColorOption/ColorOption.md",
        "summary": "A selectable option that shows a color swatch and the color's name, with an optional short mark. It is a toggle button: each option is chosen on its own. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/erp/ColorOption.jsx` (TODO `CMP-PAT-04`).",
        "whenToUse": [
          "Choose any number of colors from a list: the colors available for an item, the colors to filter by.",
          "When the color name must be visible next to the swatch."
        ],
        "whenNotToUse": [
          "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics).",
          "A free-form color input: use a color field.",
          "To display a color with no interaction: use `Swatch`."
        ],
        "keyboard": [],
        "examples": [
          "<div role=\"group\" aria-label=\"Available colors\">\n  <ColorOption color=\"#1f3a93\" label=\"Navy\" isSelected={on.navy} onChange={set('navy')} />\n  <ColorOption color=\"#c0392b\" label=\"Brick red\" mark=\"12 left\" />\n</div>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Choose any number of colors from a list: the colors available for an item, the colors to filter by.\n- When the color name must be visible next to the swatch."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics).\n- A free-form color input: use a color field.\n- To display a color with no interaction: use `Swatch`."
          },
          {
            "id": "semantics-decision-toggle-not-radio",
            "heading": "Semantics decision: toggle, not radio",
            "content": "The option exposes `aria-pressed` (a toggle button), not a radio. The handoff uses it for multi-select (\"which colors does this item come in?\"), and a radio group would force a single answer and a roving tab stop. React Aria `ColorSwatchPicker` was evaluated and does not fit: it is a single-select listbox of bare swatches with generated color names and no room for a visible label or mark. For single choice, use `ChoiceTiles`.\n\nGroup several options with `ColorOptionGroup` (a named group with a shared `value`, error text and roving arrow keys, see `ColorOptionGroup.md`). Outside a group every option is its own tab stop."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nbutton[aria-pressed]\n  Swatch (decorative)\n  label\n  mark | check            (mark text if given, otherwise a check while selected)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                          | Type                       | Default  | Notes                                                    |\n| --------------------------------------------- | -------------------------- | -------- | -------------------------------------------------------- |\n| `color`                                       | `string`                   | required | `#rgb` or `#rrggbb`.                                     |\n| `label`                                       | `string`                   | required | Visible name; accessible name of the button.             |\n| `mark`                                        | `ReactNode`                | none     | Short text after the label. Part of the accessible name. |\n| `isSelected` / `defaultSelected` / `onChange` | `boolean` / `(on) => void` | `false`  | React Aria names.                                        |\n| `isDisabled`                                  | `boolean`                  | `false`  | Disabled opacity token and a dashed border.              |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `aria-pressed` states the selection; the selected look is also a doubled accent border, a soft fill and a check (not color alone).\n- The swatch is decorative because the label names the color.\n- Boundaries use `--ds-line-control` (3:1); hover brightens the border, never the fill. Focus: accent border and ring. Forced colors: `Highlight` border when selected."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Label is the color name in sentence case (\"Brick red\"); `mark` is two or three words at most."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<div role=\"group\" aria-label=\"Available colors\">\n  <ColorOption color=\"#1f3a93\" label=\"Navy\" isSelected={on.navy} onChange={set('navy')} />\n  <ColorOption color=\"#c0392b\" label=\"Brick red\" mark=\"12 left\" />\n</div>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria `ToggleButton` (hover, press, focus and keyboard from RAC); no React hover state or inline styles.\n- The swatch is decorative; the disabled opacity is `--ds-opacity-disabled` (handoff 0.45); the border is `--ds-line-control` instead of `--ds-line`.\n- `selected` / `disabled` / `onClick` became `isSelected` / `isDisabled` / `onChange(isSelected)`. The mark (\"✓\" glyph) is an icon while selected, or your own `mark` text."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ChoiceTiles",
            "reason": "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics)."
          },
          {
            "name": "RadioGroup",
            "reason": "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics)."
          },
          {
            "name": "Swatch",
            "reason": "To display a color with no interaction: use `Swatch`."
          }
        ],
        "uses": ["Icon", "Swatch"],
        "usedBy": []
      }
    },
    {
      "name": "ColorOptionGroup",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ColorOptionGroup } from 'loui-ds/patterns';",
      "folder": "src/patterns/ColorOption",
      "sourceFile": "src/patterns/ColorOption/ColorOptionGroup.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A named group of `ColorOption`s that shares one value: the list of chosen option `id`s. It\nadds the group name (`label`, `aria-label` or `aria-labelledby`), helper text, an error linked\nwith `aria-describedby`, per-option `isDisabled` (set it on the `ColorOption`) and one tab\nstop with arrow keys between options (they stop at the ends, like `ToggleGroup`). `Space` and\n`Enter` choose the focused option.\n\nList the options alphabetically by label (AGENTS.md 5b); the group keeps the order you give.\nBuilt on React Aria `ToggleButtonGroup`.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The `ColorOption` children. Each needs an `id` that is unique in the group (its value)."
        },
        {
          "name": "selectionMode",
          "type": "'single' | 'multiple'",
          "required": false,
          "default": "'multiple'",
          "description": "`multiple` (default): any number of colors can be chosen (the group is a `toolbar` of toggle\nbuttons). `single`: at most one color (a `radiogroup`).",
          "values": ["single", "multiple"]
        },
        {
          "name": "value",
          "type": "readonly string[]",
          "required": false,
          "description": "The `id`s of the chosen options (controlled). Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "readonly string[]",
          "required": false,
          "description": "The `id`s of the options chosen at the start (uncontrolled)."
        },
        {
          "name": "onChange",
          "type": "(value: string[]) => void",
          "required": false,
          "description": "Called with the `id`s of all chosen options after the user chooses or clears one."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the options. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while `isInvalid` is set. Linked with `aria-describedby`."
        },
        {
          "name": "isInvalid",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Marks the group invalid: danger border on the unselected options and the error text."
        },
        {
          "name": "columns",
          "type": "number",
          "required": false,
          "default": "2",
          "description": "Maximum number of columns. Default `2`. The grid collapses to fewer columns on narrow\ncontainers so an option never gets narrower than `--ds-color-option-group-min-width`."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the group root."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the element that holds the options."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleGroupProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-bg",
        "--ds-color-option-group-columns",
        "--ds-color-option-group-gap",
        "--ds-color-option-group-min-width",
        "--ds-control-h-md",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-3",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Patterns/ColorOption",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-color-option--default",
            "hasPlay": true
          },
          {
            "name": "Selected",
            "id": "patterns-color-option--selected",
            "hasPlay": false
          },
          {
            "name": "WithMark",
            "id": "patterns-color-option--with-mark",
            "hasPlay": false
          },
          {
            "name": "SelectedWithMark",
            "id": "patterns-color-option--selected-with-mark",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "patterns-color-option--disabled",
            "hasPlay": false
          },
          {
            "name": "DisabledSelected",
            "id": "patterns-color-option--disabled-selected",
            "hasPlay": false
          },
          {
            "name": "LongLabel",
            "id": "patterns-color-option--long-label",
            "hasPlay": false
          },
          {
            "name": "LightColor",
            "id": "patterns-color-option--light-color",
            "hasPlay": false
          },
          {
            "name": "DarkColor",
            "id": "patterns-color-option--dark-color",
            "hasPlay": false
          },
          {
            "name": "InAGroup",
            "id": "patterns-color-option--in-agroup",
            "description": "Several options in a named group: each toggles on its own (any number of colors).",
            "hasPlay": true
          },
          {
            "name": "KeyboardToggle",
            "id": "patterns-color-option--keyboard-toggle",
            "description": "Every option is a tab stop; Space and Enter toggle.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "patterns-color-option--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "patterns-color-option--focused",
            "hasPlay": true
          },
          {
            "name": "Default",
            "id": "patterns-color-option-group--default",
            "hasPlay": true
          },
          {
            "name": "Preselected",
            "id": "patterns-color-option-group--preselected",
            "hasPlay": false
          },
          {
            "name": "WithDescription",
            "id": "patterns-color-option-group--with-description",
            "hasPlay": false
          },
          {
            "name": "DisabledItem",
            "id": "patterns-color-option-group--disabled-item",
            "hasPlay": false
          },
          {
            "name": "DisabledGroup",
            "id": "patterns-color-option-group--disabled-group",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "patterns-color-option-group--invalid",
            "hasPlay": true
          },
          {
            "name": "SingleSelection",
            "id": "patterns-color-option-group--single-selection",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "patterns-color-option-group--controlled",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "patterns-color-option-group--long-labels",
            "hasPlay": false
          },
          {
            "name": "SingleColumn",
            "id": "patterns-color-option-group--single-column",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "patterns-color-option-group--portuguese",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-color-option-group--right-to-left",
            "hasPlay": false
          },
          {
            "name": "KeyboardRoving",
            "id": "patterns-color-option-group--keyboard-roving",
            "description": "Arrow keys move between options and stop at the ends; Space toggles.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ColorOption/ColorOption.md",
        "summary": "A selectable option that shows a color swatch and the color's name, with an optional short mark. It is a toggle button: each option is chosen on its own. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/erp/ColorOption.jsx` (TODO `CMP-PAT-04`).",
        "whenToUse": [
          "Choose any number of colors from a list: the colors available for an item, the colors to filter by.",
          "When the color name must be visible next to the swatch."
        ],
        "whenNotToUse": [
          "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics).",
          "A free-form color input: use a color field.",
          "To display a color with no interaction: use `Swatch`."
        ],
        "keyboard": [],
        "examples": [
          "<div role=\"group\" aria-label=\"Available colors\">\n  <ColorOption color=\"#1f3a93\" label=\"Navy\" isSelected={on.navy} onChange={set('navy')} />\n  <ColorOption color=\"#c0392b\" label=\"Brick red\" mark=\"12 left\" />\n</div>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Choose any number of colors from a list: the colors available for an item, the colors to filter by.\n- When the color name must be visible next to the swatch."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics).\n- A free-form color input: use a color field.\n- To display a color with no interaction: use `Swatch`."
          },
          {
            "id": "semantics-decision-toggle-not-radio",
            "heading": "Semantics decision: toggle, not radio",
            "content": "The option exposes `aria-pressed` (a toggle button), not a radio. The handoff uses it for multi-select (\"which colors does this item come in?\"), and a radio group would force a single answer and a roving tab stop. React Aria `ColorSwatchPicker` was evaluated and does not fit: it is a single-select listbox of bare swatches with generated color names and no room for a visible label or mark. For single choice, use `ChoiceTiles`.\n\nGroup several options with `ColorOptionGroup` (a named group with a shared `value`, error text and roving arrow keys, see `ColorOptionGroup.md`). Outside a group every option is its own tab stop."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nbutton[aria-pressed]\n  Swatch (decorative)\n  label\n  mark | check            (mark text if given, otherwise a check while selected)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                          | Type                       | Default  | Notes                                                    |\n| --------------------------------------------- | -------------------------- | -------- | -------------------------------------------------------- |\n| `color`                                       | `string`                   | required | `#rgb` or `#rrggbb`.                                     |\n| `label`                                       | `string`                   | required | Visible name; accessible name of the button.             |\n| `mark`                                        | `ReactNode`                | none     | Short text after the label. Part of the accessible name. |\n| `isSelected` / `defaultSelected` / `onChange` | `boolean` / `(on) => void` | `false`  | React Aria names.                                        |\n| `isDisabled`                                  | `boolean`                  | `false`  | Disabled opacity token and a dashed border.              |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `aria-pressed` states the selection; the selected look is also a doubled accent border, a soft fill and a check (not color alone).\n- The swatch is decorative because the label names the color.\n- Boundaries use `--ds-line-control` (3:1); hover brightens the border, never the fill. Focus: accent border and ring. Forced colors: `Highlight` border when selected."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Label is the color name in sentence case (\"Brick red\"); `mark` is two or three words at most."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<div role=\"group\" aria-label=\"Available colors\">\n  <ColorOption color=\"#1f3a93\" label=\"Navy\" isSelected={on.navy} onChange={set('navy')} />\n  <ColorOption color=\"#c0392b\" label=\"Brick red\" mark=\"12 left\" />\n</div>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria `ToggleButton` (hover, press, focus and keyboard from RAC); no React hover state or inline styles.\n- The swatch is decorative; the disabled opacity is `--ds-opacity-disabled` (handoff 0.45); the border is `--ds-line-control` instead of `--ds-line`.\n- `selected` / `disabled` / `onClick` became `isSelected` / `isDisabled` / `onChange(isSelected)`. The mark (\"✓\" glyph) is an icon while selected, or your own `mark` text."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ChoiceTiles",
            "reason": "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics)."
          },
          {
            "name": "RadioGroup",
            "reason": "To choose exactly one color: use `ChoiceTiles` or `RadioGroup` (radio semantics)."
          },
          {
            "name": "Swatch",
            "reason": "To display a color with no interaction: use `Swatch`."
          }
        ],
        "uses": ["ErrorText", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "Combobox",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Combobox } from 'loui-ds';",
      "folder": "src/components/Combobox",
      "sourceFile": "src/components/Combobox/Combobox.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A text input with a filtered list of options in a popover (ARIA combobox pattern, built on\nReact Aria `ComboBox`): the input has `role=\"combobox\"` with `aria-expanded`,\n`aria-controls`, `aria-autocomplete=\"list\"` and `aria-activedescendant`, so focus stays in\nthe input while the arrow keys move through the list.\n\nTyping filters the options accent- and case-insensitively. `Enter` selects the highlighted\noption, `Escape` closes the list and then restores the selected text, `ArrowDown` / `ArrowUp`\nopen the list and move, `Home` / `End` move the caret in the input (React Aria keeps them for\ntext editing). With `allowsCustomValue` the typed text itself is a valid value.\n\nUse it for long lists (10 or more options); for fewer prefer `Select`. `onCreate` adds a\n`Create \"text\"` row for text that matches no option (see Combobox.md).",
      "props": [
        {
          "name": "options",
          "type": "readonly (ComboboxOption | ComboboxGroup)[]",
          "required": true,
          "description": "The options, or groups of options. Keep the order you want users to see (see Combobox.md)."
        },
        {
          "name": "value",
          "type": "string | null",
          "required": false,
          "description": "The selected option `value`; `null` means nothing is selected. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "string | null",
          "required": false,
          "description": "The initially selected `value` when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((value: string | null) => void)",
          "required": false,
          "description": "Called with the selected `value`, or `null` when the selection is cleared."
        },
        {
          "name": "filter",
          "type": "false | ((label: string, query: string, option: ComboboxOption) => boolean)",
          "required": false,
          "description": "How options are matched against the typed text. Default: accent- and case-insensitive\n\"contains\" on the label (`matchesSearch`), so `cafe` finds `Café`. Pass your own function,\nor `false` to show `options` as given: use it when the list is loaded from a server as the\nuser types (listen to `onInputChange`, update `options`, set `isLoading` meanwhile).\n\nThe function receives the whole option as a third argument, so you can search by a field\nthat is not the label (`option.value` as a code, `option.meta` as an SKU). Two-argument\nfunctions keep working. Options that share a label are matched together: give each option\na distinct label when the filter looks at other fields."
        },
        {
          "name": "selectFirstOnEnter",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Press `Enter` with no option highlighted to select the first matching option (typing\n\"tri\" then `Enter` picks \"Trimestral\"). Default `false`: the WAI-ARIA editable combobox\ndoes not auto-select, so `Enter` without a highlighted option keeps the typed text (and\nsubmits the surrounding form). Turn it on for lookup fields where the typed text alone is\nnever a valid value. Ignored while the list is closed, empty or has only disabled options."
        },
        {
          "name": "onCreate",
          "type": "((inputValue: string) => void)",
          "required": false,
          "description": "Turns on the \"create new\" row. While the typed text is not blank and matches no option label\n(accent- and case-insensitive), the list ends with a `Create \"text\"` row (message\n`createOption`). Choosing it (click, or `ArrowDown` then `Enter`) calls `onCreate` with the\ntrimmed text and does not call `onChange`; the typed text is never replaced by the row's\ntext. Add the option to `options` in the handler and set `value` to it: the input then shows\nits label. Pair it with a controlled `value`. If the handler selects nothing, the input\nempties when the list closes, as in any combobox without `allowsCustomValue`; with\n`allowsCustomValue` the typed text stays as the value."
        },
        {
          "name": "isLoading",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Shows a spinner in the box and, when there are no options yet, a \"Loading\" row instead of\n\"No results\". The consumer owns the data: see `filter={false}` and `onInputChange`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the box is empty. Never a substitute for the label."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the options."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<input>` element."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ noResults: string; loading: string; showOptions: string; createOption: (value: string) => string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `noResults`, `loading`, `showOptions`, `createOption`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaComboBoxProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "shouldFocusWrap",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ComboBoxProps",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "allowsCustomValue",
              "type": "boolean",
              "required": false
            },
            {
              "name": "defaultInputValue",
              "type": "string",
              "required": false
            },
            {
              "name": "inputValue",
              "type": "string",
              "required": false
            },
            {
              "name": "menuTrigger",
              "type": "'focus' | 'input' | 'manual'",
              "required": false
            },
            {
              "name": "onInputChange",
              "type": "(value: string) => void",
              "required": false
            },
            {
              "name": "onOpenChange",
              "type": "(isOpen: boolean, menuTrigger?: MenuTriggerAction) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: ComboBoxValidationValue<\"single\">) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Combobox",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "noResults",
            "kind": "string",
            "default": "No results",
            "description": "Shown in the list when the typed text matches no option."
          },
          {
            "key": "loading",
            "kind": "string",
            "default": "Loading",
            "description": "Shown in the list while options are loading and there are none yet; also names the spinner."
          },
          {
            "key": "showOptions",
            "kind": "string",
            "default": "Show options",
            "description": "Accessible name of the button that opens the list."
          },
          {
            "key": "createOption",
            "kind": "function",
            "default": "Create “{value}”",
            "signature": "(value: string)",
            "description": "Last row of the list when `onCreate` is set and the typed text matches no option."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-combobox-popover-max-height",
        "--ds-field-control-padding-inline",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-6",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-z-dropdown"
      ],
      "stories": {
        "title": "Components/Combobox",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-combobox--default",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-combobox--open",
            "description": "The suggestions open under the field, for the visual review (TEST-020).",
            "hasPlay": true
          },
          {
            "name": "KeyboardContract",
            "id": "components-combobox--keyboard-contract",
            "hasPlay": true
          },
          {
            "name": "WithDescriptionAndMeta",
            "id": "components-combobox--with-description-and-meta",
            "hasPlay": true
          },
          {
            "name": "SelectWithPointer",
            "id": "components-combobox--select-with-pointer",
            "description": "Pointer path: filter by typing, click the suggestion, the list closes and the value stays. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "WithSelection",
            "id": "components-combobox--with-selection",
            "hasPlay": true
          },
          {
            "name": "Sections",
            "id": "components-combobox--sections",
            "hasPlay": true
          },
          {
            "name": "DisabledOption",
            "id": "components-combobox--disabled-option",
            "hasPlay": false
          },
          {
            "name": "NoResults",
            "id": "components-combobox--no-results",
            "hasPlay": true
          },
          {
            "name": "CustomValue",
            "id": "components-combobox--custom-value",
            "hasPlay": true
          },
          {
            "name": "Loading",
            "id": "components-combobox--loading",
            "hasPlay": true
          },
          {
            "name": "AsyncFromServer",
            "id": "components-combobox--async-from-server",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-combobox--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-combobox--invalid",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-combobox--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-combobox--read-only",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-combobox--sizes",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-combobox--long-text",
            "hasPlay": false
          },
          {
            "name": "WithLeading",
            "id": "components-combobox--with-leading",
            "description": "A `leading` node on each option; the selected one also shows at the start of the input.",
            "hasPlay": true
          },
          {
            "name": "CreateNew",
            "id": "components-combobox--create-new",
            "description": "Typed text that matches nothing ends the list with a Create row; choosing it selects the new option.",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-combobox--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Combobox/Combobox.md",
        "summary": "A text input with a filtered list of options in a popover (ARIA combobox pattern). Built on React Aria `ComboBox` (`Input`, `Button`, `Popover`, `ListBox`). Shared field behavior is in [Field](../Field/Field.md). Task: CMP-INP-01.",
        "whenToUse": [
          "Choosing from a long list (10 or more options): customers, products, cities.",
          "Choosing from a list or typing a new value (`allowsCustomValue`).",
          "Options loaded from a server as the user types (see Async)."
        ],
        "whenNotToUse": [
          "2 to 4 options: `RadioGroup`. 4 to 9 options: `Select`.",
          "Searching the whole app or a table: `SearchInput`.",
          "Several selections: not supported yet (multiple selection is P3)."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Choosing from a long list (10 or more options): customers, products, cities.\n- Choosing from a list or typing a new value (`allowsCustomValue`).\n- Options loaded from a server as the user types (see Async)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- 2 to 4 options: `RadioGroup`. 4 to 9 options: `Select`.\n- Searching the whole app or a table: `SearchInput`.\n- Several selections: not supported yet (multiple selection is P3)."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                                                                                   | Notes                                                                                                                                                                                                                                                                                            |\n| -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `label` / `aria-label` / `aria-labelledby`                                                                                             | One is required.                                                                                                                                                                                                                                                                                 |\n| `options`                                                                                                                              | `ComboboxOption[]` (`value`, `label`, `description`, `meta`, `leading`, `isDisabled`) or groups `{ id, label, options }`.                                                                                                                                                                        |\n| `value` / `defaultValue` / `onChange(value)`                                                                                           | The selected option `value`, `null` for none.                                                                                                                                                                                                                                                    |\n| `inputValue` / `defaultInputValue` / `onInputChange`                                                                                   | The text in the box (React Aria props, passed through).                                                                                                                                                                                                                                          |\n| `allowsCustomValue`                                                                                                                    | The typed text is a valid value; the form then receives the text.                                                                                                                                                                                                                                |\n| `filter`                                                                                                                               | Default accent- and case-insensitive \"contains\" on the label (`matchesSearch`). A function `(label, query, option) => boolean` (the whole option is the third argument, so you can search a code in `value` or `meta`; two-argument functions keep working), or `false` for no client filtering. |\n| `onCreate(text)`                                                                                                                       | Adds a `Create “text”` row when the typed text matches no option; see Create new below.                                                                                                                                                                                                          |\n| `fullWidth`                                                                                                                            | In an `Inline` row the field takes the free space (`flex: 1 1 auto`, can shrink). Off by default; in a block context the field already spans the width.                                                                                                                                          |\n| `selectFirstOnEnter`                                                                                                                   | Default `false`. `Enter` with no option highlighted selects the first enabled match (typing \"tri\" then `Enter` picks \"Trimestral\").                                                                                                                                                              |\n| `isLoading`                                                                                                                            | Spinner in the box; \"Loading\" row when there are no options yet.                                                                                                                                                                                                                                 |\n| `menuTrigger`                                                                                                                          | `input` (default), `focus` or `manual`.                                                                                                                                                                                                                                                          |\n| `description`, `descriptionTone`, `errorMessage`, `size`, `placeholder`, `name`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | As in `Select`.                                                                                                                                                                                                                                                                                  |\n| `messages`                                                                                                                             | `noResults`, `loading`, `showOptions`, `createOption`, `requiredMark`.                                                                                                                                                                                                                           |\n\nOption order is yours (see `Select.md`): sort with `sortByText` unless the order carries meaning."
          },
          {
            "id": "option-leading-swatch-icon-avatar",
            "heading": "Option leading (swatch, icon, avatar)",
            "content": "`leading` on an option puts a node before its label (a `Swatch`, an `Icon`, an `Avatar`). It is hidden from assistive technology: the option name stays its text (label, meta, description), so put anything that carries meaning in the text, never only in the node. While an option with `leading` is selected and the input shows its label, the same node appears at the start of the input (a colour picker shows the chosen colour); it disappears as soon as the text is edited.\n\n```tsx\n<Combobox\n  label=\"Colour\"\n  options={[{ value: 'navy', label: 'Navy', leading: <Swatch color=\"#1f3a5f\" decorative /> }]}\n/>\n```"
          },
          {
            "id": "create-new-oncreate",
            "heading": "Create new (`onCreate`)",
            "content": "While the typed text is not blank and matches no option label (accent- and case-insensitive), the list ends with a `Create “text”` row (message `createOption`; `Criar “text”` in pt-BR). It is a normal option: `ArrowDown` reaches it, `Enter` or a click chooses it. Choosing it calls `onCreate` with the trimmed text and does **not** call `onChange`, and the typed text is never replaced by the row's text. In the handler add the option to `options` and set `value` to it; the input then shows its label.\n\n```tsx\n<Combobox\n  label=\"Label\"\n  options={labels}\n  value={value}\n  onChange={setValue}\n  onCreate={(name) => {\n    const created = { value: slug(name), label: name };\n    setLabels((list) => [...list, created]);\n    setValue(created.value);\n  }}\n/>\n```\n\nKeep `value` controlled. If the handler selects nothing, the input empties when the list closes, as in any combobox without `allowsCustomValue`; with `allowsCustomValue` the typed text stays as the value (there the Create row is rarely needed). The row is not offered when the field is read-only. Creating is reversible: offer Undo with `useUndoable`. The full flow (create, select, toast with Undo) is the `create-on-the-fly` recipe (`docs/recipes/create-on-the-fly.md`)."
          },
          {
            "id": "async-options-documented-pattern-a-full-async-list-is-p2",
            "heading": "Async options (documented pattern; a full async list is P2)",
            "content": "Set `filter={false}`, keep the query from `onInputChange`, fetch, and pass the result as `options` with `isLoading` meanwhile. The component never fetches and does not virtualize; for thousands of options filter on the server."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                     | Behavior                                                                                         |\n| ----------------------- | ------------------------------------------------------------------------------------------------ |\n| Typing                  | Opens the list and filters                                                                       |\n| `ArrowDown` / `ArrowUp` | Open the list (first / last option) and move; focus stays in the input (`aria-activedescendant`) |\n| `Enter`                 | Select the highlighted option and close                                                          |\n| `Escape`                | Close the list; pressed again, restore the selected text (clear with `allowsCustomValue`)        |\n| `Home` / `End`          | Move the caret in the input (React Aria keeps them for text editing, not for the list)           |\n| `Tab`                   | Commit and leave                                                                                 |\n\n`Enter` with nothing highlighted keeps the typed text and selects nothing, as in the WAI-ARIA editable combobox. Turn on `selectFirstOnEnter` for lookup fields where the typed text alone is never a valid value (it does nothing while the list is closed, empty or has only disabled options; a highlighted option always wins).\n\nHotkeys: A consumer `onKeyDown` does not hide keys from `useHotkeys(..., { allowInEditable: true })` shortcuts: a key keeps bubbling unless your handler called `event.preventDefault()` on it. Recipe: `Enter` confirms and `Escape` cancels (both with `preventDefault()`), every other key stays a hotkey. See `docs/architecture/hotkeys.md`.\n\nThe input has `role=\"combobox\"`, `aria-expanded`, `aria-controls`, `aria-autocomplete=\"list\"`. The chevron button (\"Show options\") is out of the tab order. While the list is open React Aria hides the rest of the page from assistive technology. An option's accessible name contains label, meta and description. The empty state is a single option-like row so screen readers announce it."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the noun (\"Customer\"). Empty state text is \"No results\" (sentence case, no punctuation); localize with `messages`."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Combobox\n  label=\"Customer\"\n  name=\"customer\"\n  options={sortByText(customers, (c) => c.label, locale)}\n  onChange={setCustomerId}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A real combobox: the search box is the field itself (the handoff had a button that opened a popover with an unlabelled search input), with ARIA wiring, `Escape` behavior and form submission of the option value.\n- Filtering is accent-insensitive (the handoff used `toLowerCase`).\n- `searchPlaceholder`, `emptyText` and `placeholder` strings became messages and `placeholder`; `meta` and `description` are kept/added; sections, custom values, loading, sizes and invalid/disabled/read-only states are new.\n- The popover is limited to the viewport width: a full-width box on a narrow screen no longer overflows the page."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "RadioGroup",
            "reason": "2 to 4 options: `RadioGroup`. 4 to 9 options: `Select`."
          },
          {
            "name": "Select",
            "reason": "2 to 4 options: `RadioGroup`. 4 to 9 options: `Select`."
          },
          {
            "name": "SearchInput",
            "reason": "Searching the whole app or a table: `SearchInput`."
          }
        ],
        "uses": ["FieldMessages", "Icon", "Label", "Spinner"],
        "usedBy": []
      }
    },
    {
      "name": "CommandPalette",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { CommandPalette } from 'loui-ds';",
      "folder": "src/components/CommandPalette",
      "sourceFile": "src/components/CommandPalette/CommandPalette.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A command palette: a modal with a search field and a grouped list of commands, opened with\n`Mod+K` (configurable). Built on React Aria `Autocomplete`, `SearchField` and `ListBox` inside a\n`ModalOverlay`: the search field is a combobox that keeps focus while the arrow keys move\nthrough the commands (`aria-activedescendant`), `Enter` runs the highlighted command and\ncloses, `Escape` closes and focus returns to where it was.\n\nSearch ignores case and accents and matches label, keywords and group. With `includeHotkeys`\nit also lists the shortcuts registered through `useHotkeys`.",
      "props": [
        {
          "name": "commands",
          "type": "readonly Command[]",
          "required": true,
          "description": "The commands. Search covers the label, the keywords and the group."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "((isOpen: boolean) => void)",
          "required": false,
          "description": "Called when the palette opens (from the hotkey) or closes (Escape, outside press, a command ran)."
        },
        {
          "name": "hotkey",
          "type": "string | false",
          "required": false,
          "default": "Mod+K",
          "description": "The shortcut that opens the palette, registered in the hotkey registry. Default `'Mod+K'`.\nIt works while an input has focus. Pass `false` to open the palette only through `isOpen`."
        },
        {
          "name": "includeHotkeys",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Also list every shortcut registered with `useHotkeys` that has a `description`, as a command\nthat runs its handler (grouped by the shortcut's `group`, or under \"Shortcuts\"). A registered\nshortcut whose description equals a command's label is listed once. Default `false`."
        },
        {
          "name": "recentIds",
          "type": "readonly string[]",
          "required": false,
          "description": "Ids of recently run commands, most recent first, shown under \"Recent\" while the search is\nempty. Persisting them is up to the app: collect them in `onRun`."
        },
        {
          "name": "maxRecent",
          "type": "number",
          "required": false,
          "default": "5",
          "description": "Most recent commands shown. Default `5`."
        },
        {
          "name": "onRun",
          "type": "((commandId: string) => void)",
          "required": false,
          "description": "Called after a command was chosen, with its id, so the app can update `recentIds`."
        },
        {
          "name": "sort",
          "type": "'alphabetical' | 'given'",
          "required": false,
          "default": "'alphabetical'",
          "description": "Order of groups and commands. `alphabetical` (default) sorts both with the active locale,\nignoring accents; `given` keeps the order of `commands` and lists groups in order of first use.",
          "values": ["alphabetical", "given"]
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Placeholder and accessible name of the search field. Default from messages: \"Search or run a command\"."
        },
        {
          "name": "messages",
          "type": "Partial<{ title: string; placeholder: string; commands: string; recent: string; ungrouped: string; shortcuts: string; noResults: string; noResultsHint: string; openDescription: string; openGroup: string; resultCount: (n: number) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the palette panel."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the panel (`role=\"dialog\"`)."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "CommandPalette",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "title",
            "kind": "string",
            "default": "Command palette",
            "description": "Accessible name of the palette dialog."
          },
          {
            "key": "placeholder",
            "kind": "string",
            "default": "Search or run a command",
            "description": "Placeholder and accessible name of the search field."
          },
          {
            "key": "commands",
            "kind": "string",
            "default": "Commands",
            "description": "Accessible name of the list of commands."
          },
          {
            "key": "recent",
            "kind": "string",
            "default": "Recent",
            "description": "Heading of the group of recently run commands."
          },
          {
            "key": "ungrouped",
            "kind": "string",
            "default": "Commands",
            "description": "Heading for commands that have no `group`."
          },
          {
            "key": "shortcuts",
            "kind": "string",
            "default": "Shortcuts",
            "description": "Heading for registered shortcuts that have no `group` (when `includeHotkeys` is set)."
          },
          {
            "key": "noResults",
            "kind": "string",
            "default": "No commands found",
            "description": "Shown when no command matches the search."
          },
          {
            "key": "noResultsHint",
            "kind": "string",
            "default": "Try another search term.",
            "description": "Second line of the empty state: what to do next."
          },
          {
            "key": "openDescription",
            "kind": "string",
            "default": "Open the command palette",
            "description": "Description of the shortcut that opens the palette, listed by `ShortcutsHelp`."
          },
          {
            "key": "openGroup",
            "kind": "string",
            "default": "General",
            "description": "Group of the shortcut that opens the palette."
          },
          {
            "key": "resultCount",
            "kind": "function",
            "default": "2 commands",
            "signature": "(n: number)",
            "description": "Spoken after each search: how many commands match."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-command-palette-list-max-height",
        "--ds-command-palette-offset-top",
        "--ds-command-palette-width",
        "--ds-control-h-lg",
        "--ds-dialog-gutter",
        "--ds-dialog-padding",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-body",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-overlay",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-shadow-dialog",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-space-5",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/CommandPalette",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-command-palette--default",
            "description": "The keyboard contract: open with Ctrl+K, type, arrows move, Enter runs and closes, focus returns.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-command-palette--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Filtered",
            "id": "components-command-palette--filtered",
            "description": "Typing filters by label, keywords and group, ignoring case and accents; the match is highlighted.",
            "hasPlay": true
          },
          {
            "name": "Empty",
            "id": "components-command-palette--empty",
            "description": "Nothing matches: say so and what to do. Escape still closes, even with text in the field.",
            "hasPlay": true
          },
          {
            "name": "WithRecent",
            "id": "components-command-palette--with-recent",
            "description": "Recently run commands lead the list while the search is empty. The app stores the ids.",
            "hasPlay": false
          },
          {
            "name": "GivenOrder",
            "id": "components-command-palette--given-order",
            "description": "Commands in the order you give them, groups in order of first use.",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-command-palette--long-labels",
            "description": "A command whose label is long wraps instead of pushing the shortcut out.",
            "hasPlay": false
          },
          {
            "name": "WithRegisteredHotkeys",
            "id": "components-command-palette--with-registered-hotkeys",
            "description": "`includeHotkeys` lists every described shortcut registered with `useHotkeys` and runs its handler.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-command-palette--controlled",
            "description": "Controlled: the app owns the open state, for example to open it from a toolbar button.",
            "hasPlay": true
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-command-palette--portuguese-brazil",
            "description": "Brazilian Portuguese: placeholder, headings, empty state and the spoken key names.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-command-palette--right-to-left",
            "description": "Right to left: the icon, the label and the shortcut mirror; the arrow keys keep moving up and down.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/CommandPalette/CommandPalette.md",
        "summary": "A modal search box that finds and runs commands from the keyboard. It opens with `Mod+K` (Command K on Apple platforms, Control K elsewhere), filters a grouped list as you type and runs the chosen command. It is the \"⌘K for everything\" of AGENTS.md section 5.",
        "whenToUse": [
          "An app with many actions or destinations that should all be reachable without the mouse: navigate, create, filter, switch.",
          "Together with `useHotkeys`, so every shortcut is also discoverable by name (`includeHotkeys`)."
        ],
        "whenNotToUse": [
          "Searching data (orders, customers): use `SearchInput` with suggestions, or a results page. The palette runs commands; a command may open that search.",
          "Choosing a value in a form: use `Combobox` or `Select`.",
          "A short list of actions on one object: use `Menu`."
        ],
        "keyboard": [
          "Keyboard: `Mod+K` opens, `↑ ↓` move, `Enter` runs, `Esc` closes. Hover highlights like the keyboard does."
        ],
        "examples": [
          "<HotkeyProvider>\n  <CommandPalette\n    commands={[\n      {\n        id: 'orders',\n        label: 'Go to orders',\n        group: 'Navigation',\n        shortcut: '1',\n        onAction: () => navigate('/orders'),\n      },\n      {\n        id: 'new',\n        label: 'Create order',\n        group: 'Orders',\n        keywords: ['invoice'],\n        shortcut: 'F2',\n        onAction: createOrder,\n      },\n    ]}\n    includeHotkeys\n    recentIds={recent}\n    onRun={(id) => setRecent((ids) => [id, ...ids.filter((x) => x !== id)].slice(0, 5))}\n  />\n</HotkeyProvider>",
          "const [open, setOpen] = useState(false);\n<Button onPress={() => setOpen(true)}>Commands</Button>\n<CommandPalette commands={commands} isOpen={open} onOpenChange={setOpen} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- An app with many actions or destinations that should all be reachable without the mouse: navigate, create, filter, switch.\n- Together with `useHotkeys`, so every shortcut is also discoverable by name (`includeHotkeys`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Searching data (orders, customers): use `SearchInput` with suggestions, or a results page. The palette runs commands; a command may open that search.\n- Choosing a value in a form: use `Combobox` or `Select`.\n- A short list of actions on one object: use `Menu`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nModalOverlay (dims the page, dialog layer)\n  Modal\n    Dialog  role=\"dialog\"  aria-label=\"Command palette\"\n      SearchField / Input  role=\"combobox\"  (focus stays here)\n      ListBox  role=\"listbox\"  aria-label=\"Commands\"\n        Section \"Recent\"                (only while the search is empty)\n        Section <group>\n          Option  icon · label (match highlighted) · shortcut (Kbd)\n      status    \"3 commands\"            (visually hidden live region)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop             | Type                          | Default          | Notes                                                                                      |\n| ---------------- | ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------ |\n| `commands`       | `Command[]`                   | required         | `id`, `label`, `group`, `keywords`, `shortcut`, `icon`, `onAction`, `isDisabled`           |\n| `isOpen`         | `boolean`                     | -                | Controlled open state. Pair with `onOpenChange`                                            |\n| `defaultOpen`    | `boolean`                     | `false`          | Initial state when uncontrolled                                                            |\n| `onOpenChange`   | `(isOpen: boolean) => void`   | -                | Opens from the hotkey; closes on Escape, outside press or after a command                  |\n| `hotkey`         | `string \\| false`             | `'Mod+K'`        | Registered in the hotkey registry. Works from inside inputs. `false`: no hotkey            |\n| `includeHotkeys` | `boolean`                     | `false`          | Also lists described shortcuts registered with `useHotkeys`; running one calls its handler |\n| `recentIds`      | `string[]`                    | -                | Most recent first. The app stores them (collect with `onRun`)                              |\n| `maxRecent`      | `number`                      | `5`              | Most recent commands shown                                                                 |\n| `onRun`          | `(commandId: string) => void` | -                | Called when a command is chosen                                                            |\n| `sort`           | `'alphabetical' \\| 'given'`   | `'alphabetical'` | Groups and commands sorted with the locale, ignoring accents, or as given                  |\n| `placeholder`    | `string`                      | message          | \"Search or run a command\"                                                                  |\n| `messages`       | `Partial<messages>`           | -                | Per-instance overrides (see below)                                                         |\n\n`Command.shortcut` is display only: a combo (`'Mod+Shift+P'`, `'F2'`) or `Kbd` keys (`['Mod', 'K']`). It does not bind the key."
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- Search matches the label, the `keywords` and the group, ignoring case and accents (`cafe` finds \"Open café report\"). The matching part of the label is highlighted. Disabled commands stay searchable but cannot run.\n- The first command is highlighted when the palette opens and after each typed character, so `Enter` runs it at once. `ArrowUp` / `ArrowDown` move the highlight; focus stays in the field.\n- `Enter` (or a click) runs the command and closes the palette. The command runs once the palette has unmounted, so a dialog it opens is not fought over by focus restoration.\n- `Escape` always closes, even when the field has text (no \"clear first\" step), and focus returns to the element that had it.\n- The search text resets every time the palette opens.\n- Shortcuts of the page do not fire while the palette is open (see `docs/architecture/hotkeys.md`).\n- `includeHotkeys`: a registered shortcut whose description equals a command's label is listed once (the command wins); the palette's own shortcut is not listed."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A modal `dialog` named \"Command palette\". Focus is trapped, the page behind is inert, focus returns on close (React Aria).\n- The field follows the WAI-ARIA combobox pattern with a listbox popup: `role=\"combobox\"`, `aria-controls`, `aria-expanded=\"true\"` and `aria-activedescendant` for the highlighted option. React Aria delays moving `aria-activedescendant` by about 500 ms while typing so screen readers are not interrupted by the highlight change.\n- Groups are `role=\"group\"` named by their heading. Disabled options are `aria-disabled`.\n- After each search a visually hidden `role=\"status\"` says how many commands match (\"3 commands\", \"No commands found\").\n- Shortcuts are drawn with `Kbd`, which adds spoken key names (\"Control Shift O\") to the option's name.\n- Keyboard: `Mod+K` opens, `↑ ↓` move, `Enter` runs, `Esc` closes. Hover highlights like the keyboard does.\n- Motion honors `prefers-reduced-motion`; `forced-colors` gets a system border and highlight outline."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are a verb plus an object in sentence case: \"Create order\", \"Go to customers\".\n- Group names are nouns: \"Navigation\", \"Orders\". Use `keywords` for synonyms and other languages instead of long labels.\n- Messages: `title`, `placeholder`, `commands`, `recent`, `ungrouped`, `shortcuts`, `noResults`, `noResultsHint`, `openDescription`, `openGroup`, `resultCount(n)`. English and pt-BR ship; override any of them per instance or through `DSProvider messages={{ CommandPalette: { ... } }}`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<HotkeyProvider>\n  <CommandPalette\n    commands={[\n      {\n        id: 'orders',\n        label: 'Go to orders',\n        group: 'Navigation',\n        shortcut: '1',\n        onAction: () => navigate('/orders'),\n      },\n      {\n        id: 'new',\n        label: 'Create order',\n        group: 'Orders',\n        keywords: ['invoice'],\n        shortcut: 'F2',\n        onAction: createOrder,\n      },\n    ]}\n    includeHotkeys\n    recentIds={recent}\n    onRun={(id) => setRecent((ids) => [id, ...ids.filter((x) => x !== id)].slice(0, 5))}\n  />\n</HotkeyProvider>\n```\n\nOpen it from a button instead of (or as well as) the hotkey:\n\n```tsx\nconst [open, setOpen] = useState(false);\n<Button onPress={() => setOpen(true)}>Commands</Button>\n<CommandPalette commands={commands} isOpen={open} onOpenChange={setOpen} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff had no command palette; the ERP bound F2/F9/N/Enter/Esc/1-4 directly. Here those keys are ordinary `useHotkeys` registrations and the palette lists them.\n- Size, list height and offset are tokens (`--ds-command-palette-*`)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SearchInput",
            "reason": "Searching data (orders, customers): use `SearchInput` with suggestions, or a results page. The palette runs commands; a command may open that search."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value in a form: use `Combobox` or `Select`."
          },
          {
            "name": "Select",
            "reason": "Choosing a value in a form: use `Combobox` or `Select`."
          },
          {
            "name": "Menu",
            "reason": "A short list of actions on one object: use `Menu`."
          }
        ],
        "uses": ["Icon", "Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "ConfirmDialog",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ConfirmDialog } from 'loui-ds';",
      "folder": "src/components/Dialog",
      "sourceFile": "src/components/Dialog/ConfirmDialog.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A destructive-action confirmation: an `alertdialog` with the question as the title, the\nconsequence in one sentence, a Cancel button and a confirm button that names the action.\n\nInitial focus is on **Cancel**, so pressing Enter or Space right after the dialog opens never\nconfirms. Outside presses do not dismiss it (`Escape` and Cancel do), because the user has to\nanswer. Follow a confirmed delete with a `Toast` that offers Undo when the data can come back.",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": true,
          "description": "The question, naming the object: \"Delete order #1048?\". Not \"Are you sure?\" and not\n\"Confirm\"."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": true,
          "description": "The consequence, in one sentence: \"The order and its 14 lines will be removed. This can't\nbe undone.\" Required, because a confirmation without a consequence is only a speed bump."
        },
        {
          "name": "confirmLabel",
          "type": "string",
          "required": true,
          "description": "Label of the confirming button. Names the action, verb plus object: \"Delete order\". Never\n\"OK\", \"Yes\" or \"Confirm\"."
        },
        {
          "name": "onConfirm",
          "type": "() => void",
          "required": true,
          "description": "Called when the user confirms. The dialog closes afterwards unless `closeOnConfirm` is `false`."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Optional extra content between the description and the buttons (for example a list of affected items)."
        },
        {
          "name": "tone",
          "type": "'danger' | 'neutral'",
          "required": false,
          "default": "'danger'",
          "description": "`danger` (default): red `danger-solid` button for a destructive action. `neutral`: the `primary` button.",
          "values": ["danger", "neutral"]
        },
        {
          "name": "initialFocus",
          "type": "'cancel' | 'confirm'",
          "required": false,
          "default": "'cancel'",
          "description": "Which button gets the initial focus. `cancel` (default) is the safe choice and the\nproduct principle for destructive actions; `confirm` is for non-destructive confirmations.",
          "values": ["cancel", "confirm"]
        },
        {
          "name": "closeOnConfirm",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Keep the dialog open after `onConfirm`, for example while an async request runs. Default `true` closes it."
        },
        {
          "name": "isPending",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Shows a spinner on the confirm button and ignores presses while a request runs."
        },
        {
          "name": "messages",
          "type": "Partial<{ cancel: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Maximum width: `sm` 420px (default), `md` 540px, `lg` 680px. Narrower on small screens.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "trigger",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": false,
          "description": "The element that opens the dialog (a `Button`). Optional: omit it and control the dialog\nwith `isOpen`. Focus returns to the element that was focused when the dialog opened."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the dialog opens or closes."
        },
        {
          "name": "isDismissable",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Whether pressing the overlay closes the dialog. Default `true`; `ConfirmDialog` sets `false`."
        },
        {
          "name": "isKeyboardDismissDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables closing with `Escape`. Use it only while a request is running."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the dialog panel."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the dialog panel."
        }
      ],
      "inheritedProps": [],
      "variants": {
        "size": ["sm", "md", "lg"],
        "tone": ["danger", "neutral"]
      },
      "messages": {
        "providerKey": "Dialog",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "cancel",
            "kind": "string",
            "default": "Cancel",
            "description": "Label of the button that dismisses a `ConfirmDialog` without doing anything."
          }
        ]
      },
      "cssVariables": [
        "--ds-dialog-gutter",
        "--ds-dialog-padding",
        "--ds-dialog-width-lg",
        "--ds-dialog-width-md",
        "--ds-dialog-width-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-overlay",
        "--ds-radius-lg",
        "--ds-shadow-dialog",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/ConfirmDialog",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-confirm-dialog--default",
            "description": "Initial focus is on Cancel, so Enter right after opening cancels instead of deleting.",
            "hasPlay": true
          },
          {
            "name": "ConfirmByKeyboard",
            "id": "components-confirm-dialog--confirm-by-keyboard",
            "hasPlay": true
          },
          {
            "name": "EscapeCancels",
            "id": "components-confirm-dialog--escape-cancels",
            "hasPlay": true
          },
          {
            "name": "ConfirmFlow",
            "id": "components-confirm-dialog--confirm-flow",
            "description": "The whole destructive flow with a pointer: opening lands focus on Cancel (not on the red\nbutton), and the focus ring is part of the visual baseline. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-confirm-dialog--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Neutral",
            "id": "components-confirm-dialog--neutral",
            "description": "A non-destructive confirmation: primary button, focus on the action.",
            "hasPlay": false
          },
          {
            "name": "Pending",
            "id": "components-confirm-dialog--pending",
            "hasPlay": false
          },
          {
            "name": "WithDetails",
            "id": "components-confirm-dialog--with-details",
            "hasPlay": false
          },
          {
            "name": "Default",
            "id": "components-dialog--default",
            "description": "The keyboard contract: open, focus moves in, Tab stays inside, Escape closes, focus returns.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-dialog--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Medium",
            "id": "components-dialog--medium",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-dialog--large",
            "hasPlay": false
          },
          {
            "name": "WithoutBody",
            "id": "components-dialog--without-body",
            "hasPlay": false
          },
          {
            "name": "LongContent",
            "id": "components-dialog--long-content",
            "hasPlay": false
          },
          {
            "name": "LongTitle",
            "id": "components-dialog--long-title",
            "hasPlay": false
          },
          {
            "name": "InsideSheet",
            "id": "components-dialog--inside-sheet",
            "description": "A Dialog opened from a Sheet. Escape closes the Dialog first and leaves the Sheet open.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-dialog--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Dialog/Dialog.md",
        "summary": "A modal dialog, and `ConfirmDialog` for destructive confirmations. Built on React Aria `DialogTrigger`, `ModalOverlay`, `Modal` and `Dialog`.\n\n- Handoff source: `components/feedback/Dialog` (CMP-FBK-03). Overlay management: ARCH-005.",
        "whenToUse": [
          "A short task or decision that blocks the page: edit a record in a small form, confirm a deletion.",
          "`ConfirmDialog`: every deletion. \"Deleting always asks for confirmation\" (AGENTS.md section 5)."
        ],
        "whenNotToUse": [
          "Reversible actions (mark, link, change status): do them and offer **Undo** in a `Toast`. Do not confirm.",
          "Viewing or editing a record next to its list: use `Sheet`.",
          "Interactive content anchored to a control: use `Popover`.",
          "A message about the page: use `Alert`.",
          "Nesting dialogs more than two deep."
        ],
        "keyboard": [
          "`Escape` closes **only the top-most layer**. A `Dialog` opened from a `Sheet` closes first and the Sheet stays (tested). Every overlay uses React Aria's shared layer stack instead of its own window listener (ARCH-005)."
        ],
        "examples": [
          "<ConfirmDialog\n  trigger={<Button variant=\"danger\">Delete order</Button>}\n  title=\"Delete order #1048?\"\n  description=\"The order and its 14 lines will be removed. This can't be undone.\"\n  confirmLabel=\"Delete order\"\n  onConfirm={() => deleteOrder(1048)}\n/>\n\n<Dialog\n  trigger={<Button>Edit customer</Button>}\n  title=\"Edit customer\"\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>Save customer</Button>\n    </>\n  )}\n>\n  <Input label=\"Name\" autoFocus />\n</Dialog>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A short task or decision that blocks the page: edit a record in a small form, confirm a deletion.\n- `ConfirmDialog`: every deletion. \"Deleting always asks for confirmation\" (AGENTS.md section 5)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Reversible actions (mark, link, change status): do them and offer **Undo** in a `Toast`. Do not confirm.\n- Viewing or editing a record next to its list: use `Sheet`.\n- Interactive content anchored to a control: use `Popover`.\n- A message about the page: use `Alert`.\n- Nesting dialogs more than two deep."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nDialogTrigger\n  trigger            Button                                (optional when controlled)\n  ModalOverlay       --ds-overlay, z dialog\n    Modal            raised surface, strong line, radius lg, dialog shadow, width sm | md | lg\n      Dialog         role=dialog | alertdialog, aria-labelledby = title, aria-describedby = description\n        header       h2 title (17/24, 600) · description (14/22, text-2)\n        body         scrolls\n        footer       actions aligned to the end; divider when there is a body\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Dialog`\n\n| Prop                                      | Type                                    | Default  | Notes                                                   |\n| ----------------------------------------- | --------------------------------------- | -------- | ------------------------------------------------------- |\n| `title`                                   | `ReactNode` (required)                  | none     | Heading and accessible name.                            |\n| `description`                             | `ReactNode`                             |          | Linked with `aria-describedby`.                         |\n| `children`                                | `ReactNode \\| ({ close }) => ReactNode` |          | Body.                                                   |\n| `footer`                                  | `ReactNode \\| ({ close }) => ReactNode` |          | Actions.                                                |\n| `size`                                    | `'sm' \\| 'md' \\| 'lg'`                  | `sm`     | Max width 420 / 540 / 680 px (`--ds-dialog-width-*`).   |\n| `role`                                    | `'dialog' \\| 'alertdialog'`             | `dialog` |                                                         |\n| `trigger`                                 | `ReactElement`                          |          | Optional when controlled.                               |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                         |          | Controlled / uncontrolled open state.                   |\n| `isDismissable`                           | `boolean`                               | `true`   | Press on the overlay closes it.                         |\n| `isKeyboardDismissDisabled`               | `boolean`                               | `false`  | Disables `Escape`; use only while a request is running. |\n\n`ConfirmDialog` adds: `confirmLabel` (required, names the action), `onConfirm` (required), `tone` (`danger` | `neutral`), `initialFocus` (`cancel` | `confirm`, default `cancel`), `closeOnConfirm`, `isPending`, `messages` (`cancel`). `description` is required: it is the consequence. `isDismissable` defaults to `false`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Focus is **trapped** while open, **restored** to the element that opened it, the page behind is `inert`/`aria-hidden` and does not scroll (all React Aria).\n- **Initial focus.** Plain `Dialog`: the dialog itself, or the child with `autoFocus` (put it on the first field). `ConfirmDialog`: **Cancel**, so pressing Enter or Space as soon as it opens never confirms. The handoff focused nothing.\n- `Escape` closes **only the top-most layer**. A `Dialog` opened from a `Sheet` closes first and the Sheet stays (tested). Every overlay uses React Aria's shared layer stack instead of its own window listener (ARCH-005).\n- Named by the title (`aria-labelledby`, from the `h2`) and described by the description (`aria-describedby`; not linked in the handoff).\n- `ConfirmDialog` uses `alertdialog`, which screen readers announce with its description. A press outside does not dismiss it.\n- Reduced motion: the 150 ms fade and 8 px rise are removed, so the dialog leaves the DOM immediately when it closes (tested). Forced colors: border in `CanvasText`.\n- **Exit animation.** The dialog (and `ConfirmDialog`) stays mounted for the 150 ms fade-out and keeps rendering the props of its last open render during that time. Clearing the state that feeds the title, description, body or footer in the same update that closes it (`setOrder(null)`) does not make the panel animate out empty, so no `useLast`-style workaround is needed. If the tab is hidden the browser does not run animations and React Aria unmounts the dialog only once the tab is visible again.\n- 320 px reflow: the dialog shrinks to the viewport less the 24 px gutter and the body scrolls.\n\n### Layers\n\nOverlays are portaled to `body`, each with its own layer: sheet (40) < dialog (45) < dropdown (50) < popover (55) < toast (58) < tooltip (60). A `Select`, `Menu`, `Popover` or `Combobox` opened from inside a Dialog or Sheet therefore renders **above** it. This is safe because a modal blocks page-level popovers while it is open. Tests assert the order.\n\nControlled use without a `trigger` renders the overlay directly, without a `DialogTrigger`, so opening the dialog logs nothing to the console (GAP-022). Focus still returns to the element focused before it opened."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: a question for confirmations that names the object (\"Delete order #1048?\"), otherwise verb plus object or a noun phrase (\"Edit customer\"). Sentence case, no exclamation marks.\n- Description: the consequence in one sentence (\"The order and its 14 lines will be removed. This can't be undone.\").\n- Buttons: Cancel (secondary) then the action, verb plus object (\"Save customer\", \"Delete order\"). Never \"OK\" or \"Yes\". One primary action.\n- After a confirmed delete show a `Toast` with **Undo** when the data can come back."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<ConfirmDialog\n  trigger={<Button variant=\"danger\">Delete order</Button>}\n  title=\"Delete order #1048?\"\n  description=\"The order and its 14 lines will be removed. This can't be undone.\"\n  confirmLabel=\"Delete order\"\n  onConfirm={() => deleteOrder(1048)}\n/>\n\n<Dialog\n  trigger={<Button>Edit customer</Button>}\n  title=\"Edit customer\"\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>Save customer</Button>\n    </>\n  )}\n>\n  <Input label=\"Name\" autoFocus />\n</Dialog>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Focus trap, restore, initial focus, scroll lock and inert background from React Aria; the handoff had none.\n- `aria-labelledby` and a linked `aria-describedby` instead of `aria-label={title}`.\n- No window-level `Escape` listener per dialog; stack-aware.\n- `width` (420 / 440) became `size` (`sm` / `md` / `lg`) with component tokens; `contained` mode is not ported (a story frame hosts open dialogs in the docs).\n- New: `ConfirmDialog` implementing the destructive-confirmation principle, controlled and uncontrolled state, messages (English and pt-BR)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Sheet",
            "reason": "Viewing or editing a record next to its list: use `Sheet`."
          },
          {
            "name": "Popover",
            "reason": "Interactive content anchored to a control: use `Popover`."
          },
          {
            "name": "Alert",
            "reason": "A message about the page: use `Alert`."
          }
        ],
        "uses": ["Button"],
        "usedBy": []
      }
    },
    {
      "name": "Container",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Container } from 'loui-ds';",
      "folder": "src/components/Container",
      "sourceFile": "src/components/Container/Container.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Centers page content and limits its line length: a maximum inline size plus a side gutter.\nIt carries no landmark role; put it inside `main`.",
      "props": [
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg' | 'full'",
          "required": false,
          "description": "Maximum inline size: `sm` 640px, `md` 960px, `lg` 1200px (`--ds-container-*`), or `full`\nfor no maximum. Default is the page content maximum (`--ds-content-max`, 1200px).",
          "values": ["sm", "md", "lg", "full"]
        },
        {
          "name": "gutter",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Adds the content gutter (`--ds-content-gutter`, 24px) as inline padding. Default `true`."
        },
        {
          "name": "as",
          "type": "'div' | 'article' | 'aside' | 'footer' | 'form' | 'header' | 'li' | 'main' | 'nav' | 'ol' | 'section' | 'ul'",
          "required": false,
          "default": "'div'",
          "description": "Element to render, `div` by default.",
          "values": [
            "div",
            "article",
            "aside",
            "footer",
            "form",
            "header",
            "li",
            "main",
            "nav",
            "ol",
            "section",
            "ul"
          ]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg", "full"]
      },
      "cssVariables": [
        "--ds-container-lg",
        "--ds-container-md",
        "--ds-container-sm",
        "--ds-content-gutter",
        "--ds-content-max"
      ],
      "stories": {
        "title": "Layout/Container",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-container--default",
            "description": "The page content maximum (1200px) with a 24px gutter.",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "layout-container--small",
            "hasPlay": false
          },
          {
            "name": "Medium",
            "id": "layout-container--medium",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "layout-container--large",
            "hasPlay": false
          },
          {
            "name": "Full",
            "id": "layout-container--full",
            "hasPlay": false
          },
          {
            "name": "NoGutter",
            "id": "layout-container--no-gutter",
            "hasPlay": false
          },
          {
            "name": "Narrow",
            "id": "layout-container--narrow",
            "description": "On a 320px screen the container shrinks to the viewport and keeps its gutter.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-container--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Container/Container.md",
        "summary": "Centers page content and limits its line length: a maximum inline size and a side gutter.",
        "whenToUse": [
          "Inside the `main` of a page to keep content readable on wide screens.",
          "Settings pages and forms (`size=\"sm\"` or `\"md\"`), dashboards (`\"lg\"`)."
        ],
        "whenNotToUse": [
          "Inside `Card`, `Dialog` or `Sheet`: those set their own padding.",
          "For full-bleed content such as a table: use `size=\"full\"` or skip the container."
        ],
        "keyboard": [],
        "examples": [
          "<main id=\"main\">\n  <Container size=\"md\">\n    <Heading level={1}>Settings</Heading>…\n  </Container>\n</main>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Inside the `main` of a page to keep content readable on wide screens.\n- Settings pages and forms (`size=\"sm\"` or `\"md\"`), dashboards (`\"lg\"`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Inside `Card`, `Dialog` or `Sheet`: those set their own padding.\n- For full-bleed content such as a table: use `size=\"full\"` or skip the container."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (max-inline-size, margin-inline: auto, padding-inline: gutter)\n  children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop     | Type                                | Default              | Notes                                                               |\n| -------- | ----------------------------------- | -------------------- | ------------------------------------------------------------------- |\n| `size`   | `'sm' \\| 'md' \\| 'lg' \\| 'full'`    | content max (1200px) | 640 / 960 / 1200px (`--ds-container-*`); `full` removes the maximum |\n| `gutter` | `boolean`                           | `true`               | Adds `--ds-content-gutter` (24px) inline padding                    |\n| `as`     | `'div' \\| 'section' \\| 'article' …` | `'div'`              | Element to render                                                   |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "responsive-behavior",
            "heading": "Responsive behavior",
            "content": "The container is `inline-size: 100%` up to its maximum, so it narrows with the viewport and keeps its gutter down to 320px."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Carries no landmark role. The page's single `main` comes from `AppShell` (or your own `<main>`); put the container inside it.\n- Logical properties (`margin-inline`, `padding-inline`) make it direction-neutral."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<main id=\"main\">\n  <Container size=\"md\">\n    <Heading level={1}>Settings</Heading>…\n  </Container>\n</main>\n```\n\nInside `AppShell`, which already pads `main`, use `<Container gutter={false}>` or `<AppShell padded={false}>`."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff laid screens out at a fixed 1180px minimum. The container gives a maximum, never a minimum."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Card",
            "reason": "Inside `Card`, `Dialog` or `Sheet`: those set their own padding."
          },
          {
            "name": "Dialog",
            "reason": "Inside `Card`, `Dialog` or `Sheet`: those set their own padding."
          },
          {
            "name": "Sheet",
            "reason": "Inside `Card`, `Dialog` or `Sheet`: those set their own padding."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "CurrencyInput",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { CurrencyInput } from 'loui-ds';",
      "folder": "src/components/CurrencyInput",
      "sourceFile": "src/components/CurrencyInput/CurrencyInput.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A money field. The value is an integer in minor units (ADR-0006) plus an ISO `currency`, never\na float. It uses the **digits-only typing model**: every digit you type shifts in from the\nright (typing `1`, `2`, `3` gives `1.23`), separators are ignored, Backspace shifts the last\ndigit out, and the caret always stays at the end. Pasting is smarter: `12.50` and `12,50` both\npaste as 12.50, read against the locale (see CurrencyInput.md). The box is mono and\nright-aligned; the currency symbol is part of the text, where the locale puts it, so screen\nreaders announce it with the amount.\n\nThe form receives the integer minor units under `name`.",
      "props": [
        {
          "name": "currency",
          "type": "string",
          "required": true,
          "description": "ISO 4217 code of the currency (`'USD'`, `'BRL'`, `'JPY'`). It decides the number of fraction\ndigits (JPY has 0, KWD has 3), the symbol and where the locale places it."
        },
        {
          "name": "value",
          "type": "number | null",
          "required": false,
          "description": "The amount in **integer minor units** (ADR-0006): `123456` is `$1,234.56` in USD and\n`¥123456` in JPY. `null` is an empty field (nothing entered), which is different from `0`."
        },
        {
          "name": "defaultValue",
          "type": "number | null",
          "required": false,
          "description": "The initial amount when uncontrolled. Defaults to empty (`null`)."
        },
        {
          "name": "onChange",
          "type": "((minorUnits: number | null) => void)",
          "required": false,
          "description": "Called with the new amount in minor units, or `null` when the field is emptied."
        },
        {
          "name": "allowNegative",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Accepts negative amounts. Type `-` anywhere to flip the sign. Default `false`."
        },
        {
          "name": "maxDigits",
          "type": "number",
          "required": false,
          "default": "15",
          "description": "Most digits the amount may have, fraction digits included (`15` is the maximum, the\nlargest count that is always a safe integer). Typing beyond it is ignored. Default `15`."
        },
        {
          "name": "entryMode",
          "type": "'cents' | 'amount'",
          "required": false,
          "default": "'cents'",
          "description": "How digits are typed. `'cents'` (default) shifts digits in from the right: `200` is 2.00.\n`'amount'` types whole units: `200` is 200.00, and the decimal mark (`.` or `,`) starts the\nfraction (`12,5` is 12.50). Both give integer minor units. See \"Typing modes\" in the docs.",
          "values": ["cents", "amount"]
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "Name for form submission. The form receives the **integer minor units** (`123456`), or an\nempty string when the field is empty, never the formatted text."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field is empty (`value` is `null`), until the first digit is typed."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<input>` element."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTextFieldProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "aria-activedescendant",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-autocomplete",
              "type": "'none' | 'inline' | 'list' | 'both'",
              "required": false
            },
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"false\" | \"true\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "enterKeyHint",
              "type": "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TextFieldRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TextFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "autoCorrect",
              "type": "string",
              "required": false
            },
            {
              "name": "spellCheck",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [],
      "stories": {
        "title": "Components/CurrencyInput",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-currency-input--default",
            "hasPlay": true
          },
          {
            "name": "Filled",
            "id": "components-currency-input--filled",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-currency-input--with-description",
            "hasPlay": false
          },
          {
            "name": "ToneMessage",
            "id": "components-currency-input--tone-message",
            "description": "A live check under the field: the tone follows the difference, the words say what it is.",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-currency-input--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-currency-input--invalid",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-currency-input--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-currency-input--read-only",
            "hasPlay": true
          },
          {
            "name": "AllowNegative",
            "id": "components-currency-input--allow-negative",
            "hasPlay": true
          },
          {
            "name": "MaxDigits",
            "id": "components-currency-input--max-digits",
            "hasPlay": true
          },
          {
            "name": "Brazilian",
            "id": "components-currency-input--brazilian",
            "hasPlay": true
          },
          {
            "name": "ZeroAndThreeDigitCurrencies",
            "id": "components-currency-input--zero-and-three-digit-currencies",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-currency-input--sizes",
            "hasPlay": false
          },
          {
            "name": "Controlled",
            "id": "components-currency-input--controlled",
            "hasPlay": true
          },
          {
            "name": "WithPlaceholder",
            "id": "components-currency-input--with-placeholder",
            "description": "The placeholder shows while the value is `null`, also when focused, until a digit is typed.",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-currency-input--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          },
          {
            "name": "AmountMode",
            "id": "components-currency-input--amount-mode",
            "description": "Amount mode types whole units: `200` is 200.00 and the decimal mark starts the fraction.",
            "hasPlay": true
          },
          {
            "name": "AmountModeBrazilian",
            "id": "components-currency-input--amount-mode-brazilian",
            "hasPlay": true
          },
          {
            "name": "AmountModeYen",
            "id": "components-currency-input--amount-mode-yen",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/CurrencyInput/CurrencyInput.md",
        "summary": "A money field. The value is an **integer in minor units** plus an ISO 4217 `currency` (ADR-0006), never a float. Built on [Input](../Input/Input.md); the formatting comes from `Intl` through `formatCurrency`-style helpers and `parseCurrencyInput` (see `docs/architecture/utilities.md`). Task: CMP-INP-05.",
        "whenToUse": [
          "Prices, totals, payments, adjustments: anywhere a person types an amount of money.",
          "Editing a price inline, in place of its text in a table row or list (see the `InlinePriceEdit` recipe)."
        ],
        "whenNotToUse": [
          "Quantities or measures: `NumberField`.",
          "Showing an amount (not editing it): `formatCurrency(minorUnits, currency, locale)` from `loui-ds`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Prices, totals, payments, adjustments: anywhere a person types an amount of money.\n- Editing a price inline, in place of its text in a table row or list (see the `InlinePriceEdit` recipe)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Quantities or measures: `NumberField`.\n- Showing an amount (not editing it): `formatCurrency(minorUnits, currency, locale)` from `loui-ds`."
          },
          {
            "id": "editing-model-digits-shift-in-from-the-right-decision",
            "heading": "Editing model: digits shift in from the right (decision)",
            "content": "Two models were considered:\n\n1. **React Aria `NumberField` with `style: 'currency'`**. Reuses its parsing and stepping, but its editing model is \"type a number, format on blur\": locale decimal marks must be typed, the caret and text change on blur, and the value is a JS `number` in major units, so every boundary needs `Math.round(x * 100)` and float drift (`0.1 + 0.2`) becomes a bug source. Money in the handoff is also typed without separators.\n2. **Digits-only typing** (the handoff model, implemented by `parseCurrencyInput`). Typing `1`, `2`, `3` gives `$1.23`; `1`, `2`, `3`, `4`, `5`, `6` gives `$1,234.56`. Separators and symbols are ignored, `Backspace` shifts the last digit out, the caret always stays at the end. The state is an integer from the first keystroke to the form value, so there is no floating point anywhere; the same code serves JPY (0 digits) and KWD (3 digits); no locale decimal mark has to be known to type.\n\n**Decision: digits-only typing**, with a smarter **paste** (below). The trade-off: a person who types `12.5` gets `$1.25`, not `$12.50`. That is the point of the model (type `1250`), the field always shows what it will submit, and paste covers the decimal-writing habit."
          },
          {
            "id": "typing-modes",
            "heading": "Typing modes",
            "content": "`entryMode` chooses how typed digits are read. Both modes keep the value an integer in minor units, call `onChange` with integers or `null`, accept the same smart paste, and honor `maxDigits` and `allowNegative`.\n\n|                     | `cents` (default)                                                    | `amount`                                                                                 |\n| ------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| Typing `200` in USD | `$2.00`                                                              | `$200.00`                                                                                |\n| Model               | Digits shift in from the right; separators ignored; caret at the end | Digits are whole units; a decimal mark starts the fraction                               |\n| Use for             | Cash-register style, fast numeric-keypad entry, POS                  | People who think in whole units, prices like \"200\", forms where users type decimal marks |\n\n**Amount mode rules** (`parseCurrencyAmount`, string manipulation only, no floats):\n\n- Digits are whole units until a decimal mark. The mark is the **last** `.` or `,` (or the locale's own decimal mark) when it is the only one of its kind and either it is the locale's decimal mark or it is followed by at most as many digits as the currency has (2 for USD/BRL, 3 for KWD). A mark followed by more digits is grouping and ignored, so users can mix habits: `12.5` and `12,5` both mean 12.50; `1,234.56` (en-US) and `1.234,56` (pt-BR) both mean 1234.56; `1,234` in USD is 1234.00.\n- At most as many fraction digits as the currency has, and `maxDigits` counts minor-unit digits (`maxDigits: 5` allows up to 999.99). After the locale's decimal mark, extra digits are ignored while typing (`12.567` stays `12.56`), and a second locale decimal mark is ignored. JPY (0 digits) has no fraction: marks are ignored.\n- Examples en-US USD: `200` is 20000, `12.5` is 1250, `12,50` is 1250. pt-BR BRL: `200` is 20000, `12,5` is 1250, `1.234,56` is 123456. JPY: `1500` is 1500. KWD en-US: `1.5` is 1500.\n- **One reading per edit.** While you type, a mark that was read as the decimal mark keeps that reading until you delete it: pt-BR `12.90` stays 12,90 and one more digit is refused (the text stays `12.90`), instead of turning the `.` into a thousands mark and jumping to 12.905,00. The same holds for a foreign mark in either locale (en-US `12,50` then `7` is refused). So a consumer that saves on every keystroke never sees the order of magnitude change. Type grouped numbers without separators, or paste them: **paste** keeps the smart reading (`1.234,56` in pt-BR pastes as 1234.56). A second decimal mark is ignored.\n- Digits beyond 15 are never cut: typing past the limit is ignored and a paste with more than 15 digits is ignored (the value stays as it was), because shortening it would change the amount.\n- While focused the box edits **plain text** (no symbol) with normal caret editing, and keeps what you typed readable: `200,` stays `200,`. The existing amount is selected on focus, by keyboard or by a mouse click, so typing replaces it; while nothing has been edited yet a click on the already focused field keeps it selected (a lone caret at the start would put digits before the amount). After the first edit clicks place the caret normally and the mouse can select text. On blur the box shows the full currency format (`R$ 200,00`). `Backspace` edits the text, select all + `Delete` gives `null`, and a lone `-` (with `allowNegative`) is negative zero as in `cents` mode. The mobile keyboard is `decimal` (`text` with `allowNegative`)."
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- `value` / `defaultValue` / `onChange` are **integer minor units** or `null` (empty, which is not `0`). `onChange` receives `null` when the field is emptied (select all, then `Delete`).\n- Display: the currency's own format with the symbol where the locale puts it (`$1,234.56`, `R$ 1.234,56`, `1.234,56 €`, `¥1,234`), mono, tabular, right-aligned. The symbol is part of the text, so screen readers announce it with the amount.\n- Caret (`cents` mode): always moved to the end after every edit and whenever you click or arrow into the text, so a click anywhere never lets digits land at the start. A range selection (Ctrl+A) is allowed so you can clear the field.\n- `allowNegative`: type `-` anywhere to flip the sign; a lone `-` shows `-$0.00` (negative zero) and the next digit keeps the sign. Without it, minus signs are ignored.\n- `maxDigits` (default and maximum 15, the largest count that is always a safe integer): typing beyond it is ignored. Large amounts are exact (`$9,999,999,999,999.99` is `999999999999999`).\n- **Paste** reads the text against the locale: `12.50` and `12,50` both paste as 12.50; `1,234.56` in en-US and `1.234,56` in pt-BR are grouped thousands; a lone separator followed by exactly three digits is a thousands group (`1.234` is 1234 in pt-BR but 1.23 in en-US because `.` is the en-US decimal mark); extra fraction digits are rounded half up; symbols and spaces are dropped; text without digits is ignored.\n- **Form**: `name` submits the **integer minor units** (`123456`) or an empty string, through a hidden input; it is not sent while the field is disabled. The visible text is never submitted."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                                                                                   | Notes                                                                                              |\n| -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| `currency`                                                                                                                             | Required ISO 4217 code.                                                                            |\n| `value` / `defaultValue` / `onChange`                                                                                                  | `number \\| null` in minor units.                                                                   |\n| `allowNegative`, `maxDigits`                                                                                                           | See above.                                                                                         |\n| `entryMode`                                                                                                                            | `'cents'` (default) or `'amount'`. See \"Typing modes\".                                             |\n| `label` / `aria-label` / `aria-labelledby`                                                                                             | One is required.                                                                                   |\n| `description`, `descriptionTone`, `errorMessage`, `size`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly`, `placeholder`, `name` | As in `Input`.                                                                                     |\n| `placeholder`                                                                                                                          | Shown while `value` is `null`, also while focused, until the first digit is typed.                 |\n| `fullWidth`                                                                                                                            | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default. |\n| `onKeyDown`, `onFocus`, `onBlur`                                                                                                       | Event handlers, composed with the internal ones (see below).                                       |\n| `inputRef`, `ref`, `className`, `inputClassName`, `messages`                                                                           | As in `Input`.                                                                                     |"
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "A text input (`inputmode=\"numeric\"`, or `text` with `allowNegative` so a minus can be typed on mobile) named by the label, described by the description and the error. Digits type, `Backspace` removes the last digit, `-` flips the sign when allowed. There is no live region; the value is announced as typed. The border stays 3:1 in every state (shared Input styling).\n\nHotkeys: A consumer `onKeyDown` does not hide keys from `useHotkeys(..., { allowInEditable: true })` shortcuts: a key keeps bubbling unless your handler called `event.preventDefault()` on it. Recipe: `Enter` confirms and `Escape` cancels (both with `preventDefault()`), every other key stays a hotkey. See `docs/architecture/hotkeys.md`."
          },
          {
            "id": "events-onkeydown-onfocus-onblur",
            "heading": "Events: `onKeyDown`, `onFocus`, `onBlur`",
            "content": "These come from the underlying text field and are **composed** with the internal handlers, never replaced:\n\n- `onKeyDown`: runs for every key. A key your handler does not `preventDefault()` keeps bubbling to `useHotkeys` shortcuts and still types a digit. Call `preventDefault()` for the keys you take (`Enter`, `Escape`).\n- `onFocus`: your handler runs first, then the caret moves to the end of the text.\n- `onBlur`: your handler runs after the field resets a lone `-` (negative zero). `onChange` has already delivered the last typed value, so a blur handler may confirm it.\n\nInline price editing (`Enter` confirms, `Escape` cancels, blur confirms) is the recipe `inline-price-edit`."
          },
          {
            "id": "placeholder",
            "heading": "Placeholder",
            "content": "While `value` is `null` the box is empty and shows `placeholder` in `--ds-text-3`, focused or not. It disappears with the first digit and returns when the field is emptied again (select all, then `Delete`). `0` is a value, not empty: it shows `$0.00`. Use the placeholder for a hint such as an inherited price, never as the label."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label the thing being priced (\"Price per unit\"). The currency symbol is automatic, never part of the label. Errors say how to fix: \"Enter a price above zero.\""
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<CurrencyInput\n  label=\"Price per unit\"\n  currency=\"BRL\"\n  name=\"price\"\n  value={priceMinor}\n  onChange={setPriceMinor}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The value is integer minor units (the handoff used a `number` in major units), the currency is an ISO code (the handoff took a `\"R$\"` string), and the format comes from `Intl` (the handoff hard-coded `pt-BR`).\n- Negative amounts, `maxDigits`, paste handling, `null`, caret stability, form submission of the integer, description and error, and sizes are new."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "NumberField",
            "reason": "Quantities or measures: `NumberField`."
          }
        ],
        "uses": ["Input"],
        "usedBy": []
      }
    },
    {
      "name": "DataTable",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { DataTable } from 'loui-ds';",
      "folder": "src/components/DataTable",
      "sourceFile": "src/components/DataTable/DataTable.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A data table with locale-aware sorting, row selection with a bulk action bar, client-side\npagination, loading and empty states. Built on React Aria `Table` (a real `<table>` with\ngrid keyboard navigation): the header cell of a sortable column is a press target with\n`aria-sort`, and the selection checkboxes are labelled.\n\nSorting cycles ascending, descending, none, and is stable: rows that compare equal keep the\norder you passed. Strings compare with the active locale (accent- and case-insensitive,\nnatural number order), numbers numerically. The table never reorders rows on its own;\nlists are alphabetical by default in this design system, so pass sorted rows or `defaultSort`.",
      "props": [
        {
          "name": "columns",
          "type": "readonly DataTableColumn<T>[]",
          "required": true,
          "description": "Column definitions, in display order."
        },
        {
          "name": "rows",
          "type": "readonly T[]",
          "required": true,
          "description": "The rows, in the order you want them shown until the user sorts. Lists are alphabetical by\ndefault in this design system (AGENTS.md rule 5b): sort the rows before passing them, or\npass `defaultSort` for a text column. The table never reorders rows on its own."
        },
        {
          "name": "rowKey",
          "type": "string",
          "required": false,
          "default": "'id' as keyof T & string",
          "description": "Property of a row that holds its unique id. Default `id`. The value is converted to a string."
        },
        {
          "name": "selectionMode",
          "type": "'none' | 'single' | 'multiple'",
          "required": false,
          "default": "'none'",
          "description": "`none` (default), `single` or `multiple`. Selecting adds a checkbox column.",
          "values": ["none", "single", "multiple"]
        },
        {
          "name": "selectedKeys",
          "type": "ReadonlySet<string>",
          "required": false,
          "description": "Selected row ids (controlled). Selection survives page changes; \"select all\" applies to the rows of the current page."
        },
        {
          "name": "defaultSelectedKeys",
          "type": "ReadonlySet<string>",
          "required": false,
          "description": "Selected row ids when uncontrolled."
        },
        {
          "name": "onSelectionChange",
          "type": "(keys: ReadonlySet<string>) => void",
          "required": false,
          "description": "Called with the new set of selected row ids."
        },
        {
          "name": "bulkActions",
          "type": "(selectedKeys: ReadonlySet<string>, clear: () => void) => ReactNode",
          "required": false,
          "description": "Content of the bulk action bar, shown while rows are selected: usually small `Button`s. `clear` empties the selection."
        },
        {
          "name": "sort",
          "type": "DataTableSort | null",
          "required": false,
          "description": "Current sort (controlled). `null` means no sort."
        },
        {
          "name": "defaultSort",
          "type": "DataTableSort | null",
          "required": false,
          "description": "Sort applied first when uncontrolled, for example `{ key: 'customer', direction: 'asc' }`."
        },
        {
          "name": "onSortChange",
          "type": "(sort: DataTableSort | null) => void",
          "required": false,
          "description": "Called when the user changes the sort. Header presses cycle ascending, descending, none."
        },
        {
          "name": "pageSize",
          "type": "number | false",
          "required": false,
          "default": "10",
          "description": "Rows per page; `false` shows all rows without pagination. Default `10`. Pagination appears only when there is more than one page."
        },
        {
          "name": "page",
          "type": "number",
          "required": false,
          "description": "Current page, 1-based (controlled)."
        },
        {
          "name": "defaultPage",
          "type": "number",
          "required": false,
          "default": "1",
          "description": "Page shown first when uncontrolled. Default `1`. The page returns to 1 when the sort or the set of rows changes."
        },
        {
          "name": "onPageChange",
          "type": "(page: number) => void",
          "required": false,
          "description": "Called with the new page."
        },
        {
          "name": "onRowAction",
          "type": "(row: T) => void",
          "required": false,
          "description": "Called when the user activates a row (click, or Enter on the focused row). Adds a pointer cursor and hover style."
        },
        {
          "name": "activeKey",
          "type": "string",
          "required": false,
          "description": "Id of the active row (for example the one open in a side sheet). It gets the raised background and the ink bar."
        },
        {
          "name": "isLoading",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Replaces the rows with skeleton rows and announces \"Loading rows\"."
        },
        {
          "name": "loadingRowCount",
          "type": "number",
          "required": false,
          "default": "5",
          "description": "Number of skeleton rows while loading. Default `5`."
        },
        {
          "name": "emptyState",
          "type": "ReactNode",
          "required": false,
          "description": "Content when there are no rows. Default: an `EmptyState` with built-in text."
        },
        {
          "name": "reserveBulkBarSpace",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Keeps the space of the bulk action bar free while nothing is selected, so the table does not\njump down when the first row is selected (fixed positions). Turn off to let the table move\nup. Default `true`; only applies when `selectionMode` is not `none`."
        },
        {
          "name": "stickyHeader",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Keeps the header visible while the body scrolls. Set `maxHeight` to scroll inside the table; without it the header sticks to the page scroll."
        },
        {
          "name": "maxHeight",
          "type": "string | number",
          "required": false,
          "description": "Maximum height of the table area before it scrolls (a number is px, or any CSS length)."
        },
        {
          "name": "messages",
          "type": "Partial<{ selectRow: string; selection: string; selectAll: string; selectedCount: (count: number, formatted: string) => string; clearSelection: string; bulkActions: string; loading: string; emptyTitle: string; emptyDescription: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the table. Required unless `aria-labelledby` is set."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the table (a visible heading)."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "DataTable",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "selectRow",
            "kind": "string",
            "default": "Select row",
            "description": "Accessible name of the checkbox that selects one row. React Aria appends the row name (see `getRowLabel`): \"Select row ORD-1001\"."
          },
          {
            "key": "selection",
            "kind": "string",
            "default": "Selection",
            "description": "Hidden header text of the selection column in `single` mode, where there is no select-all checkbox."
          },
          {
            "key": "selectAll",
            "kind": "string",
            "default": "Select all rows on this page",
            "description": "Accessible name of the header checkbox. It selects the rows on the current page."
          },
          {
            "key": "selectedCount",
            "kind": "function",
            "default": "{formatted} selected",
            "signature": "(count: number, formatted: string)",
            "description": "Visible and announced selection summary. `formatted` is `count` formatted for the locale."
          },
          {
            "key": "clearSelection",
            "kind": "string",
            "default": "Clear selection",
            "description": "Button of the bulk action bar that clears the selection."
          },
          {
            "key": "bulkActions",
            "kind": "string",
            "default": "Actions for selected rows",
            "description": "Accessible name of the bulk action bar."
          },
          {
            "key": "loading",
            "kind": "string",
            "default": "Loading rows",
            "description": "Announced while the table shows loading rows."
          },
          {
            "key": "emptyTitle",
            "kind": "string",
            "default": "No records found",
            "description": "Title of the default empty state."
          },
          {
            "key": "emptyDescription",
            "kind": "string",
            "default": "Try another filter or search term.",
            "description": "Description of the default empty state."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-data-table-active-bar-w",
        "--ds-data-table-bulk-bar-h",
        "--ds-data-table-cell-padding-inline",
        "--ds-data-table-col-min-w",
        "--ds-data-table-col-w",
        "--ds-data-table-flex-count",
        "--ds-data-table-gap",
        "--ds-data-table-header-padding-block",
        "--ds-data-table-max-h",
        "--ds-data-table-min-w",
        "--ds-data-table-selection-col-w",
        "--ds-data-table-sticky-z-index",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-line-subtle",
        "--ds-radius-md",
        "--ds-row-h",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface-raised",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/DataTable",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-data-table--default",
            "description": "Rows appear in the order given; the table never reorders on its own.",
            "hasPlay": true
          },
          {
            "name": "AlphabeticalByDefault",
            "id": "components-data-table--alphabetical-by-default",
            "description": "Lists are alphabetical by default (AGENTS.md rule 5b). `defaultSort` applies a locale-aware\nsort (accent- and case-insensitive) on first render; the user can still change it.",
            "hasPlay": true
          },
          {
            "name": "Sorting",
            "id": "components-data-table--sorting",
            "description": "Pressing a header cycles ascending, descending, none.",
            "hasPlay": true
          },
          {
            "name": "KeyboardSort",
            "id": "components-data-table--keyboard-sort",
            "description": "Tab enters the grid on the first row; Arrow Up reaches the headers; Enter or Space sorts.",
            "hasPlay": true
          },
          {
            "name": "Selection",
            "id": "components-data-table--selection",
            "hasPlay": true
          },
          {
            "name": "SelectAllOnPage",
            "id": "components-data-table--select-all-on-page",
            "description": "The header checkbox selects the rows of the current page; selection survives page changes.",
            "hasPlay": true
          },
          {
            "name": "KeyboardSelection",
            "id": "components-data-table--keyboard-selection",
            "description": "Space toggles the focused checkbox.",
            "hasPlay": true
          },
          {
            "name": "SingleSelection",
            "id": "components-data-table--single-selection",
            "hasPlay": true
          },
          {
            "name": "Paginated",
            "id": "components-data-table--paginated",
            "description": "Client-side pagination with the `Pagination` component; it returns to page 1 when the sort changes.",
            "hasPlay": true
          },
          {
            "name": "SortSelectPaginate",
            "id": "components-data-table--sort-select-paginate",
            "description": "Sort, select and paginate in one flow: the selection and the sort survive the page change. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "ClickableRows",
            "id": "components-data-table--clickable-rows",
            "description": "Clickable rows (`onRowAction`) and the active row, for a list that opens a detail panel.",
            "hasPlay": true
          },
          {
            "name": "SelectionAndRowAction",
            "id": "components-data-table--selection-and-row-action",
            "description": "Selected and clickable rows together: the checkbox selects, the row opens.",
            "hasPlay": false
          },
          {
            "name": "Loading",
            "id": "components-data-table--loading",
            "hasPlay": true
          },
          {
            "name": "Empty",
            "id": "components-data-table--empty",
            "hasPlay": true
          },
          {
            "name": "CustomEmpty",
            "id": "components-data-table--custom-empty",
            "hasPlay": false
          },
          {
            "name": "StickyHeader",
            "id": "components-data-table--sticky-header",
            "description": "The header stays visible while the body scrolls inside `maxHeight`.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-data-table--long-text",
            "description": "Long text is truncated with an ellipsis; fixed-width columns keep their size.",
            "hasPlay": false
          },
          {
            "name": "EmptyWithMinimumColumnWidth",
            "id": "components-data-table--empty-with-minimum-column-width",
            "description": "An empty table with `minWidth` columns does not scroll sideways: the empty state fits the container.",
            "hasPlay": true
          },
          {
            "name": "MinimumColumnWidth",
            "id": "components-data-table--minimum-column-width",
            "hasPlay": true
          },
          {
            "name": "WrappingColumns",
            "id": "components-data-table--wrapping-columns",
            "description": "`truncate: false` lets a column wrap instead of ending in an ellipsis, and a mono header such as\n\"Supplier name\" can use two lines instead of being cut to \"SUPPL…\". Other columns still truncate.",
            "hasPlay": true
          },
          {
            "name": "HiddenHeaderLabel",
            "id": "components-data-table--hidden-header-label",
            "description": "`hideLabel` hides the header text of an icon-only or action column; `label` stays the accessible\nname of the column header.",
            "hasPlay": true
          },
          {
            "name": "ButtonInCell",
            "id": "components-data-table--button-in-cell",
            "description": "A Button inside a cell handles its own press; the row action does not run.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-data-table--controlled",
            "hasPlay": true
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-data-table--portuguese-brazil",
            "description": "Text, plurals, numbers and the sort order follow the provider locale.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/DataTable/DataTable.md",
        "summary": "A table of records with locale-aware sorting, row selection with a bulk action bar, clickable rows, client-side pagination, loading and empty states. Built on React Aria `Table`: a real `<table>` with grid keyboard navigation, `aria-sort` on sortable headers and labelled selection checkboxes.",
        "whenToUse": [
          "Lists of records the user scans, sorts and acts on: orders, customers, invoices.",
          "Several columns of comparable data. For one line per record with a menu of actions use `ActionList`."
        ],
        "whenNotToUse": [
          "Fewer than three columns, or content that is mostly text: use `ActionList` or a plain list.",
          "Server-side sorting and paging over very large data sets: control `sort` and `page` yourself and pass the page rows with `pageSize={false}` (virtualization is not provided).",
          "Layout: this is a data grid, not a layout table."
        ],
        "keyboard": [
          "Grid keyboard model from React Aria: `Tab` enters the table (first row), arrow keys move between cells, rows and the header (Arrow Up from the first row), `Enter` or `Space` on a sortable header sorts, `Space` on a selection checkbox toggles, `Enter` on a row runs `onRowAction`, `Tab` leaves the table."
        ],
        "examples": [
          "<DataTable\n  aria-label=\"Orders\"\n  rows={orders}\n  rowKey=\"id\"\n  defaultSort={{ key: 'customer', direction: 'asc' }}\n  selectionMode=\"multiple\"\n  columns={[\n    { key: 'id', label: 'Order', width: '120px', mono: true, muted: true },\n    { key: 'customer', label: 'Customer', isRowHeader: true },\n    {\n      key: 'status',\n      label: 'Status',\n      width: '120px',\n      render: (r) => <Badge tone=\"success\">{r.status}</Badge>,\n    },\n    {\n      key: 'total',\n      label: 'Total',\n      width: '120px',\n      align: 'end',\n      mono: true,\n      render: (r) => formatCurrency(r.total, 'USD', locale),\n    },\n  ]}\n  bulkActions={(keys, clear) => (\n    <Button\n      size=\"sm\"\n      onPress={() => {\n        archive(keys);\n        clear();\n      }}\n    >\n      Archive orders\n    </Button>\n  )}\n  onRowAction={(row) => openOrder(row.id)}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Lists of records the user scans, sorts and acts on: orders, customers, invoices.\n- Several columns of comparable data. For one line per record with a menu of actions use `ActionList`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Fewer than three columns, or content that is mostly text: use `ActionList` or a plain list.\n- Server-side sorting and paging over very large data sets: control `sort` and `page` yourself and pass the page rows with `pageSize={false}` (virtualization is not provided).\n- Layout: this is a data grid, not a layout table."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  div[role=group]        bulk action bar (while rows are selected): \"N selected\", bulkActions, Clear selection\n  div.scroll\n    table[role=grid]     named by aria-label / aria-labelledby\n      thead  th[aria-sort] caption-mono header per column (+ selection checkbox column)\n      tbody  tr > td      row height --ds-row-h; active/selected: raised bg + 2px ink bar\n  nav                    Pagination (only when there is more than one page)\n  span[role=status]      visually hidden live region: \"N selected\" / \"Loading rows\"\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                         | Type                                          | Default | Notes                                                                                                                                                    |\n| ------------------------------------------------------------ | --------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `columns`                                                    | `DataTableColumn<T>[]`                        | -       | `key`, `label`, `width`, `minWidth`, `truncate`, `hideLabel`, `align` (`start`/`end`), `mono`, `muted`, `sortable`, `sortValue`, `render`, `isRowHeader` |\n| `rows`                                                       | `T[]`                                         | -       | Shown in the order given until the user sorts                                                                                                            |\n| `rowKey`                                                     | `keyof T`                                     | `id`    | Unique id property; converted to a string                                                                                                                |\n| `aria-label`                                                 | `string`                                      | -       | Required (or `aria-labelledby`)                                                                                                                          |\n| `selectionMode`                                              | `none \\| single \\| multiple`                  | `none`  | Adds a checkbox column                                                                                                                                   |\n| `selectedKeys` / `defaultSelectedKeys` / `onSelectionChange` | `ReadonlySet<string>`                         |         | Controlled / uncontrolled. Selection survives page changes                                                                                               |\n| `bulkActions`                                                | `(keys, clear) => ReactNode`                  |         | Content of the bar shown while rows are selected                                                                                                         |\n| `sort` / `defaultSort` / `onSortChange`                      | `{ key, direction: 'asc' \\| 'desc' } \\| null` | `null`  | Controlled / uncontrolled sort                                                                                                                           |\n| `pageSize`                                                   | `number \\| false`                             | `10`    | `false` disables pagination                                                                                                                              |\n| `page` / `defaultPage` / `onPageChange`                      | `number`                                      | `1`     | Controlled / uncontrolled page                                                                                                                           |\n| `onRowAction`                                                | `(row) => void`                               |         | Row click or Enter. Adds hover style and pointer cursor. A `Button` in a cell does not trigger it (see \"Buttons inside cells\")                           |\n| `activeKey`                                                  | `string`                                      |         | Row open elsewhere (raised background + ink bar)                                                                                                         |\n| `isLoading`                                                  | `boolean`                                     | `false` | Skeleton rows (`loadingRowCount`, default 5) and a live \"Loading rows\"                                                                                   |\n| `emptyState`                                                 | `ReactNode`                                   |         | Default: `EmptyState` with built-in text                                                                                                                 |\n| `reserveBulkBarSpace`                                        | `boolean`                                     | `true`  | Keeps the bulk bar space free so the table does not jump when the first row is selected                                                                  |\n| `stickyHeader`, `maxHeight`                                  | `boolean`, `number \\| string`                 |         | With `maxHeight` the body scrolls and the header stays; without it the header sticks to the page scroll                                                  |\n| `messages`                                                   | `Partial<messages>`                           |         | Per-instance text overrides                                                                                                                              |"
          },
          {
            "id": "column-sizing",
            "heading": "Column sizing",
            "content": "| Column prop | Type      | Default | Effect                                                                                                                                     |\n| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| `width`     | `string`  | -       | CSS width (`96px`, `20%`). Columns without it share the remaining space equally                                                            |\n| `minWidth`  | `string`  | -       | CSS minimum width (`120px`, `8rem`). The column is never narrower; a container that is too small scrolls horizontally instead of squashing |\n| `truncate`  | `boolean` | `true`  | `true`: header and cells stay on one line and end in an ellipsis. `false`: they wrap and the row grows                                     |\n| `hideLabel` | `boolean` | `false` | Hides the header text visually (icon-only or action columns). `label` stays the accessible name                                            |\n\n- Both sizes go through CSS custom properties on the header cell (`--ds-data-table-col-w`, `--ds-data-table-col-min-w`). Browsers ignore `min-width` on cells in a fixed table layout, so when any column has a `minWidth` the table gets a minimum width (`--ds-data-table-min-w`): the sum of every column's own minimum (fixed columns at the larger of `width` and `minWidth`, flexible columns at their `minWidth`, a flexible column without `minWidth` at 0). Without any `minWidth` the layout is unchanged.\n- **Flexible columns keep their own minimum.** Each flexible column is its `minWidth` plus an equal share of the space left after all minimums. Example in a 1000px container with `Order` (`width: 100px`), `Customer` (`minWidth: 240px`), `Status` (`minWidth: 80px`) and `Total` (no `minWidth`): 580px is left, so the widths are 100, 433, 273 and 193px. In a 200px container the table is 420px wide and scrolls: 100, 240, 80 and just the cell padding for `Total`. Give every flexible column that must stay readable a `minWidth`.\n- Use fixed units (`px`, `rem`) for `width` and `minWidth` in a table with a `minWidth` column; percentages are not part of the minimum calculation, so in development the table logs one console warning (per table) naming the first column that has a `%` `width` or `minWidth` while any column has a `minWidth`. The scroll area is a size container (`container-type: inline-size`) because browsers treat a `calc()` mixing a percentage as `auto` in a fixed table layout; a container unit is a plain length.\n- An empty table (no rows, not loading) drops the table minimum width, so the empty state never scrolls sideways. The header stays visible and its labels are cut with an ellipsis.\n- A table with a `minWidth` column scrolls horizontally inside its own area (`overflow-x: auto`). A sticky header then sticks to that area, so also set `maxHeight` when using `stickyHeader`.\n- Use `minWidth` for content that breaks when squeezed: a row of swatches, a badge, a code. A column with both `width` and `minWidth` is as wide as the larger of the two.\n- Truncated text is not lost: the full text stays in the DOM (assistive technology reads it) and the truncated header label carries a `title`. Prefer `truncate: false` over a long truncated value that users need to read.\n- **Headers never wrap by default.** The mono uppercase caption style is cut with an ellipsis in a narrow column (\"SUPPL…\"). Either give the column a `minWidth` that fits the label, or set `truncate: false` to allow a two-line header.\n- **Icon-only columns:** set `hideLabel: true` and `sortable: false`. The header cell is named by `label` (\"Row actions\"), so a screen reader still announces it; never leave the label empty."
          },
          {
            "id": "sorting",
            "heading": "Sorting",
            "content": "- Pressing a sortable header cycles **ascending, descending, none**. `aria-sort` is `ascending`, `descending` or `none` on the header cell.\n- Strings use `compareText` for the active locale: accent- and case-insensitive, natural number order (`Item 2` before `Item 10`). Numbers, booleans and dates compare numerically; numbers come before strings.\n- The sort is **stable in both directions**: equal rows keep the order you passed (descending is not a reversed array). Empty values (`null`, `undefined`, `NaN`, invalid dates) always come last.\n- Sorting covers all rows, not just the current page; the page returns to 1 when the sort or the set of rows changes (a new array with the same row ids does not reset the page).\n- Use `sortValue` when the displayed text differs from the value to sort by, `sortable: false` for columns that are not a property of the row.\n\n### Alphabetical by default\n\nLists are alphabetical by default in this design system (AGENTS.md rule 5b; exceptions: tasks by urgency, dated records newest first). The table does not reorder rows on its own. Either pass rows already ordered, or set `defaultSort={{ key: 'customer', direction: 'asc' }}` (see the `AlphabeticalByDefault` story)."
          },
          {
            "id": "selection",
            "heading": "Selection",
            "content": "- Checkboxes are named \"Select row {row header text}\" and \"Select all rows on this page\"; the row header is the column marked `isRowHeader` (default: the first column), so choose the most descriptive one.\n- \"Select all\" applies to the rows on the current page. Selection of other pages is kept. `bulkActions` receives only ids that still exist in `rows`.\n- The bulk bar shows \"N selected\" (plural-aware) and a \"Clear selection\" button. A visually hidden polite `status` announces the count as it changes.\n- `single` mode shows a checkbox per row but no select-all."
          },
          {
            "id": "buttons-inside-cells",
            "heading": "Buttons inside cells",
            "content": "A `Button` inside a cell handles its own press: the press does **not** also trigger `onRowAction` (covered by a test). This is a design decision, not a bug: a row can open a detail panel while its buttons run their own action (\"Archive\", \"Open menu\"). Keep the row action and the cell action distinct, give the Button an accessible name that includes the row (\"Archive ORD-1001\"), and do not repeat the row action in a Button."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Grid keyboard model from React Aria: `Tab` enters the table (first row), arrow keys move between cells, rows and the header (Arrow Up from the first row), `Enter` or `Space` on a sortable header sorts, `Space` on a selection checkbox toggles, `Enter` on a row runs `onRowAction`, `Tab` leaves the table.\n- Sortable headers expose `aria-sort`; the sort icon is decorative. Active sorted headers also use a stronger color, never color alone.\n- Selection state is `aria-selected` on rows and the checked state of the checkboxes; the count is text.\n- While loading the root has `aria-busy`, skeleton rows are not selectable and the first cell of each carries visually hidden \"Loading rows\".\n- Forced colors: selected and active rows keep the bar (system `Highlight`); focus rings are outlines.\n- Not provided: `aria-rowcount`/`aria-rowindex` for the rows of other pages (React Aria derives them from the rendered page). Pagination announces the range (\"11–20 of 42\")."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Column labels in sentence case; CSS applies the uppercase mono caption style.\n- Bulk actions are verb + object (\"Archive orders\"); destructive ones still confirm in a dialog.\n- Empty text says what happened and what to do (\"No records found. Try another filter or search term.\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<DataTable\n  aria-label=\"Orders\"\n  rows={orders}\n  rowKey=\"id\"\n  defaultSort={{ key: 'customer', direction: 'asc' }}\n  selectionMode=\"multiple\"\n  columns={[\n    { key: 'id', label: 'Order', width: '120px', mono: true, muted: true },\n    { key: 'customer', label: 'Customer', isRowHeader: true },\n    {\n      key: 'status',\n      label: 'Status',\n      width: '120px',\n      render: (r) => <Badge tone=\"success\">{r.status}</Badge>,\n    },\n    {\n      key: 'total',\n      label: 'Total',\n      width: '120px',\n      align: 'end',\n      mono: true,\n      render: (r) => formatCurrency(r.total, 'USD', locale),\n    },\n  ]}\n  bulkActions={(keys, clear) => (\n    <Button\n      size=\"sm\"\n      onPress={() => {\n        archive(keys);\n        clear();\n      }}\n    >\n      Archive orders\n    </Button>\n  )}\n  onRowAction={(row) => openOrder(row.id)}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A real `<table>` (React Aria `Table`) instead of a CSS grid of `div`s: proper `rowgroup`, `columnheader`, `rowheader`, `aria-sort`, `aria-selected`; no `role` override on a button.\n- Sorting is locale-aware and stable; cycling ends in \"none\"; `useMemo` dependencies are complete.\n- `defaultSort` takes `{ key, direction }` (was `{ key, dir }`); `onRowClick` is `onRowAction`; `selectable` is `selectionMode`; `align` is `start`/`end` (logical, RTL-safe); `bulkActions` receives a `Set`.\n- Selection and sort/page state are controllable; selection persists across pages; the page resets on sort or data change.\n- Checkboxes are labelled from messages; selection count is a live region with plural messages.\n- Loading (skeleton rows), empty (default `EmptyState`) and sticky header are built in.\n- Column visibility, column resizing and virtualization are not included."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ActionList",
            "reason": "Fewer than three columns, or content that is mostly text: use `ActionList` or a plain list."
          }
        ],
        "uses": [
          "Button",
          "Checkbox",
          "EmptyState",
          "Icon",
          "Pagination",
          "Skeleton",
          "VisuallyHidden"
        ],
        "usedBy": []
      }
    },
    {
      "name": "DatePicker",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { DatePicker } from 'loui-ds';",
      "folder": "src/components/DatePicker",
      "sourceFile": "src/components/DatePicker/DatePicker.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A date field with a calendar popover (React Aria `DatePicker`). The value is a\n`YYYY-MM-DD` string (no time zone, so the day never shifts); the typed segments, the\nplaceholder, month and weekday names and the first day of the week all follow the\n`DSProvider` locale. Type the date (arrows or digits per segment) or open the calendar;\n\"Today\" and \"Clear\" sit under the grid.\n\nThe form receives the ISO date under `name`. Range selection is not part of this component.",
      "props": [
        {
          "name": "value",
          "type": "string | null",
          "required": false,
          "description": "The date as `YYYY-MM-DD` (no time zone), or `null` for none. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "string | null",
          "required": false,
          "description": "The initial date when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((value: string | null) => void)",
          "required": false,
          "description": "Called with the new date as `YYYY-MM-DD` once every segment is filled, or `null` from the Clear button. An incomplete date is not reported."
        },
        {
          "name": "min",
          "type": "string",
          "required": false,
          "description": "Earliest selectable date (`YYYY-MM-DD`). Typing an earlier date makes the field invalid."
        },
        {
          "name": "max",
          "type": "string",
          "required": false,
          "description": "Latest selectable date (`YYYY-MM-DD`)."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the calendar."
        },
        {
          "name": "messages",
          "type": "Partial<{ today: string; clear: string; openCalendar: string; } & { requiredMark: string; } & Partial<{ previousMonth: string; nextMonth: string; }>>",
          "required": false,
          "description": "Message overrides for this instance: `today`, `clear`, `openCalendar`, `previousMonth`, `nextMonth`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaDatePickerProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "DatePickerBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "firstDayOfWeek",
              "type": "'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'",
              "required": false
            },
            {
              "name": "pageBehavior",
              "type": "'single' | 'visible'",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "DatePickerStateOptions",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean | (() => boolean)",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "OverlayTriggerProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isOpen",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onOpenChange",
              "type": "(isOpen: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: CalendarDate) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "DatePicker",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "today",
            "kind": "string",
            "default": "Today",
            "description": "Footer button that picks today's date."
          },
          {
            "key": "clear",
            "kind": "string",
            "default": "Clear",
            "description": "Footer button that empties the field."
          },
          {
            "key": "openCalendar",
            "kind": "string",
            "default": "Open calendar",
            "description": "Accessible name of the button that opens the calendar."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-calendar-cell-size",
        "--ds-calendar-today-ring-width",
        "--ds-calendar-weekday-height",
        "--ds-field-control-padding-inline",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-text-inverse",
        "--ds-weight-medium",
        "--ds-weight-semibold",
        "--ds-z-dropdown"
      ],
      "stories": {
        "title": "Components/DatePicker",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-date-picker--default",
            "hasPlay": true
          },
          {
            "name": "CalendarKeyboard",
            "id": "components-date-picker--calendar-keyboard",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-date-picker--open",
            "description": "The calendar open under the field, for the visual review (TEST-020).",
            "hasPlay": true
          },
          {
            "name": "PickDate",
            "id": "components-date-picker--pick-date",
            "description": "Pointer path: open the calendar, click a day, the popover closes and the field shows the date. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-date-picker--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "WithValue",
            "id": "components-date-picker--with-value",
            "hasPlay": false
          },
          {
            "name": "MinAndMax",
            "id": "components-date-picker--min-and-max",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-date-picker--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-date-picker--invalid",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-date-picker--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-date-picker--read-only",
            "hasPlay": false
          },
          {
            "name": "Brazilian",
            "id": "components-date-picker--brazilian",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-date-picker--sizes",
            "hasPlay": false
          },
          {
            "name": "StandaloneCalendar",
            "id": "components-date-picker--standalone-calendar",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-date-picker--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/DatePicker/DatePicker.md",
        "summary": "`DatePicker` is a date field (typed segments) with a calendar popover and \"Today\" / \"Clear\" buttons. `Calendar` is the standalone month grid. Built on React Aria `DatePicker`, `DateInput`, `DateSegment`, `Calendar`, `CalendarGrid`, `CalendarCell`, `Popover`. Task: CMP-INP-02.",
        "whenToUse": [
          "A single calendar date: delivery, due date, birthday.",
          "`Calendar` alone for inline planners and for composing your own picker."
        ],
        "whenNotToUse": [
          "A date range: not supported yet (range picker is P2).",
          "A time of day or a timestamp: the value has no time and no time zone.",
          "Dates years away from now: typing the segments is faster than the grid."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A single calendar date: delivery, due date, birthday.\n- `Calendar` alone for inline planners and for composing your own picker."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A date range: not supported yet (range picker is P2).\n- A time of day or a timestamp: the value has no time and no time zone.\n- Dates years away from now: typing the segments is faster than the grid."
          },
          {
            "id": "value-contract",
            "heading": "Value contract",
            "content": "`value`, `defaultValue`, `onChange`, `min` and `max` are **ISO strings `YYYY-MM-DD`** (the `ISODate` type of `src/utils/date`), never `Date`. Conversion to React Aria's `CalendarDate` happens inside the component (`@internationalized/date`, see below), so the day cannot shift with the viewer's time zone. Invalid strings are treated as \"no date\". `onChange` receives a string only once every segment is filled, or `null` from the Clear button; an incomplete date is not reported. The form receives the ISO string under `name`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                                                 | Notes                                                                                                                           |\n| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`                                                           | One is required.                                                                                                                |\n| `value` / `defaultValue` / `onChange`                                                                | ISO string or `null`.                                                                                                           |\n| `min`, `max`                                                                                         | ISO strings. Outside days are disabled in the grid; typed dates outside are invalid. \"Today\" is disabled when today is outside. |\n| `description`, `errorMessage`, `size`, `name`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | As in `Select`.                                                                                                                 |\n| `fullWidth`                                                                                          | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.                              |\n| `messages`                                                                                           | `today`, `clear`, `openCalendar`, `previousMonth`, `nextMonth`, `requiredMark`.                                                 |\n\n`Calendar` takes `value`, `defaultValue`, `onChange(iso)`, `min`, `max`, `isDisabled`, `isReadOnly`, `autoFocus`, `messages`, and requires `aria-label` or `aria-labelledby`."
          },
          {
            "id": "locale",
            "heading": "Locale",
            "content": "Segment order and separators, placeholders (`dd/mm/aaaa`), month names, weekday letters and the first day of the week come from the `DSProvider` locale through React Aria; there are no hard-coded names. Only the button texts are in the message catalog (`Calendar` key for the month buttons, `DatePicker` for the rest)."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key (calendar grid)               | Behavior                                                      |\n| --------------------------------- | ------------------------------------------------------------- |\n| Arrows                            | Previous / next day, previous / next week                     |\n| `PageUp` / `PageDown`             | Previous / next month                                         |\n| `Shift+PageUp` / `Shift+PageDown` | Previous / next year                                          |\n| `Home` / `End`                    | First / last day of the **month** (React Aria; APG says week) |\n| `Enter` / `Space`                 | Select the focused day                                        |\n| `Escape`                          | Close the popover, focus returns to the button                |\n\nIn the field: digits fill the segment and move to the next; arrows change a segment's value; `Backspace` clears a segment; `Tab` moves between segments. Each day is named with its full date (\"Wednesday, October 28, 2026\", \"Today, ...\"), so the grid is usable without sight. Today has a ring (`--ds-text-3`), the selected day an accent fill with `--ds-text-inverse`; disabled days are dimmed and `aria-disabled`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the date's role (\"Delivery date\"). Errors say how to fix: \"Choose a date after today.\""
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<DatePicker\n  label=\"Delivery date\"\n  name=\"delivery\"\n  min=\"2026-10-30\"\n  value={date}\n  onChange={setDate}\n/>\n```"
          },
          {
            "id": "dependency-note",
            "heading": "Dependency note",
            "content": "The component imports `@internationalized/date`, which React Aria already depends on. It is not a direct dependency of this package yet, so under pnpm's strict layout it must be added to `package.json` (`pnpm add @internationalized/date@^3.12.4`, already in the lockfile as a transitive dependency)."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Typed segments with locale-aware placeholders instead of a button showing a `toLocaleDateString` string; calendar is an ARIA grid with full keyboard navigation (the handoff used plain buttons).\n- Month and weekday names come from `Intl` through the locale, not hard-coded Portuguese arrays; the week start follows the locale.\n- Day buttons have full-date names; `min` / `max` are enforced for typing too; today is a ring, the footer buttons (\"Today\", \"Clear\") come from messages.\n- `Date` objects and `T12:00:00` tricks are gone; the value is an ISO string end to end."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["Button", "FieldMessages", "Icon", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "Description",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Description } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Helper text under a field (the handoff `hint`). Linked to the control with\n`aria-describedby` by the surrounding React Aria field. It stays visible when an error shows.\nWith `tone` it becomes a toned message (\"Matches\", \"Over by $5\"); the tone adds colour only,\nthe announcement is the same text.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": ""
        },
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Tints the text for a live check (`success` \"Matches\", `warning` \"Over by $5\", `danger`\n\"Short by $5\"). Default `neutral`. The words carry the meaning: colour only reinforces it,\nso never rely on the tone alone.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": ["FileUpload", "Radio", "RadioGroup"]
      }
    },
    {
      "name": "Dialog",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Dialog } from 'loui-ds';",
      "folder": "src/components/Dialog",
      "sourceFile": "src/components/Dialog/Dialog.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A modal dialog built on React Aria `DialogTrigger`, `ModalOverlay`, `Modal` and `Dialog`.\n\nReact Aria traps focus, restores it to the trigger on close, locks page scroll, hides the\nrest of the page from assistive technology and closes only the top-most layer on `Escape`\n(a Dialog opened from a Sheet closes first). Initial focus goes to the dialog itself unless\na child has `autoFocus`; for destructive confirmations use `ConfirmDialog`, which focuses\nCancel.",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": true,
          "description": "Heading of the dialog and its accessible name. A question for a confirmation (\"Delete\norder #1048?\"), otherwise a noun phrase or a verb plus object (\"Edit customer\")."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "One or two sentences under the title. It is linked to the dialog with `aria-describedby`,\nso screen readers read it right after the title."
        },
        {
          "name": "children",
          "type": "ReactNode | ((options: DialogRenderOptions) => ReactNode)",
          "required": false,
          "description": "Body of the dialog. A function receives `close`."
        },
        {
          "name": "footer",
          "type": "ReactNode | ((options: DialogRenderOptions) => ReactNode)",
          "required": false,
          "description": "Actions, aligned to the end of the footer. Cancel (secondary) before the confirming action;\none primary action at most. A function receives `close`."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'sm'",
          "description": "Maximum width: `sm` 420px (default), `md` 540px, `lg` 680px. Narrower on small screens.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "role",
          "type": "'dialog' | 'alertdialog'",
          "required": false,
          "default": "'dialog'",
          "description": "`dialog` (default), or `alertdialog` for a message that needs an answer before the user\ncontinues, such as a destructive confirmation. `ConfirmDialog` uses `alertdialog`.",
          "values": ["dialog", "alertdialog"]
        },
        {
          "name": "trigger",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": false,
          "description": "The element that opens the dialog (a `Button`). Optional: omit it and control the dialog\nwith `isOpen`. Focus returns to the element that was focused when the dialog opened."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the dialog opens or closes."
        },
        {
          "name": "isDismissable",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Whether pressing the overlay closes the dialog. Default `true`; `ConfirmDialog` sets `false`."
        },
        {
          "name": "isKeyboardDismissDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables closing with `Escape`. Use it only while a request is running."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the dialog panel."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the dialog panel."
        }
      ],
      "inheritedProps": [],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "cancel",
            "kind": "string",
            "default": "Cancel",
            "description": "Label of the button that dismisses a `ConfirmDialog` without doing anything."
          }
        ]
      },
      "cssVariables": [
        "--ds-dialog-gutter",
        "--ds-dialog-padding",
        "--ds-dialog-width-lg",
        "--ds-dialog-width-md",
        "--ds-dialog-width-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-overlay",
        "--ds-radius-lg",
        "--ds-shadow-dialog",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/ConfirmDialog",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-confirm-dialog--default",
            "description": "Initial focus is on Cancel, so Enter right after opening cancels instead of deleting.",
            "hasPlay": true
          },
          {
            "name": "ConfirmByKeyboard",
            "id": "components-confirm-dialog--confirm-by-keyboard",
            "hasPlay": true
          },
          {
            "name": "EscapeCancels",
            "id": "components-confirm-dialog--escape-cancels",
            "hasPlay": true
          },
          {
            "name": "ConfirmFlow",
            "id": "components-confirm-dialog--confirm-flow",
            "description": "The whole destructive flow with a pointer: opening lands focus on Cancel (not on the red\nbutton), and the focus ring is part of the visual baseline. TEST-012.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-confirm-dialog--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Neutral",
            "id": "components-confirm-dialog--neutral",
            "description": "A non-destructive confirmation: primary button, focus on the action.",
            "hasPlay": false
          },
          {
            "name": "Pending",
            "id": "components-confirm-dialog--pending",
            "hasPlay": false
          },
          {
            "name": "WithDetails",
            "id": "components-confirm-dialog--with-details",
            "hasPlay": false
          },
          {
            "name": "Default",
            "id": "components-dialog--default",
            "description": "The keyboard contract: open, focus moves in, Tab stays inside, Escape closes, focus returns.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-dialog--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Medium",
            "id": "components-dialog--medium",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-dialog--large",
            "hasPlay": false
          },
          {
            "name": "WithoutBody",
            "id": "components-dialog--without-body",
            "hasPlay": false
          },
          {
            "name": "LongContent",
            "id": "components-dialog--long-content",
            "hasPlay": false
          },
          {
            "name": "LongTitle",
            "id": "components-dialog--long-title",
            "hasPlay": false
          },
          {
            "name": "InsideSheet",
            "id": "components-dialog--inside-sheet",
            "description": "A Dialog opened from a Sheet. Escape closes the Dialog first and leaves the Sheet open.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-dialog--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Dialog/Dialog.md",
        "summary": "A modal dialog, and `ConfirmDialog` for destructive confirmations. Built on React Aria `DialogTrigger`, `ModalOverlay`, `Modal` and `Dialog`.\n\n- Handoff source: `components/feedback/Dialog` (CMP-FBK-03). Overlay management: ARCH-005.",
        "whenToUse": [
          "A short task or decision that blocks the page: edit a record in a small form, confirm a deletion.",
          "`ConfirmDialog`: every deletion. \"Deleting always asks for confirmation\" (AGENTS.md section 5)."
        ],
        "whenNotToUse": [
          "Reversible actions (mark, link, change status): do them and offer **Undo** in a `Toast`. Do not confirm.",
          "Viewing or editing a record next to its list: use `Sheet`.",
          "Interactive content anchored to a control: use `Popover`.",
          "A message about the page: use `Alert`.",
          "Nesting dialogs more than two deep."
        ],
        "keyboard": [
          "`Escape` closes **only the top-most layer**. A `Dialog` opened from a `Sheet` closes first and the Sheet stays (tested). Every overlay uses React Aria's shared layer stack instead of its own window listener (ARCH-005)."
        ],
        "examples": [
          "<ConfirmDialog\n  trigger={<Button variant=\"danger\">Delete order</Button>}\n  title=\"Delete order #1048?\"\n  description=\"The order and its 14 lines will be removed. This can't be undone.\"\n  confirmLabel=\"Delete order\"\n  onConfirm={() => deleteOrder(1048)}\n/>\n\n<Dialog\n  trigger={<Button>Edit customer</Button>}\n  title=\"Edit customer\"\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>Save customer</Button>\n    </>\n  )}\n>\n  <Input label=\"Name\" autoFocus />\n</Dialog>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A short task or decision that blocks the page: edit a record in a small form, confirm a deletion.\n- `ConfirmDialog`: every deletion. \"Deleting always asks for confirmation\" (AGENTS.md section 5)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Reversible actions (mark, link, change status): do them and offer **Undo** in a `Toast`. Do not confirm.\n- Viewing or editing a record next to its list: use `Sheet`.\n- Interactive content anchored to a control: use `Popover`.\n- A message about the page: use `Alert`.\n- Nesting dialogs more than two deep."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nDialogTrigger\n  trigger            Button                                (optional when controlled)\n  ModalOverlay       --ds-overlay, z dialog\n    Modal            raised surface, strong line, radius lg, dialog shadow, width sm | md | lg\n      Dialog         role=dialog | alertdialog, aria-labelledby = title, aria-describedby = description\n        header       h2 title (17/24, 600) · description (14/22, text-2)\n        body         scrolls\n        footer       actions aligned to the end; divider when there is a body\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Dialog`\n\n| Prop                                      | Type                                    | Default  | Notes                                                   |\n| ----------------------------------------- | --------------------------------------- | -------- | ------------------------------------------------------- |\n| `title`                                   | `ReactNode` (required)                  | none     | Heading and accessible name.                            |\n| `description`                             | `ReactNode`                             |          | Linked with `aria-describedby`.                         |\n| `children`                                | `ReactNode \\| ({ close }) => ReactNode` |          | Body.                                                   |\n| `footer`                                  | `ReactNode \\| ({ close }) => ReactNode` |          | Actions.                                                |\n| `size`                                    | `'sm' \\| 'md' \\| 'lg'`                  | `sm`     | Max width 420 / 540 / 680 px (`--ds-dialog-width-*`).   |\n| `role`                                    | `'dialog' \\| 'alertdialog'`             | `dialog` |                                                         |\n| `trigger`                                 | `ReactElement`                          |          | Optional when controlled.                               |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                         |          | Controlled / uncontrolled open state.                   |\n| `isDismissable`                           | `boolean`                               | `true`   | Press on the overlay closes it.                         |\n| `isKeyboardDismissDisabled`               | `boolean`                               | `false`  | Disables `Escape`; use only while a request is running. |\n\n`ConfirmDialog` adds: `confirmLabel` (required, names the action), `onConfirm` (required), `tone` (`danger` | `neutral`), `initialFocus` (`cancel` | `confirm`, default `cancel`), `closeOnConfirm`, `isPending`, `messages` (`cancel`). `description` is required: it is the consequence. `isDismissable` defaults to `false`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Focus is **trapped** while open, **restored** to the element that opened it, the page behind is `inert`/`aria-hidden` and does not scroll (all React Aria).\n- **Initial focus.** Plain `Dialog`: the dialog itself, or the child with `autoFocus` (put it on the first field). `ConfirmDialog`: **Cancel**, so pressing Enter or Space as soon as it opens never confirms. The handoff focused nothing.\n- `Escape` closes **only the top-most layer**. A `Dialog` opened from a `Sheet` closes first and the Sheet stays (tested). Every overlay uses React Aria's shared layer stack instead of its own window listener (ARCH-005).\n- Named by the title (`aria-labelledby`, from the `h2`) and described by the description (`aria-describedby`; not linked in the handoff).\n- `ConfirmDialog` uses `alertdialog`, which screen readers announce with its description. A press outside does not dismiss it.\n- Reduced motion: the 150 ms fade and 8 px rise are removed, so the dialog leaves the DOM immediately when it closes (tested). Forced colors: border in `CanvasText`.\n- **Exit animation.** The dialog (and `ConfirmDialog`) stays mounted for the 150 ms fade-out and keeps rendering the props of its last open render during that time. Clearing the state that feeds the title, description, body or footer in the same update that closes it (`setOrder(null)`) does not make the panel animate out empty, so no `useLast`-style workaround is needed. If the tab is hidden the browser does not run animations and React Aria unmounts the dialog only once the tab is visible again.\n- 320 px reflow: the dialog shrinks to the viewport less the 24 px gutter and the body scrolls.\n\n### Layers\n\nOverlays are portaled to `body`, each with its own layer: sheet (40) < dialog (45) < dropdown (50) < popover (55) < toast (58) < tooltip (60). A `Select`, `Menu`, `Popover` or `Combobox` opened from inside a Dialog or Sheet therefore renders **above** it. This is safe because a modal blocks page-level popovers while it is open. Tests assert the order.\n\nControlled use without a `trigger` renders the overlay directly, without a `DialogTrigger`, so opening the dialog logs nothing to the console (GAP-022). Focus still returns to the element focused before it opened."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: a question for confirmations that names the object (\"Delete order #1048?\"), otherwise verb plus object or a noun phrase (\"Edit customer\"). Sentence case, no exclamation marks.\n- Description: the consequence in one sentence (\"The order and its 14 lines will be removed. This can't be undone.\").\n- Buttons: Cancel (secondary) then the action, verb plus object (\"Save customer\", \"Delete order\"). Never \"OK\" or \"Yes\". One primary action.\n- After a confirmed delete show a `Toast` with **Undo** when the data can come back."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<ConfirmDialog\n  trigger={<Button variant=\"danger\">Delete order</Button>}\n  title=\"Delete order #1048?\"\n  description=\"The order and its 14 lines will be removed. This can't be undone.\"\n  confirmLabel=\"Delete order\"\n  onConfirm={() => deleteOrder(1048)}\n/>\n\n<Dialog\n  trigger={<Button>Edit customer</Button>}\n  title=\"Edit customer\"\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>Save customer</Button>\n    </>\n  )}\n>\n  <Input label=\"Name\" autoFocus />\n</Dialog>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Focus trap, restore, initial focus, scroll lock and inert background from React Aria; the handoff had none.\n- `aria-labelledby` and a linked `aria-describedby` instead of `aria-label={title}`.\n- No window-level `Escape` listener per dialog; stack-aware.\n- `width` (420 / 440) became `size` (`sm` / `md` / `lg`) with component tokens; `contained` mode is not ported (a story frame hosts open dialogs in the docs).\n- New: `ConfirmDialog` implementing the destructive-confirmation principle, controlled and uncontrolled state, messages (English and pt-BR)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Sheet",
            "reason": "Viewing or editing a record next to its list: use `Sheet`."
          },
          {
            "name": "Popover",
            "reason": "Interactive content anchored to a control: use `Popover`."
          },
          {
            "name": "Alert",
            "reason": "A message about the page: use `Alert`."
          }
        ],
        "uses": [],
        "usedBy": ["ShortcutsHelp"]
      }
    },
    {
      "name": "Divider",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Divider } from 'loui-ds';",
      "folder": "src/components/Divider",
      "sourceFile": "src/components/Divider/Divider.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A 1px line that separates groups of content, built on React Aria `Separator`\n(`<hr>`, or `role=\"separator\"` with `aria-orientation` when vertical). Prefer spacing to\nseparate related things; use a divider where the content changes topic.",
      "props": [
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'",
          "description": "`horizontal` (default) separates stacked content; `vertical` separates items in a row and needs a parent with a height.",
          "values": ["horizontal", "vertical"]
        },
        {
          "name": "variant",
          "type": "'subtle' | 'default' | 'strong' | 'dashed'",
          "required": false,
          "default": "'default'",
          "description": "Line weight by token: `subtle` (`--ds-line-subtle`, internal dividers), `default`\n(`--ds-line`, between cards and sections) or `strong` (`--ds-line-strong`, around controls\nand overlays). `dashed` is a 1px dashed `--ds-line-strong` rule for a total row or a\nreceipt tear line (AGENTS.md section 7). Default `default`.",
          "values": ["subtle", "default", "strong", "dashed"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the separator element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "SeparatorProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "elementType",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "variant": ["subtle", "default", "strong", "dashed"]
      },
      "cssVariables": ["--ds-line", "--ds-line-strong", "--ds-line-subtle"],
      "stories": {
        "title": "Layout/Divider",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-divider--default",
            "hasPlay": true
          },
          {
            "name": "Subtle",
            "id": "layout-divider--subtle",
            "hasPlay": false
          },
          {
            "name": "Strong",
            "id": "layout-divider--strong",
            "hasPlay": false
          },
          {
            "name": "Dashed",
            "id": "layout-divider--dashed",
            "description": "The total-row rule: dashed, for the line above a total (AGENTS.md section 7).",
            "hasPlay": false
          },
          {
            "name": "DashedOnPaper",
            "id": "layout-divider--dashed-on-paper",
            "description": "The dashed rule inside the paper scope.",
            "hasPlay": false
          },
          {
            "name": "Vertical",
            "id": "layout-divider--vertical",
            "description": "A vertical divider stretches to the height of a row.",
            "hasPlay": true
          },
          {
            "name": "Weights",
            "id": "layout-divider--weights",
            "description": "All three weights together, to compare.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Divider/Divider.md",
        "summary": "A 1px line that separates groups of content. Built on React Aria `Separator`.",
        "whenToUse": [
          "Between sections of a panel or a menu-like list where the topic changes.",
          "Vertically, between inline groups in a toolbar."
        ],
        "whenNotToUse": [
          "To separate closely related items: use spacing.",
          "Around every field or row: borders-first hierarchy comes from surfaces, not a line between everything.",
          "To wrap a Card in a Card: use a divider inside one card instead."
        ],
        "keyboard": [],
        "examples": [
          "<Stack gap={4}>\n  <Section />\n  <Divider />\n  <Section />\n</Stack>\n\n<Inline>\n  <Button>Bold</Button>\n  <Divider orientation=\"vertical\" />\n  <Button>Link</Button>\n</Inline>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Between sections of a panel or a menu-like list where the topic changes.\n- Vertically, between inline groups in a toolbar."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To separate closely related items: use spacing.\n- Around every field or row: borders-first hierarchy comes from surfaces, not a line between everything.\n- To wrap a Card in a Card: use a divider inside one card instead."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nhr (1px line)          horizontal\ndiv[role=separator]    vertical, with aria-orientation\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop          | Type                                            | Default        | Notes                                                                                                                                                       |\n| ------------- | ----------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `orientation` | `'horizontal' \\| 'vertical'`                    | `'horizontal'` | Vertical needs a parent with a height (it stretches in a flex row)                                                                                          |\n| `variant`     | `'subtle' \\| 'default' \\| 'strong' \\| 'dashed'` | `'default'`    | `--ds-line-subtle` (internal), `--ds-line` (cards, sections), `--ds-line-strong`; `dashed` is a dashed `--ds-line-strong` rule for a total row or tear line |\n\nReact Aria separator props (`aria-label`, …) and `ref` pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Rendered by React Aria: a horizontal divider is an `<hr>`; a vertical one has `role=\"separator\"` and `aria-orientation=\"vertical\"`.\n- It is a thematic break for assistive technology, so do not use it as a pure visual rule between unrelated things.\n- Line colors are borders, not text; `forced-colors` paints them with `CanvasText`."
          },
          {
            "id": "dashed-rule",
            "heading": "Dashed rule",
            "content": "`variant=\"dashed\"` is the \"total row\" rule of AGENTS.md section 7: a 1px dashed line in `--ds-line-strong`, for the line above a total or a tear line on a receipt preview. It works on the dark surfaces and inside the paper scope (`data-theme=\"paper\"`, where the line becomes the paper rule). In `forced-colors` it is drawn with `CanvasText`. Like the other variants it is a decorative border, not a text or control boundary."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Stack gap={4}>\n  <Section />\n  <Divider />\n  <Section />\n</Stack>\n\n<Inline>\n  <Button>Bold</Button>\n  <Divider orientation=\"vertical\" />\n  <Button>Link</Button>\n</Inline>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff drew dividers with `border-top` on a wrapper; this is a real separator with three line weights from tokens plus a dashed rule."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "DonutMeter",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { DonutMeter } from 'loui-ds';",
      "folder": "src/components/DonutMeter",
      "sourceFile": "src/components/DonutMeter/DonutMeter.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A circular meter for one proportion: how much of a whole is used, done or filled (storage\nused, goal reached, stock level). The percentage is printed in the center, formatted with\n`Intl` for the active locale. Built on React Aria `Meter` (`role=\"meter\"` with\n`aria-valuenow`, `aria-valuemin`, `aria-valuemax` and `aria-valuetext`).\n\nIt needs an accessible name: a visible `label` (printed under the ring), `aria-label` or\n`aria-labelledby`. For a task that makes progress use `Progress`; for a bar use `Meter`;\nfor one proportion among several use `ProportionBar`.",
      "props": [
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'accent'",
          "description": "Color of the ring: `accent` (default), `neutral`, `success`, `warning` or `danger`. Color\nis never the only signal: the percentage is printed in the center and the label says what\nit is.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Diameter token: `sm` 48px, `md` 72px (default), `lg` 96px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "showValue",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Prints the percentage in the center. On by default. Turn it off when the center is empty on purpose."
        },
        {
          "name": "centerLabel",
          "type": "ReactNode",
          "required": false,
          "description": "Replaces the percentage in the center (\"12\" over \"days\"). Visual only: screen readers get\n`valueLabel` (or the percentage) as the value of the meter."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the meter element."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ProgressBarBaseProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "formatOptions",
              "type": "NumberFormatOptions",
              "required": false
            },
            {
              "name": "maxValue",
              "type": "number",
              "required": false
            },
            {
              "name": "minValue",
              "type": "number",
              "required": false
            },
            {
              "name": "value",
              "type": "number",
              "required": false
            },
            {
              "name": "valueLabel",
              "type": "ReactNode",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"],
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-donut-meter-color",
        "--ds-donut-meter-size-lg",
        "--ds-donut-meter-size-md",
        "--ds-donut-meter-size-sm",
        "--ds-donut-meter-stroke-w",
        "--ds-donut-meter-value",
        "--ds-duration-progress",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-fs-heading",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-num",
        "--ds-lh-heading",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-lh-num",
        "--ds-space-2",
        "--ds-success",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/DonutMeter",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-donut-meter--default",
            "hasPlay": true
          },
          {
            "name": "Small",
            "id": "components-donut-meter--small",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-donut-meter--large",
            "hasPlay": false
          },
          {
            "name": "Neutral",
            "id": "components-donut-meter--neutral",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "components-donut-meter--success",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-donut-meter--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-donut-meter--danger",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-donut-meter--empty",
            "hasPlay": false
          },
          {
            "name": "Full",
            "id": "components-donut-meter--full",
            "hasPlay": false
          },
          {
            "name": "CustomRange",
            "id": "components-donut-meter--custom-range",
            "hasPlay": true
          },
          {
            "name": "CustomCenter",
            "id": "components-donut-meter--custom-center",
            "hasPlay": false
          },
          {
            "name": "WithoutCenterValue",
            "id": "components-donut-meter--without-center-value",
            "hasPlay": false
          },
          {
            "name": "AriaLabelOnly",
            "id": "components-donut-meter--aria-label-only",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-donut-meter--long-label",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "components-donut-meter--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-donut-meter--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/DonutMeter/DonutMeter.md",
        "summary": "A circular meter for one proportion: how much of a whole is used, done or filled (storage used, goal reached, stock level). The percentage is printed in the center, formatted with `Intl` for the active locale. Built on React Aria `Meter` (`role=\"meter\"`). It is a separate component from `Progress`/`Meter`, in its own folder, and does not change them.\n\n- TODO `GAP-007`.",
        "whenToUse": [
          "One share of a known total, as a compact visual in a card or a table cell.",
          "When the center number is the main message and the ring supports it."
        ],
        "whenNotToUse": [
          "Progress of a task that is running: use `Progress`.",
          "A bar in a column or a row: use `Meter` (`Progress` module).",
          "Several parts of a whole: use `ProportionBar` (a legend with share, value and count columns). One part of a known total as a bar, with the remainder named, is also `ProportionBar` with one segment and `total`. Percentages next to a ring or chart series: `Legend` with `showShare`.",
          "A trend over time: use `Sparkline` or `Chart`."
        ],
        "keyboard": [],
        "examples": [
          "<DonutMeter label=\"Storage used\" value={62} />\n\n<DonutMeter\n  label=\"Storage\"\n  value={6.2}\n  maxValue={10}\n  valueLabel=\"6.2 of 10 GB\"\n  tone=\"warning\"\n  size=\"lg\"\n/>\n\n<DonutMeter aria-label=\"Trial\" value={40} centerLabel=\"12 d\" valueLabel=\"12 days left\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- One share of a known total, as a compact visual in a card or a table cell.\n- When the center number is the main message and the ring supports it."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Progress of a task that is running: use `Progress`.\n- A bar in a column or a row: use `Meter` (`Progress` module).\n- Several parts of a whole: use `ProportionBar` (a legend with share, value and count columns). One part of a known total as a bar, with the remainder named, is also `ProportionBar` with one segment and `total`. Percentages next to a ring or chart series: `Legend` with `showShare`.\n- A trend over time: use `Sparkline` or `Chart`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=meter] aria-valuenow/min/max/valuetext\n  div.ring\n    svg[aria-hidden]   (track circle + filled circle)\n    span.center        (percentage or centerLabel)\n  label                (optional, under the ring)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                    | Type                                                          | Default     | Notes                                                                                     |\n| ----------------------- | ------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------- |\n| `value`                 | `number`                                                      | `0`         | Within `minValue` and `maxValue`; clamped.                                                |\n| `minValue` / `maxValue` | `number`                                                      | `0` / `100` |                                                                                           |\n| `label`                 | `ReactNode`                                                   | none        | Visible name under the ring. One of `label`, `aria-label`, `aria-labelledby` is required. |\n| `valueLabel`            | `string`                                                      | percentage  | What screen readers hear (\"6.2 of 10 GB\"). The center still shows the percentage.         |\n| `tone`                  | `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'accent'`  | Color of the ring. Never the only signal.                                                 |\n| `size`                  | `'sm' \\| 'md' \\| 'lg'`                                        | `'md'`      | Diameter 48 / 72 / 96px (`--ds-donut-meter-size-*`).                                      |\n| `showValue`             | `boolean`                                                     | `true`      | Prints the percentage in the center.                                                      |\n| `centerLabel`           | `ReactNode`                                                   | none        | Replaces the percentage in the center (visual only; pair with `valueLabel`).              |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `role=\"meter\"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and `aria-valuetext` from React Aria. An accessible name is required (types and a development warning). The drawing is hidden from assistive technology; the center text is covered by `aria-valuetext`.\n- Color is never the only signal: the percentage is printed in the center. The ring and track keep 3:1 against the surface in every tone.\n- Motion: the ring animates 300ms; none under `prefers-reduced-motion`. Forced colors: track `GrayText`, ring `CanvasText`.\n- Not interactive and not focusable."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Label: what is measured, in sentence case (\"Storage used\"). The unit and total belong in `valueLabel` (\"6.2 of 10 GB\").\n- Pair a tone change with text that says why (\"Almost full\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<DonutMeter label=\"Storage used\" value={62} />\n\n<DonutMeter\n  label=\"Storage\"\n  value={6.2}\n  maxValue={10}\n  valueLabel=\"6.2 of 10 GB\"\n  tone=\"warning\"\n  size=\"lg\"\n/>\n\n<DonutMeter aria-label=\"Trial\" value={40} centerLabel=\"12 d\" valueLabel=\"12 days left\" />\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Progress",
            "reason": "Progress of a task that is running: use `Progress`."
          },
          {
            "name": "Meter",
            "reason": "A bar in a column or a row: use `Meter` (`Progress` module)."
          },
          {
            "name": "ProportionBar",
            "reason": "Several parts of a whole: use `ProportionBar` (a legend with share, value and count columns). One part of a known total as a bar, with the remainder named, is also `ProportionBar` with one segment and `total`. Percentages next to a ring or chart series: `Legend` with `showShare`."
          },
          {
            "name": "Legend",
            "reason": "Several parts of a whole: use `ProportionBar` (a legend with share, value and count columns). One part of a known total as a bar, with the remainder named, is also `ProportionBar` with one segment and `total`. Percentages next to a ring or chart series: `Legend` with `showShare`."
          },
          {
            "name": "Sparkline",
            "reason": "A trend over time: use `Sparkline` or `Chart`."
          },
          {
            "name": "Chart",
            "reason": "A trend over time: use `Sparkline` or `Chart`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Drawer",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Drawer } from 'loui-ds';",
      "folder": "src/components/Drawer",
      "sourceFile": "src/components/Drawer/Drawer.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A non-modal side panel for filters, inspectors and secondary navigation. It sits next to the\ncontent in the page flow: there is no scrim, the page stays interactive, focus is not trapped\nand the page is not made inert. Place it as a sibling of the main content inside a flex or grid\ncontainer; it renders nothing while closed.\n\nUse `Sheet` instead when the panel is a task that must be finished or dismissed first\n(editing one record): that one is modal.\n\n`Escape` closes the drawer while focus is inside it and returns focus to the trigger.",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": true,
          "description": "Heading of the drawer and its accessible name."
        },
        {
          "name": "children",
          "type": "ReactNode | ((options: DrawerRenderOptions) => ReactNode)",
          "required": false,
          "description": "Body of the drawer; it scrolls when it is taller than the drawer. A function receives `close`."
        },
        {
          "name": "footer",
          "type": "ReactNode | ((options: DrawerRenderOptions) => ReactNode)",
          "required": false,
          "description": "Footer actions, aligned to the end. A function receives `close`."
        },
        {
          "name": "placement",
          "type": "'start' | 'end'",
          "required": false,
          "default": "'end'",
          "description": "Edge the drawer sits against: `end` (default) or `start`. Both follow the writing direction.",
          "values": ["start", "end"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Width: `sm` 280px, `md` 320px (default), `lg` 400px. It never exceeds its container.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "padded",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Pads the body. Turn it off for a table or a full-bleed list. Default `true`."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the drawer opens or closes through the user (close button or `Escape`)."
        },
        {
          "name": "autoFocus",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Moves focus into the drawer when it opens after the first render. A drawer that is open on\nfirst render never takes focus. Default `true`; turn it off for a panel that should stay\nbeside the page without interrupting what the user is doing."
        },
        {
          "name": "role",
          "type": "'complementary' | 'region'",
          "required": false,
          "default": "'complementary'",
          "description": "Landmark role. `complementary` (default) for content that supports the page (filters,\ninspectors); `region` for a section the user may want to find in the landmark list.",
          "values": ["complementary", "region"]
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2",
          "description": "Level of the title heading. Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "messages",
          "type": "Partial<{ close: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the drawer panel."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the drawer panel."
        }
      ],
      "inheritedProps": [],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Drawer",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "close",
            "kind": "string",
            "default": "Close panel",
            "description": "Name of the close button in the Drawer header."
          }
        ]
      },
      "cssVariables": [
        "--ds-drawer-from",
        "--ds-drawer-padding",
        "--ds-drawer-shift",
        "--ds-drawer-width",
        "--ds-drawer-width-lg",
        "--ds-drawer-width-md",
        "--ds-drawer-width-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-subtle",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface",
        "--ds-text",
        "--ds-topbar-h",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Drawer",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-drawer--default",
            "hasPlay": false
          },
          {
            "name": "AtStart",
            "id": "components-drawer--at-start",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "components-drawer--small",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-drawer--large",
            "hasPlay": false
          },
          {
            "name": "Region",
            "id": "components-drawer--region",
            "hasPlay": false
          },
          {
            "name": "Unpadded",
            "id": "components-drawer--unpadded",
            "hasPlay": false
          },
          {
            "name": "LongContent",
            "id": "components-drawer--long-content",
            "hasPlay": false
          },
          {
            "name": "Rtl",
            "id": "components-drawer--rtl",
            "hasPlay": false
          },
          {
            "name": "OpenAndCloseWithKeyboard",
            "id": "components-drawer--open-and-close-with-keyboard",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Drawer/Drawer.md",
        "summary": "A non-modal side panel that sits next to the content, for filters, inspectors and secondary navigation. There is no scrim, the page stays interactive and focus is not trapped. Generic: it lives in the core entry point.",
        "whenToUse": [],
        "whenNotToUse": [
          "A short confirmation or form: `Dialog`.",
          "A few controls anchored to a button: `Popover`.",
          "Top-level navigation: `Sidebar`."
        ],
        "keyboard": [
          "`Escape` closes the drawer when focus is inside it (and when no inner overlay handled it) and focus returns to the element that had it before the drawer opened, normally the trigger. Closing from the close button does the same."
        ],
        "examples": [
          "const [open, setOpen] = useState(false);\n\n<div style={{ display: 'flex' }}>\n  <main>\n    <Button onPress={() => setOpen(true)}>Open filters</Button>\n  </main>\n  <Drawer\n    title=\"Filters\"\n    isOpen={open}\n    onOpenChange={setOpen}\n    footer={({ close }) => (\n      <Button variant=\"primary\" onPress={close}>\n        Apply filters\n      </Button>\n    )}\n  >\n    <Input label=\"Customer\" />\n  </Drawer>\n</div>;"
        ],
        "sections": [
          {
            "id": "drawer-or-sheet",
            "heading": "Drawer or Sheet",
            "content": "|          | Drawer                                                                   | Sheet                                                      |\n| -------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- |\n| Modality | Non-modal: page stays usable                                             | Modal: page is `inert`, scroll locked                      |\n| Layout   | In the page flow, beside the content                                     | Overlay above the page, with a scrim                       |\n| Focus    | Not trapped; Tab leaves the drawer                                       | Trapped and restored                                       |\n| Use for  | Filters, inspector for the current selection, secondary navigation, help | Viewing or editing one record, a task to finish or dismiss |\n\nRule of thumb: if the user should keep working in the page while the panel is open, use a `Drawer`. If the panel is a task that needs a decision before going back, use a `Sheet`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A short confirmation or form: `Dialog`.\n- A few controls anchored to a button: `Popover`.\n- Top-level navigation: `Sidebar`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\naside (role complementary | region, aria-labelledby = title)\n  header     --ds-topbar-h tall: heading + close IconButton\n  body       scrolls; padded or not\n  footer     optional, end-aligned actions\n```\n\nPlace the drawer as a sibling of the main content in a flex or grid container. It renders nothing while closed and does not animate out."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                      | Type                                    | Default         | Notes                                                                               |\n| ----------------------------------------- | --------------------------------------- | --------------- | ----------------------------------------------------------------------------------- |\n| `title`                                   | `ReactNode` (required)                  | none            | Heading and accessible name.                                                        |\n| `children` / `footer`                     | `ReactNode \\| ({ close }) => ReactNode` |                 | Body and end-aligned actions.                                                       |\n| `placement`                               | `'start' \\| 'end'`                      | `end`           | Edge it sits against; follows the writing direction.                                |\n| `size`                                    | `'sm' \\| 'md' \\| 'lg'`                  | `md`            | 280 / 320 / 400 px (`--ds-drawer-width-*`).                                         |\n| `padded`                                  | `boolean`                               | `true`          | Off for a table or list.                                                            |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                         | closed          | Controlled / uncontrolled. `onOpenChange` fires for user closes (button, `Escape`). |\n| `autoFocus`                               | `boolean`                               | `true`          | Moves focus to the panel when it opens after first render.                          |\n| `role`                                    | `'complementary' \\| 'region'`           | `complementary` | Landmark role.                                                                      |\n| `headingLevel`                            | `1`-`6`                                 | `2`             | Level of the title.                                                                 |\n| `messages`                                | `{ close }`                             |                 | \"Close panel\" (pt-BR \"Fechar painel\").                                              |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A named landmark (`complementary` by default, `region` on request) labelled by its title.\n- Not modal: nothing is hidden from assistive technology and Tab moves freely between the drawer and the page.\n- When the drawer opens after first render, focus moves to the panel (it is programmatically focusable, `tabIndex=-1`), so the next Tab reaches its first control. A drawer that is open on first render never takes focus; set `autoFocus={false}` to keep focus where it is.\n- `Escape` closes the drawer when focus is inside it (and when no inner overlay handled it) and focus returns to the element that had it before the drawer opened, normally the trigger. Closing from the close button does the same.\n- If the consumer closes a controlled drawer from outside (for example a toolbar toggle), focus is left where the user put it.\n- Slide-in is a short fade plus 16 px move (`--ds-drawer-shift`); it is removed under `prefers-reduced-motion` and mirrors in right-to-left layouts (`:dir(rtl)`). Borders and padding use logical properties.\n- The width is capped by its container (`max-inline-size: 100%`), so on narrow containers it fills the width instead of overflowing."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: the purpose in sentence case (\"Filters\", \"Order details\").\n- Footer buttons are verb plus object (\"Apply filters\", \"Clear filters\"). Use one primary action."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nconst [open, setOpen] = useState(false);\n\n<div style={{ display: 'flex' }}>\n  <main>\n    <Button onPress={() => setOpen(true)}>Open filters</Button>\n  </main>\n  <Drawer\n    title=\"Filters\"\n    isOpen={open}\n    onOpenChange={setOpen}\n    footer={({ close }) => (\n      <Button variant=\"primary\" onPress={close}>\n        Apply filters\n      </Button>\n    )}\n  >\n    <Input label=\"Customer\" />\n  </Drawer>\n</div>;\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "New component (GAP-006): the handoff only has the modal `Sheet`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Dialog",
            "reason": "A short confirmation or form: `Dialog`."
          },
          {
            "name": "Popover",
            "reason": "A few controls anchored to a button: `Popover`."
          },
          {
            "name": "Sidebar",
            "reason": "Top-level navigation: `Sidebar`."
          }
        ],
        "uses": ["IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "DueIndicator",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { DueIndicator } from 'loui-ds';",
      "folder": "src/components/DueIndicator",
      "sourceFile": "src/components/DueIndicator/DueIndicator.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A due date with a status in words: \"Mar 9  Overdue by 3 days\", \"Due today\", \"Due in 5 days\",\n\"Paid\", \"Paid Oct 30\". A dot and the text color follow the tone (`danger` overdue, `warning`\nwithin a week, `neutral` later, `success` paid), but the status is always text, never color\nalone. Pass `label` + `tone` instead of `date` for free-text chips (\"Pickup at 2 pm\").\n\nThe urgency comes from `dueStatus`, the wording from messages and `Intl` (`formatRelativeDays`),\nthe date from `formatDate`. Day arithmetic is calendar-based, so daylight-saving changes never\nshift the count.",
      "props": [
        {
          "name": "date",
          "type": "DueDateInput",
          "required": false,
          "description": "The due date: `YYYY-MM-DD` (`ISODate`, never shifted by a time zone), a `Date` or an ISO\ndate-time string (`2026-10-31T23:30:00-03:00`). A `Date` or date-time maps to the **local**\ncalendar day of the viewer. Invalid values throw in development."
        },
        {
          "name": "today",
          "type": "DueDateInput",
          "required": false,
          "description": "The current day, same accepted forms as `date`. Defaults to the user's local day at render\ntime. Pass it to make output deterministic (tests, stories, server rendering)."
        },
        {
          "name": "paid",
          "type": "boolean",
          "required": false,
          "description": "The item is settled: shows \"Paid\" in the success tone, whatever the date."
        },
        {
          "name": "paidDate",
          "type": "DueDateInput",
          "required": false,
          "description": "The day the item was paid (same forms as `date`). Implies `paid` and shows \"Paid Oct 30\"\nfrom the `paidOn` message, formatted with `dateStyle`."
        },
        {
          "name": "showDate",
          "type": "boolean",
          "required": false,
          "description": "Shows the formatted date before the status. Default `true`."
        },
        {
          "name": "dateStyle",
          "type": "'short' | 'medium' | 'long' | 'full' | 'monthDay' | 'monthYear'",
          "required": false,
          "description": "Format of the date (`formatDate` styles). Default `monthDay` (\"Mar 9\").",
          "values": ["short", "medium", "long", "full", "monthDay", "monthYear"]
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "The status text (\"Pickup at 2 pm\"). It carries the meaning; the tone only reinforces it."
        },
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the dot and the text.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "messages",
          "type": "Partial<{ overdue: (days: number) => string; due: (relative: string) => string; paid: string; paidOn: (date: string) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "messages": {
        "providerKey": "DueIndicator",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "overdue",
            "kind": "function",
            "default": "Overdue by 2 days",
            "signature": "(days: number)",
            "description": "Status of a date in the past. `days` is a positive whole number."
          },
          {
            "key": "due",
            "kind": "function",
            "default": "Due {relative}",
            "signature": "(relative: string)",
            "description": "Status of a date today or later. `relative` is the locale's relative time from\n`formatRelativeDays` (\"today\", \"tomorrow\", \"in 3 days\")."
          },
          {
            "key": "paid",
            "kind": "string",
            "default": "Paid",
            "description": "Status of a settled item: the due date no longer matters."
          },
          {
            "key": "paidOn",
            "kind": "function",
            "default": "Paid {date}",
            "signature": "(date: string)",
            "description": "Status of an item paid on a known day. `date` is already formatted for the locale\n(\"Oct 30\"). Word it without grammatical gender so it fits any item (\"Paid\", not \"Paga\")."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-due-indicator-dot-size",
        "--ds-due-indicator-gap",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-radius-pill",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/DueIndicator",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-due-indicator--default",
            "hasPlay": true
          },
          {
            "name": "Overdue",
            "id": "components-due-indicator--overdue",
            "hasPlay": true
          },
          {
            "name": "DueToday",
            "id": "components-due-indicator--due-today",
            "hasPlay": false
          },
          {
            "name": "DueTomorrow",
            "id": "components-due-indicator--due-tomorrow",
            "hasPlay": false
          },
          {
            "name": "Later",
            "id": "components-due-indicator--later",
            "hasPlay": false
          },
          {
            "name": "Paid",
            "id": "components-due-indicator--paid",
            "hasPlay": true
          },
          {
            "name": "PaidWithDate",
            "id": "components-due-indicator--paid-with-date",
            "hasPlay": true
          },
          {
            "name": "DateObject",
            "id": "components-due-indicator--date-object",
            "description": "A `Date` is mapped to the viewer's local calendar day.",
            "hasPlay": true
          },
          {
            "name": "DateTimeString",
            "id": "components-due-indicator--date-time-string",
            "description": "An ISO date-time string maps to the local calendar day, here the 14th.",
            "hasPlay": true
          },
          {
            "name": "LabelAndTone",
            "id": "components-due-indicator--label-and-tone",
            "description": "Free-text chips: the label carries the meaning, the tone only reinforces it.",
            "hasPlay": true
          },
          {
            "name": "WithoutDate",
            "id": "components-due-indicator--without-date",
            "hasPlay": false
          },
          {
            "name": "LongDate",
            "id": "components-due-indicator--long-date",
            "hasPlay": false
          },
          {
            "name": "AcrossDaylightSaving",
            "id": "components-due-indicator--across-daylight-saving",
            "description": "Calendar-day arithmetic: the daylight-saving change does not shift the count.",
            "hasPlay": true
          },
          {
            "name": "AllTones",
            "id": "components-due-indicator--all-tones",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "components-due-indicator--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-due-indicator--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/DueIndicator/DueIndicator.md",
        "summary": "A due date with a status in words and a tone dot: \"Mar 9 Overdue by 3 days\", \"Due today\", \"Due in 5 days\", \"Paid\", \"Paid Oct 30\". Color reinforces the status; the text always states it.\n\n- Handoff source: `components/erp/DueIndicator.jsx` (TODO `CMP-PAT-07`).",
        "whenToUse": [
          "A deadline in a list row, a card or a detail header (invoice due, task due, renewal).",
          "When urgency should be readable at a glance without relying on color."
        ],
        "whenNotToUse": [
          "A timestamp of something that already happened: format it with `formatDate` / `formatRelativeDays`.",
          "A workflow status that is not date-driven: use `Badge`."
        ],
        "keyboard": [],
        "examples": [
          "<DueIndicator date={invoice.dueISO} paid={invoice.paid} />\n\n// A Date or date-time maps to the local calendar day\n<DueIndicator date={order.dueAt} paidDate={order.paidAt} />\n\n// Deterministic output\n<DueIndicator date=\"2026-03-14\" today=\"2026-03-10\" dateStyle=\"long\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A deadline in a list row, a card or a detail header (invoice due, task due, renewal).\n- When urgency should be readable at a glance without relying on color."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A timestamp of something that already happened: format it with `formatDate` / `formatRelativeDays`.\n- A workflow status that is not date-driven: use `Badge`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan[data-tone=danger|warning|neutral|success]\n  span.dot          (aria-hidden)\n  time[datetime]    formatDate(date, locale, dateStyle)     (showDate)\n  span.status       \"Overdue by 3 days\" | \"Due today\" | \"Due in 5 days\" | \"Paid\"\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "Two forms, enforced by a discriminated union: pass `date`, or pass `label` + `tone`.\n\n| Prop        | Type                                                          | Default       | Notes                                                                                      |\n| ----------- | ------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |\n| `date`      | `ISODate \\| Date \\| string`                                   | required      | `YYYY-MM-DD`, a `Date`, or an ISO date-time string. Malformed values throw in development. |\n| `today`     | `ISODate \\| Date \\| string`                                   | local day now | Same forms as `date`. Inject for tests, stories and deterministic output.                  |\n| `paid`      | `boolean`                                                     | `false`       | Shows \"Paid\" in the success tone, ignoring the date.                                       |\n| `paidDate`  | `ISODate \\| Date \\| string`                                   | none          | Implies `paid` and shows \"Paid Oct 30\" (`paidOn` message, formatted with `dateStyle`).     |\n| `showDate`  | `boolean`                                                     | `true`        | Shows the formatted due date before the status.                                            |\n| `dateStyle` | `DateStyle`                                                   | `'monthDay'`  | Any `formatDate` style; also used for `paidDate`.                                          |\n| `label`     | `string`                                                      | none          | Free-text form: the status text. Requires `tone`; excludes `date`.                         |\n| `tone`      | `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'` | none          | Free-text form only. In the date form the tone is computed.                                |\n| `messages`  | `{ overdue, due, paid, paidOn }`                              | none          | Overrides the texts for this instance.                                                     |\n\nTone in the date form comes from `dueStatus`: overdue is `danger`, 0 to 7 days `warning`, later `neutral`.\n\n`paidDate` is a separate prop instead of widening `paid` to `boolean | date`: `paid` stays a plain flag, and \"paid, date unknown\" and \"paid on Oct 30\" read differently at the call site."
          },
          {
            "id": "time-zones",
            "heading": "Time zones",
            "content": "- `YYYY-MM-DD` is a calendar day with no time zone and takes a fast path: it is used as written and never shifted.\n- A `Date` or an ISO date-time string (`2026-10-31T23:30:00-03:00`, `2026-10-31T23:30:00Z`) is an instant. It is converted to the **viewer's local calendar day** with `toISODate`, so `2026-11-01T01:30:00Z` is Oct 31 in Sao Paulo and Nov 1 in Berlin. A date-time without an offset is read as local time.\n- Only strings that contain a time part (`YYYY-MM-DDTHH...` or `YYYY-MM-DD HH...`) are parsed with `new Date()`. If the due day must not depend on the viewer, send `YYYY-MM-DD`.\n- `today` follows the same rules. Unparsable date-times and malformed dates throw in development."
          },
          {
            "id": "free-text-form",
            "heading": "Free-text form",
            "content": "For chips whose text is not date-driven (\"Due today\", \"Pickup at 2 pm\", \"Waiting for you\"), pass `label` and `tone`. It renders the same dot and text; no date is shown and no wording is generated, so translate `label` yourself.\n\n```tsx\n<DueIndicator label=\"Pickup at 2 pm\" tone=\"neutral\" />\n<DueIndicator label=\"Waiting for you\" tone=\"accent\" />\n```"
          },
          {
            "id": "paid-date-and-locales",
            "heading": "Paid date and locales",
            "content": "`paidDate` calls the `paidOn(date)` message with the date already formatted by `formatDate` for the active locale. The default is `Paid ${date}` (en) and `Pagamento em ${date}` (pt-BR), which avoids gendered agreement (\"Paga em\" / \"Pago em\" would have to match the item). Override it per instance (`messages`), per provider or in a locale catalog:\n\n```tsx\n<DueIndicator\n  date=\"2026-03-01\"\n  paidDate=\"2026-03-08\"\n  messages={{ paidOn: (date) => `Settled on ${date}` }}\n/>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The status is text (`Overdue by 3 days`), so color is never the only signal; the dot is `aria-hidden`.\n- The date is a `<time datetime>` element.\n- Contrast: the status uses `--ds-danger`, `--ds-warning`, `--ds-success` or `--ds-text-2` on the page background, all above 4.5:1.\n- Forced colors: the dot and the text fall back to `CanvasText`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- \"Overdue by 3 days\", \"Due today\", \"Due tomorrow\", \"Due in 5 days\", \"Paid\". Sentence case, no exclamation marks.\n- Dates and relative wording come from `Intl` for the active locale; never build them by hand."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<DueIndicator date={invoice.dueISO} paid={invoice.paid} />\n\n// A Date or date-time maps to the local calendar day\n<DueIndicator date={order.dueAt} paidDate={order.paidAt} />\n\n// Deterministic output\n<DueIndicator date=\"2026-03-14\" today=\"2026-03-10\" dateStyle=\"long\" />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `dueInfo` left the component: urgency is the tested `dueStatus` utility and day arithmetic is `daysBetween` (UTC calendar math, no `T12:00:00` trick), so daylight-saving changes never shift the count. Component tests cover US and EU transitions and a leap day.\n- Month names are no longer a table: dates come from `formatDate`; wording and plurals come from messages (`en`, `pt-BR`) and `formatRelativeDays`.\n- Texts are English by default (\"Overdue by 3 days\"), not hard-coded Portuguese; `paidText` is now the `paid` message.\n- `today` stays injectable; the default is the local day of the system clock. Colors and sizes are tokens."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Badge",
            "reason": "A workflow status that is not date-driven: use `Badge`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "EmptyState",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { EmptyState } from 'loui-ds';",
      "folder": "src/components/EmptyState",
      "sourceFile": "src/components/EmptyState/EmptyState.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Tells the user that there is nothing to show, why, and what to do next. The copy follows\n\"what happened + what to do\".",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": true,
          "description": "What happened, in one short sentence: \"No orders found\". Rendered as a heading\n(see `headingLevel`)."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2",
          "description": "Heading level of the title (`h1` to `h6`). Pick the level that fits the page outline. Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "What the user can do about it: \"Try another filter or search term.\""
        },
        {
          "name": "icon",
          "type": "ReactNode",
          "required": false,
          "description": "Optional icon above the title. Decorative: it is hidden from assistive technology. No illustrations."
        },
        {
          "name": "action",
          "type": "ReactNode",
          "required": false,
          "description": "The primary action, usually a `Button`. Rendered after the secondary action."
        },
        {
          "name": "secondaryAction",
          "type": "ReactNode",
          "required": false,
          "description": "A second, lower-emphasis action."
        },
        {
          "name": "compact",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Less padding and a smaller title, for use inside a card, a panel or a table."
        },
        {
          "name": "bordered",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Dashed border. Set to `false` when the empty state already sits inside a bordered container. Default `true`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-empty-state-compact-padding-block",
        "--ds-empty-state-compact-padding-inline",
        "--ds-empty-state-description-max-w",
        "--ds-empty-state-padding-block",
        "--ds-empty-state-padding-inline",
        "--ds-fs-heading",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-radius-lg",
        "--ds-space-2",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/EmptyState",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-empty-state--default",
            "hasPlay": false
          },
          {
            "name": "WithActions",
            "id": "components-empty-state--with-actions",
            "hasPlay": false
          },
          {
            "name": "Compact",
            "id": "components-empty-state--compact",
            "hasPlay": false
          },
          {
            "name": "WithoutBorder",
            "id": "components-empty-state--without-border",
            "hasPlay": false
          },
          {
            "name": "WithIcon",
            "id": "components-empty-state--with-icon",
            "hasPlay": false
          },
          {
            "name": "TitleOnly",
            "id": "components-empty-state--title-only",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-empty-state--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-empty-state--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/EmptyState/EmptyState.md",
        "summary": "A typographic placeholder for a list, table or panel that has nothing to show. It says what happened and what the user can do about it.",
        "whenToUse": [
          "A list, table or search has no results.",
          "A feature has no data yet and the user can create some."
        ],
        "whenNotToUse": [
          "While data is loading: use `Skeleton`.",
          "When loading failed: use `Alert` with a retry action.",
          "For a short inline note inside a field: use the field's description."
        ],
        "keyboard": [],
        "examples": [
          "<EmptyState\n  title=\"No orders found\"\n  description=\"Try another filter or search term.\"\n  action={<Button>Clear filters</Button>}\n/>\n\n<EmptyState compact bordered={false} headingLevel={3} title=\"No comments\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A list, table or search has no results.\n- A feature has no data yet and the user can create some."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- While data is loading: use `Skeleton`.\n- When loading failed: use `Alert` with a retry action.\n- For a short inline note inside a field: use the field's description."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root (dashed border, centered)\n  span[aria-hidden]  icon (optional)\n  h{headingLevel}    title\n  p                  description (optional)\n  div                secondaryAction, action\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop              | Type                         | Default | Notes                                                              |\n| ----------------- | ---------------------------- | ------- | ------------------------------------------------------------------ |\n| `title`           | `ReactNode`                  | -       | What happened. Rendered as a heading                               |\n| `headingLevel`    | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | `2`     | Match the page outline (inside a `Card` titled with `h2`, use `3`) |\n| `description`     | `ReactNode`                  |         | What to do next. Max width 360 px                                  |\n| `icon`            | `ReactNode`                  |         | Optional, decorative. No illustrations                             |\n| `action`          | `ReactNode`                  |         | Primary action (usually `Button`)                                  |\n| `secondaryAction` | `ReactNode`                  |         | Lower emphasis; shown before the primary action                    |\n| `compact`         | `boolean`                    | `false` | Less padding, smaller title. For cards, panels, table bodies       |\n| `bordered`        | `boolean`                    | `true`  | Dashed border. Turn off inside an already bordered container       |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "copy-what-happened-what-to-do",
            "heading": "Copy: what happened + what to do",
            "content": "Title: say what is missing, in sentence case, without apologies or exclamation marks. Description: tell the user the next step, in one sentence.\n\n| Situation        | Title             | Description                                              | Action        |\n| ---------------- | ----------------- | -------------------------------------------------------- | ------------- |\n| Filtered to zero | No orders found   | Try another filter or search term.                       | Clear filters |\n| First use        | No orders yet     | Orders created here or in the store appear in this list. | Create order  |\n| Cleared inbox    | Nothing to review | New approvals appear here as they arrive.                | (none)        |\n\nAvoid \"Oops\", \"Sorry\", blame (\"You have no orders\") and filler (\"There is nothing to show here at the moment\")."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The title is a real heading: choose `headingLevel` so the outline stays sequential. The heading lets screen reader users find the empty region by navigating headings.\n- The icon is decorative and hidden from assistive technology; the text carries the meaning.\n- The component does not announce itself. If the empty state replaces content after a user action (for example a search), announce the result count in your own live region.\n- Actions are ordinary buttons or links with their own names; order in the DOM is secondary then primary, matching the visual order."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<EmptyState\n  title=\"No orders found\"\n  description=\"Try another filter or search term.\"\n  action={<Button>Clear filters</Button>}\n/>\n\n<EmptyState compact bordered={false} headingLevel={3} title=\"No comments\" />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The title is a heading (`<h2>` by default) instead of a `span`; the level is configurable.\n- Optional decorative `icon` slot (no illustrations, per the visual foundations).\n- The handoff Portuguese sample copy is gone; the guidance above replaces it.\n- Padding, description width and the compact padding are component tokens (`--ds-empty-state-*`). No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Skeleton",
            "reason": "While data is loading: use `Skeleton`."
          },
          {
            "name": "Alert",
            "reason": "When loading failed: use `Alert` with a retry action."
          }
        ],
        "uses": [],
        "usedBy": ["ActionList", "DataTable"]
      }
    },
    {
      "name": "ErrorText",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ErrorText } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Standalone error text for a group of controls that has no React Aria field around it (a\nhand-built set of colour options, a list of rows). Same look as `FieldError`, but it needs no\nfield context and renders whenever it has content: you decide when it shows. Link it from\nthe group with `aria-describedby` , see Field.md. Use `FieldError` inside\na field and `Alert` for an error about the whole page or form.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The message. Renders nothing when empty (`null`, `undefined`, `false` or `''`)."
        },
        {
          "name": "id",
          "type": "string",
          "required": false,
          "description": "Element id. Defaults to a generated one; pass your own to link the group with\n`aria-describedby={id}` (and `aria-errormessage={id}`)."
        },
        {
          "name": "announce",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Announces the message when it appears by rendering it as `role=\"alert\"`. Off by default:\nthe group links the text with `aria-describedby`, and focus normally moves to the first\ninvalid control on submit, which reads it. Turn it on for an error that appears with no\nfocus move (for example after a server response)."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLParagraphElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": ["ColorOptionGroup"]
      }
    },
    {
      "name": "FieldError",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { FieldError } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Error text of a field. Renders only while the field is invalid and the error is *displayed*:\nwith the default `validationBehavior=\"native\"` that is after a submit attempt (see Field.md).\nThe message sits in a `role=\"alert\"` element so it is announced when it appears; on submit focus also\nmoves to the first invalid field, which reads it again through `aria-describedby`.",
      "props": [
        {
          "name": "children",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "The message. A function receives the validation result and decides everything. A static\nmessage replaces only the browser's own constraint text; an error the app supplied (a\n`validate` rule, or `validationErrors` on a `Form`) is shown instead of it. Without\nchildren, the validation errors are shown (the browser's message for native constraints)."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "FieldMessages",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { FieldMessages } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Description followed by the error: the footer every field renders under its control.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": ""
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description, see `Description`.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": [
          "Checkbox",
          "CheckboxGroup",
          "ChoiceTiles",
          "ColorField",
          "Combobox",
          "DatePicker",
          "Input",
          "NumberField",
          "Radio",
          "RadioGroup",
          "Select",
          "Switch",
          "TagInput",
          "Textarea"
        ]
      }
    },
    {
      "name": "FieldSet",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { FieldSet } from 'loui-ds';",
      "folder": "src/components/FieldSet",
      "sourceFile": "src/components/FieldSet/FieldSet.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Groups related fields under a caption: a real `fieldset` with a `legend`, an optional\ndescription, and disabled propagation. Use it for a set of fields that answer one question\n(an address, notification channels), not for every section of a form.",
      "props": [
        {
          "name": "legend",
          "type": "ReactNode",
          "required": true,
          "description": "Caption of the group, required. It is the `legend`: screen readers announce it before each\nfield in the group (\"Billing address, Street\"). Write it in sentence case; CSS makes it a\nmono caption."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Optional help for the whole group, linked to the fieldset with `aria-describedby`."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Disables every native control inside (inputs, selects, textareas, buttons), the way\n`<fieldset disabled>` does: they cannot be focused or submitted and dim together.\nControls that are not native elements (menus, custom widgets) are not affected: disable\nthose individually."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "FieldsetHTMLAttributes",
          "domGeneric": true,
          "count": 2
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-field-set-gap",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-opacity-disabled",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/FieldSet",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-field-set--default",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-field-set--with-description",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-field-set--disabled",
            "description": "Every native control inside is disabled together.",
            "hasPlay": true
          },
          {
            "name": "WithRadioGroup",
            "id": "components-field-set--with-radio-group",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-field-set--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-field-set--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/FieldSet/FieldSet.md",
        "summary": "A group of related fields under a caption: a real `fieldset` with a `legend`, an optional description and disabled propagation.",
        "whenToUse": [
          "A set of fields that answer one question: an address, a date range, notification channels.",
          "A group that should be disabled together."
        ],
        "whenNotToUse": [
          "For every section of a form: a heading is enough to organize a long form.",
          "For a single `RadioGroup` or `CheckboxGroup`: they already render a labelled group."
        ],
        "keyboard": [],
        "examples": [
          "<FieldSet legend=\"Billing address\" description=\"Shown on every invoice.\">\n  <Input label=\"Street\" name=\"street\" />\n  <Input label=\"City\" name=\"city\" />\n</FieldSet>\n\n<FieldSet legend=\"Shipping address\" isDisabled>…</FieldSet>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A set of fields that answer one question: an address, a date range, notification channels.\n- A group that should be disabled together."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For every section of a form: a heading is enough to organize a long form.\n- For a single `RadioGroup` or `CheckboxGroup`: they already render a labelled group."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nfieldset\n  legend        mono caption, uppercase by CSS\n  p             description (optional)\n  div           the fields, stacked with --ds-field-set-gap\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop          | Type        | Default | Notes                                                                     |\n| ------------- | ----------- | ------- | ------------------------------------------------------------------------- |\n| `legend`      | `ReactNode` | —       | Required. The group's accessible name                                     |\n| `description` | `ReactNode` |         | Linked with `aria-describedby`; a consumer `aria-describedby` is kept     |\n| `isDisabled`  | `boolean`   | `false` | `fieldset disabled`: native controls inside are disabled and dim together |\n\nOther `fieldset` attributes and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A `fieldset` + `legend` is announced as a named group; screen readers read the legend before each field (\"Billing address, Street\").\n- `isDisabled` uses the native attribute, so inputs, selects, textareas and buttons inside cannot be focused or submitted, and are skipped by constraint validation. Controls that are not native elements (menus, custom widgets) are **not** affected: disable those yourself.\n- The legend uses `--ds-text-2` (the faint `--ds-text-3` is too low-contrast for a name)."
          },
          {
            "id": "legend-or-label",
            "heading": "Legend or label",
            "content": "The two names look different on purpose:\n\n| Name                                                    | Looks like                                     | Use it for                                           |\n| ------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------- |\n| `Label` (of `Input`, `Select`, `SegmentedControl`, ...) | 13/18 medium sentence-case text in `--ds-text` | Naming one field                                     |\n| `FieldSet` `legend`                                     | Mono uppercase caption in `--ds-text-2`        | Naming a group of fields, like the title of a `Card` |\n\nThe legend is a section caption, one level above the field labels it contains, so a group never reads as one more field. A single `SegmentedControl`, `RadioGroup` or `CheckboxGroup` is already a labelled group: give it a `label` and do not wrap it in a `FieldSet`. Use a `FieldSet` when several fields answer one question (\"Billing address\")."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Legends are short nouns in sentence case (\"Billing address\"); CSS uppercases them. Descriptions say what the group is for, not how to fill each field."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<FieldSet legend=\"Billing address\" description=\"Shown on every invoice.\">\n  <Input label=\"Street\" name=\"street\" />\n  <Input label=\"City\" name=\"city\" />\n</FieldSet>\n\n<FieldSet legend=\"Shipping address\" isDisabled>…</FieldSet>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff had no group component; fields were laid out in plain divs. FieldSet adds the group semantics and shared disabling."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "RadioGroup",
            "reason": "For a single `RadioGroup` or `CheckboxGroup`: they already render a labelled group."
          },
          {
            "name": "CheckboxGroup",
            "reason": "For a single `RadioGroup` or `CheckboxGroup`: they already render a labelled group."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "FileUpload",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { FileUpload } from 'loui-ds';",
      "folder": "src/components/FileUpload",
      "sourceFile": "src/components/FileUpload/FileUpload.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A drop zone with a \"Choose files\" button and the list of added files (React Aria `DropZone`\nand `FileTrigger`). Dragging is a pointer shortcut; the button is the keyboard and click way\nin. Files are validated (type, size, count) as they arrive and the errors are shown under the\nzone; added, removed and rejected files are announced politely. The component keeps no upload\nstate: pass `progress` to show a bar per file.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the drop zone, for the rules: \"PNG, JPG or PDF up to 10 MB\". Linked to the zone."
        },
        {
          "name": "accept",
          "type": "readonly string[]",
          "required": false,
          "description": "Accepted types: MIME types (`image/png`), wildcards (`image/*`) or extensions (`.pdf`).\nFiles of other types are rejected with an error message. Also filters the file dialog."
        },
        {
          "name": "maxSize",
          "type": "number",
          "required": false,
          "description": "Largest file in bytes. Larger files are rejected with an error message."
        },
        {
          "name": "maxFiles",
          "type": "number",
          "required": false,
          "description": "Most files in the list. `1` switches to single-file mode: choosing or dropping a file\nreplaces the current one. Default: no limit."
        },
        {
          "name": "value",
          "type": "readonly File[]",
          "required": false,
          "description": "The files, when controlled. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "readonly File[]",
          "required": false,
          "description": "The initial files when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((files: File[]) => void)",
          "required": false,
          "description": "Called with the full list after files are added or removed."
        },
        {
          "name": "onReject",
          "type": "((rejections: FileRejection[]) => void)",
          "required": false,
          "description": "Called with the files that were not added and why (wrong type, too large, too many)."
        },
        {
          "name": "progress",
          "type": "ReadonlyMap<File, number>",
          "required": false,
          "description": "Upload progress per file, 0 to 100, supplied by you (the component uploads nothing and\nnever invents progress). A file with a value under 100 shows a progress bar; 100 shows\n\"Done\"; a file without an entry shows only its size."
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "Name for form submission: the selected files are submitted as `File` entries under this name\n(multiple entries when several files)."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Disables choosing, dropping and removing."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the root."
        },
        {
          "name": "messages",
          "type": "Partial<{ dropZone: string; dropLabel: (multiple: boolean) => string; choose: (multiple: boolean) => string; listLabel: string; remove: (name: string) => string; uploading: (name: string) => string; ... 9 more ...; removed: (name: string) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "FileUpload",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "dropZone",
            "kind": "string",
            "default": "Drop zone",
            "description": "Accessible name of the drop target itself (React Aria adds a keyboard stop on it that accepts pasted files)."
          },
          {
            "key": "dropLabel",
            "kind": "function",
            "default": "Drop a file here",
            "signature": "(multiple: boolean)",
            "description": "Text of the drop target, which names it for assistive technology."
          },
          {
            "key": "choose",
            "kind": "function",
            "default": "Choose a file",
            "signature": "(multiple: boolean)",
            "description": "The button that opens the file dialog (the keyboard and click alternative to dragging)."
          },
          {
            "key": "listLabel",
            "kind": "string",
            "default": "Added files",
            "description": "Accessible name of the list of added files."
          },
          {
            "key": "remove",
            "kind": "function",
            "default": "Remove {name}",
            "signature": "(name: string)",
            "description": "Accessible name of the remove button of a file."
          },
          {
            "key": "uploading",
            "kind": "function",
            "default": "Uploading {name}",
            "signature": "(name: string)",
            "description": "Accessible name of the progress bar of a file that is uploading."
          },
          {
            "key": "done",
            "kind": "string",
            "default": "Done",
            "description": "Status text of a file whose progress reached 100."
          },
          {
            "key": "unitB",
            "kind": "string",
            "default": "B",
            "description": "Size units shown after the number."
          },
          {
            "key": "unitKB",
            "kind": "string",
            "default": "KB"
          },
          {
            "key": "unitMB",
            "kind": "string",
            "default": "MB"
          },
          {
            "key": "unitGB",
            "kind": "string",
            "default": "GB"
          },
          {
            "key": "wrongType",
            "kind": "function",
            "default": "\"{name}\" is not an accepted file type.",
            "signature": "(name: string)",
            "description": "Error: the file type is not accepted."
          },
          {
            "key": "tooLarge",
            "kind": "function",
            "default": "\"{name}\" is larger than {max}.",
            "signature": "(name: string, max: string)",
            "description": "Error: the file is larger than the limit (already formatted, e.g. \"10 MB\")."
          },
          {
            "key": "tooMany",
            "kind": "function",
            "default": "Only 2 files can be added. 2 not added.",
            "signature": "(max: number, skipped: number)",
            "description": "Error: files over the count limit were not added."
          },
          {
            "key": "added",
            "kind": "function",
            "default": "2 files added",
            "signature": "(n: number)",
            "description": "Live announcement after files were added."
          },
          {
            "key": "removed",
            "kind": "function",
            "default": "Removed {name}",
            "signature": "(name: string)",
            "description": "Live announcement after a file was removed."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-bg",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-file-upload-dropzone-padding-block",
        "--ds-file-upload-dropzone-padding-inline",
        "--ds-file-upload-item-padding-block",
        "--ds-file-upload-item-padding-inline",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/FileUpload",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-file-upload--default",
            "hasPlay": true
          },
          {
            "name": "WithFiles",
            "id": "components-file-upload--with-files",
            "hasPlay": false
          },
          {
            "name": "Validation",
            "id": "components-file-upload--validation",
            "hasPlay": true
          },
          {
            "name": "SingleFile",
            "id": "components-file-upload--single-file",
            "hasPlay": true
          },
          {
            "name": "WithProgress",
            "id": "components-file-upload--with-progress",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-file-upload--disabled",
            "hasPlay": true
          },
          {
            "name": "LongFileName",
            "id": "components-file-upload--long-file-name",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/FileUpload/FileUpload.md",
        "summary": "A drop zone with a \"Choose files\" button and the list of added files. Built on React Aria `DropZone` and `FileTrigger`; progress bars use [Progress](../Progress/Progress.md). The component validates and lists files; it uploads nothing. Task: CMP-INP-03.",
        "whenToUse": ["Attaching one or several files to a record or form."],
        "whenNotToUse": [
          "Image galleries, cropping or previews: a product pattern.",
          "A hidden uploader inside a button: use `FileTrigger` from React Aria directly."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Attaching one or several files to a record or form."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Image galleries, cropping or previews: a product pattern.\n- A hidden uploader inside a button: use `FileTrigger` from React Aria directly."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                         | Notes                                                                                                                                                       |\n| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`   | One is required. The root is a `group` named by it.                                                                                                         |\n| `description`                                | The rules in words (\"PNG, JPG or PDF up to 10 MB\"), linked to the zone.                                                                                     |\n| `accept`                                     | MIME types, wildcards (`image/*`) or extensions (`.pdf`). Also filters the file dialog.                                                                     |\n| `maxSize`                                    | Bytes. Larger files are rejected.                                                                                                                           |\n| `maxFiles`                                   | Count. `1` is single-file mode: a new file replaces the current one.                                                                                        |\n| `value` / `defaultValue` / `onChange(files)` | The full `File[]` after every add or remove.                                                                                                                |\n| `onReject(rejections)`                       | `{ file, reason: 'type' \\| 'size' \\| 'count' }[]` for files not added.                                                                                      |\n| `progress`                                   | `Map<File, number>` (0 to 100), yours. Under 100 shows a bar, 100 shows \"Done\", no entry shows only the size.                                               |\n| `name`                                       | Submits the files with the form (`FormData.getAll(name)` returns `File`s) through a hidden file input.                                                      |\n| `isDisabled`, `className`, `ref`, `messages` | `messages`: `dropZone`, `dropLabel`, `choose`, `listLabel`, `remove`, `uploading`, `done`, `unit*`, `wrongType`, `tooLarge`, `tooMany`, `added`, `removed`. |"
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- Errors are listed under the zone and replaced on the next add or removal. Sizes are formatted with `formatNumber` for the locale and unit messages (`1.5 MB`, pt-BR `1,5 MB`; steps of 1024).\n- Polite live region (`role=\"status\"`) announces \"2 files added\" (with the error texts), \"Removed name.pdf\". Pluralized per locale.\n- Each file row: name (truncated), size or \"Done · size\", and a ghost button \"Remove {name}\".\n- `progress` is never simulated by the component. The handoff's random progress exists only in the \"WithProgress\" story, driven by a timer."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                         | Behavior                                                                                     |\n| --------------------------- | -------------------------------------------------------------------------------------------- |\n| `Tab`                       | Zone (React Aria's own stop: `Ctrl/Cmd+V` pastes files there), Choose button, Remove buttons |\n| `Enter` / `Space` on Choose | Opens the file dialog: the keyboard and click alternative to dragging                        |\n| `Enter` / `Space` on Remove | Removes the file                                                                             |\n\nDragging is only a pointer shortcut; nothing depends on it. While files are dragged over it the zone gets an accent border and soft fill (also an outline in forced colors). The list is a `ul` named \"Added files\"."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "`description` states the limits in one sentence. Error texts name the file and the limit and are produced from messages (\"\\\"big.pdf\\\" is larger than 1 MB.\")."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<FileUpload\n  label=\"Attachments\"\n  description=\"PDF up to 10 MB.\"\n  accept={['.pdf']}\n  maxSize={10 * 1024 * 1024}\n  name=\"attachments\"\n  value={files}\n  onChange={setFiles}\n  progress={progress}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Real validation (type, size, count) with messages, a keyboard-operable button instead of a `div role=\"button\"`, live announcements, controlled value, form submission, and consumer-supplied progress.\n- Sizes and messages come from `Intl` and the catalog (the handoff hard-coded pt-BR and a comma)."
          },
          {
            "id": "manual-verification",
            "heading": "Manual verification",
            "content": "Dropping files is automated: `FileUpload.test.tsx` performs a native drag of real files through the Chrome DevTools Protocol (`commands.dropFiles`, defined in `tests/browser-commands`), because React Aria rejects synthetic `DragEvent`s. Pasting is not automatable, so check it by hand before a release that touches this component (TEST-017):\n\n1. Copy a file in the operating system file manager (Explorer, Finder), or copy an image.\n2. Tab to the drop zone (the first stop, named \"Drop zone\") so it has focus.\n3. Press `Ctrl+V` (`Cmd+V` on macOS). The file should be added to the list and announced (\"1 file added.\").\n4. Repeat with Chrome, Firefox and Safari. Record any browser where the file is not added.\n\nWhy it cannot be automated: the paste handler of React Aria reads each item through `webkitGetAsEntry()`, which returns `null` for a synthetic `DataTransfer` and for an image written to the clipboard by a script (verified in Chromium: `kind: file`, `type: image/png`, entry `null`), so the item is skipped. Only a file copied by the operating system can be tested, and no browser API can put one on the clipboard."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["Button", "Description", "IconButton", "Progress", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Form",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Form } from 'loui-ds';",
      "folder": "src/components/Form",
      "sourceFile": "src/components/Form/Form.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A form built on React Aria `Form`: native validation by default, server errors through\n`validationErrors`, a vertical rhythm of `--ds-form-gap` between its children, and an\noptional error summary. Fields inside inherit the form's `validationBehavior`.\n\nPut exactly one primary action in it, at the end (\"Save changes\"), and keep submit enabled:\na disabled submit gives no way to learn what is wrong.",
      "props": [
        {
          "name": "validationBehavior",
          "type": "'native' | 'aria'",
          "required": false,
          "default": "'native'",
          "description": "Native validation (default) uses the browser's constraint validation: a failed submit\nfocuses the first invalid field. `aria` marks fields invalid with ARIA only and never blocks\nthe submit; use it when the server is the only validator.",
          "values": ["native", "aria"]
        },
        {
          "name": "validationErrors",
          "type": "ValidationErrors",
          "required": false,
          "description": "Errors from the server, keyed by the field `name`. The matching fields become invalid and\nshow the message; the first one takes focus (or the summary does, with `errorSummary`).\nClear them (pass `undefined`) when the user edits the field or submits again."
        },
        {
          "name": "errorSummary",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Shows a `FormErrorSummary` at the top of the form. After a failed submit it takes focus and\nlists the invalid fields as links. Default `false`. To place the summary elsewhere, leave\nthis off and render `FormErrorSummary` inside the form yourself."
        },
        {
          "name": "id",
          "type": "string",
          "required": false,
          "description": "The `id` of the `<form>`. Other elements link to it with the `form` attribute, for example\na submit button in a `Sheet` footer: `<Button type=\"submit\" form=\"order-form\">`."
        },
        {
          "name": "onKeyDown",
          "type": "KeyboardEventHandler<HTMLFormElement>",
          "required": false,
          "description": "Key handlers on the `<form>`. They receive every key pressed inside a field (the events\nbubble), so a form-level shortcut such as `Ctrl+Enter` can live here. The form adds no key\nhandling of its own, so yours never replaces anything."
        },
        {
          "name": "onKeyUp",
          "type": "KeyboardEventHandler<HTMLFormElement>",
          "required": false,
          "description": "Key-up handler on the `<form>`; see `onKeyDown`."
        },
        {
          "name": "onFocus",
          "type": "FocusEventHandler<HTMLFormElement>",
          "required": false,
          "description": "Fires when focus enters the form or moves between its fields (React `focus` bubbles)."
        },
        {
          "name": "onBlur",
          "type": "FocusEventHandler<HTMLFormElement>",
          "required": false,
          "description": "Fires when focus leaves a field in the form; check `relatedTarget` to detect leaving the form."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The fields and actions of the form."
        },
        {
          "name": "messages",
          "type": "Partial<{ summaryTitle: (count: number) => string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLFormElement>",
          "required": false,
          "description": "Ref to the `<form>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FormProps",
          "domGeneric": false,
          "count": 10,
          "props": [
            {
              "name": "action",
              "type": "string | ((formData: FormData) => void | Promise<void>)",
              "required": false
            },
            {
              "name": "autoCapitalize",
              "type": "'off' | 'on' | 'none' | 'sentences' | 'words' | 'characters'",
              "required": false
            },
            {
              "name": "autoComplete",
              "type": "'off' | 'on'",
              "required": false
            },
            {
              "name": "encType",
              "type": "'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'",
              "required": false
            },
            {
              "name": "method",
              "type": "'get' | 'post' | 'dialog'",
              "required": false
            },
            {
              "name": "onInvalid",
              "type": "(event: FormEvent<HTMLFormElement>) => void",
              "required": false
            },
            {
              "name": "onReset",
              "type": "(event: FormEvent<HTMLFormElement>) => void",
              "required": false
            },
            {
              "name": "onSubmit",
              "type": "(event: FormEvent<HTMLFormElement>) => void",
              "required": false
            },
            {
              "name": "role",
              "type": "'search' | 'presentation'",
              "required": false
            },
            {
              "name": "target",
              "type": "'_blank' | '_self' | '_parent' | '_top'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Form",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "summaryTitle",
            "kind": "function",
            "default": "Fix 2 fields before saving",
            "signature": "(count: number)",
            "description": "Title of the error summary shown after a failed submit. `count` is the number of invalid fields."
          }
        ]
      },
      "cssVariables": [
        "--ds-danger",
        "--ds-danger-line",
        "--ds-danger-soft",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-form-gap",
        "--ds-form-summary-gap",
        "--ds-form-summary-padding",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-radius-lg",
        "--ds-space-5",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Form",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-form--default",
            "description": "A failed submit focuses the first invalid field, which reads its error.",
            "hasPlay": true
          },
          {
            "name": "WithErrorSummary",
            "id": "components-form--with-error-summary",
            "description": "The summary takes focus after a failed submit and links to each invalid field by its label.",
            "hasPlay": true
          },
          {
            "name": "ServerErrors",
            "id": "components-form--server-errors",
            "description": "Errors from the server, keyed by the field name.",
            "hasPlay": true
          },
          {
            "name": "ServerRoundTrip",
            "id": "components-form--server-round-trip",
            "description": "Server errors set by a submit handler, the way an app does it.",
            "hasPlay": true
          },
          {
            "name": "WithFieldSets",
            "id": "components-form--with-field-sets",
            "description": "FieldSet groups related fields; the form keeps one rhythm between groups and fields.",
            "hasPlay": false
          },
          {
            "name": "AriaValidation",
            "id": "components-form--aria-validation",
            "description": "With aria validation the browser never blocks the submit.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-form--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-form--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Form/Form.md",
        "summary": "A form built on React Aria `Form`: native validation by default, server errors, a consistent vertical rhythm and an optional error summary.",
        "whenToUse": [
          "Any set of fields submitted together: create or edit a record, settings, sign-in.",
          "When you want a failed submit to move focus to the first problem and to list all problems."
        ],
        "whenNotToUse": [
          "A single search box or an auto-saving inline edit: use the field alone.",
          "A filter bar that updates as you type: no submit, no form."
        ],
        "keyboard": [],
        "examples": [
          "<Form aria-label=\"Profile\" errorSummary onSubmit={save} validationErrors={errors}>\n  <Input label=\"Full name\" name=\"name\" isRequired />\n  <FieldSet legend=\"Contact\">\n    <Input label=\"Email\" name=\"email\" type=\"email\" isRequired />\n  </FieldSet>\n  <Button type=\"submit\" variant=\"primary\">\n    Save changes\n  </Button>\n</Form>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Any set of fields submitted together: create or edit a record, settings, sign-in.\n- When you want a failed submit to move focus to the first problem and to list all problems."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A single search box or an auto-saving inline edit: use the field alone.\n- A filter bar that updates as you type: no submit, no form."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nform (flex column, gap --ds-form-gap)\n  FormErrorSummary   (errorSummary prop, or placed by hand)\n  children           fields, FieldSets, actions\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                               | Type                                 | Default    | Notes                                                                                  |\n| ---------------------------------- | ------------------------------------ | ---------- | -------------------------------------------------------------------------------------- |\n| `validationBehavior`               | `'native' \\| 'aria'`                 | `'native'` | Form level; fields inherit it. `aria` marks fields invalid but never blocks the submit |\n| `validationErrors`                 | `Record<string, string \\| string[]>` |            | Server errors keyed by field `name`                                                    |\n| `errorSummary`                     | `boolean`                            | `false`    | Shows `FormErrorSummary` at the top                                                    |\n| `onSubmit`, `onReset`, `action`, … |                                      |            | As on a `form`                                                                         |\n| `messages`                         | `{ summaryTitle(n) }`                |            | Per-instance message overrides                                                         |\n\n`ref` points at the `<form>`. `FormErrorSummary` takes `className` and must be rendered inside a `Form`."
          },
          {
            "id": "submitting-from-outside-the-form",
            "heading": "Submitting from outside the form",
            "content": "Give the form an `id` and point a submit button at it with the `form` attribute. The browser then submits (and validates) the form natively, so the button can live anywhere in the document, such as the footer of a `Sheet`, which sits outside the body that holds the form:\n\n```tsx\n<Sheet\n  title=\"New customer\"\n  footer={\n    <Button type=\"submit\" form=\"customer-form\" variant=\"primary\">\n      Save customer\n    </Button>\n  }\n>\n  <Form id=\"customer-form\" aria-label=\"New customer\" onSubmit={save}>\n    �\n  </Form>\n</Sheet>\n```\n\nEnter inside a field submits the form as usual, and a failed native validation focuses the first invalid field (or the summary). See the `SheetForm` recipe.\n\nKeep consumer handlers (`onKeyDown`, `onFocus`, �) to what the form level needs, such as `Ctrl+Enter` to save. The form registers no React key or focus handler, so yours never replace an internal one. A handler on the form also sees keys from fields, so `event.key === 'Enter'` there is not a submit: let the browser submit instead."
          },
          {
            "id": "validation-flow",
            "heading": "Validation flow",
            "content": "1. The user presses the submit button. With native validation the browser blocks an invalid submit and fires `invalid` on each invalid field.\n2. React Aria displays the errors (`FieldError`) and focuses the **first invalid field**, whose error is read through `aria-describedby`.\n3. With `errorSummary`, focus goes to the **summary** instead: a `role=\"alert\"` box, \"Fix 2 fields before saving\", listing each invalid field as a link that focuses it.\n4. Fixing a field (on blur, when React Aria commits the edit) updates the summary; it disappears when nothing is left.\n5. Server errors: set `validationErrors` after the request; the matching fields show their message and the summary (if on) lists them and takes focus. Clear them on the next submit or edit.\n\nKeep the submit button enabled: a disabled submit gives no way to learn what is wrong."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The summary is `role=\"alert\"` with `tabindex=\"-1\"`, so it is announced and can take focus programmatically. The links use the field label (the required marker is excluded) and the field's error text.\n- Radio and checkbox groups count once, under the group's label.\n- Give the form a name (`aria-label` or `aria-labelledby`) when it should be a landmark."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- The summary title is \"Fix {n} fields before saving\" (pluralized; \"Fix 1 field before saving\"). `pt-BR` has \"Corrija {n} campos antes de salvar\".\n- Field messages say how to fix the problem (\"Enter a complete email address.\"). A static `errorMessage` text replaces only the browser's own constraint text (\"Please fill out this field\"); a server error from `validationErrors` (or a `validate` message) is shown instead of it. Use the function form when you need full control: `errorMessage={(v) => v.validationDetails.valueMissing ? 'Enter a name.' : v.validationErrors.join(' ')}`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Form aria-label=\"Profile\" errorSummary onSubmit={save} validationErrors={errors}>\n  <Input label=\"Full name\" name=\"name\" isRequired />\n  <FieldSet legend=\"Contact\">\n    <Input label=\"Email\" name=\"email\" type=\"email\" isRequired />\n  </FieldSet>\n  <Button type=\"submit\" variant=\"primary\">\n    Save changes\n  </Button>\n</Form>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff had no form-level validation; each field validated itself. Validation behavior is set once on the form, server errors are first-class, and the summary is new (GAP-003)."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "FormErrorSummary",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { FormErrorSummary } from 'loui-ds';",
      "folder": "src/components/Form",
      "sourceFile": "src/components/Form/Form.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The error summary of a `Form`: after a failed submit (or new server errors) it takes focus,\nis announced (`role=\"alert\"`), says how many fields need fixing, and lists them as links that\nmove focus to the field. It renders nothing while the form is valid. Place it inside a\n`Form`, or turn it on with the form's `errorSummary` prop.",
      "props": [
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the summary."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-danger",
        "--ds-danger-line",
        "--ds-danger-soft",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-form-gap",
        "--ds-form-summary-gap",
        "--ds-form-summary-padding",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-radius-lg",
        "--ds-space-5",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Form",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-form--default",
            "description": "A failed submit focuses the first invalid field, which reads its error.",
            "hasPlay": true
          },
          {
            "name": "WithErrorSummary",
            "id": "components-form--with-error-summary",
            "description": "The summary takes focus after a failed submit and links to each invalid field by its label.",
            "hasPlay": true
          },
          {
            "name": "ServerErrors",
            "id": "components-form--server-errors",
            "description": "Errors from the server, keyed by the field name.",
            "hasPlay": true
          },
          {
            "name": "ServerRoundTrip",
            "id": "components-form--server-round-trip",
            "description": "Server errors set by a submit handler, the way an app does it.",
            "hasPlay": true
          },
          {
            "name": "WithFieldSets",
            "id": "components-form--with-field-sets",
            "description": "FieldSet groups related fields; the form keeps one rhythm between groups and fields.",
            "hasPlay": false
          },
          {
            "name": "AriaValidation",
            "id": "components-form--aria-validation",
            "description": "With aria validation the browser never blocks the submit.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-form--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-form--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Form/Form.md",
        "summary": "A form built on React Aria `Form`: native validation by default, server errors, a consistent vertical rhythm and an optional error summary.",
        "whenToUse": [
          "Any set of fields submitted together: create or edit a record, settings, sign-in.",
          "When you want a failed submit to move focus to the first problem and to list all problems."
        ],
        "whenNotToUse": [
          "A single search box or an auto-saving inline edit: use the field alone.",
          "A filter bar that updates as you type: no submit, no form."
        ],
        "keyboard": [],
        "examples": [
          "<Form aria-label=\"Profile\" errorSummary onSubmit={save} validationErrors={errors}>\n  <Input label=\"Full name\" name=\"name\" isRequired />\n  <FieldSet legend=\"Contact\">\n    <Input label=\"Email\" name=\"email\" type=\"email\" isRequired />\n  </FieldSet>\n  <Button type=\"submit\" variant=\"primary\">\n    Save changes\n  </Button>\n</Form>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Any set of fields submitted together: create or edit a record, settings, sign-in.\n- When you want a failed submit to move focus to the first problem and to list all problems."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A single search box or an auto-saving inline edit: use the field alone.\n- A filter bar that updates as you type: no submit, no form."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nform (flex column, gap --ds-form-gap)\n  FormErrorSummary   (errorSummary prop, or placed by hand)\n  children           fields, FieldSets, actions\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                               | Type                                 | Default    | Notes                                                                                  |\n| ---------------------------------- | ------------------------------------ | ---------- | -------------------------------------------------------------------------------------- |\n| `validationBehavior`               | `'native' \\| 'aria'`                 | `'native'` | Form level; fields inherit it. `aria` marks fields invalid but never blocks the submit |\n| `validationErrors`                 | `Record<string, string \\| string[]>` |            | Server errors keyed by field `name`                                                    |\n| `errorSummary`                     | `boolean`                            | `false`    | Shows `FormErrorSummary` at the top                                                    |\n| `onSubmit`, `onReset`, `action`, … |                                      |            | As on a `form`                                                                         |\n| `messages`                         | `{ summaryTitle(n) }`                |            | Per-instance message overrides                                                         |\n\n`ref` points at the `<form>`. `FormErrorSummary` takes `className` and must be rendered inside a `Form`."
          },
          {
            "id": "submitting-from-outside-the-form",
            "heading": "Submitting from outside the form",
            "content": "Give the form an `id` and point a submit button at it with the `form` attribute. The browser then submits (and validates) the form natively, so the button can live anywhere in the document, such as the footer of a `Sheet`, which sits outside the body that holds the form:\n\n```tsx\n<Sheet\n  title=\"New customer\"\n  footer={\n    <Button type=\"submit\" form=\"customer-form\" variant=\"primary\">\n      Save customer\n    </Button>\n  }\n>\n  <Form id=\"customer-form\" aria-label=\"New customer\" onSubmit={save}>\n    �\n  </Form>\n</Sheet>\n```\n\nEnter inside a field submits the form as usual, and a failed native validation focuses the first invalid field (or the summary). See the `SheetForm` recipe.\n\nKeep consumer handlers (`onKeyDown`, `onFocus`, �) to what the form level needs, such as `Ctrl+Enter` to save. The form registers no React key or focus handler, so yours never replace an internal one. A handler on the form also sees keys from fields, so `event.key === 'Enter'` there is not a submit: let the browser submit instead."
          },
          {
            "id": "validation-flow",
            "heading": "Validation flow",
            "content": "1. The user presses the submit button. With native validation the browser blocks an invalid submit and fires `invalid` on each invalid field.\n2. React Aria displays the errors (`FieldError`) and focuses the **first invalid field**, whose error is read through `aria-describedby`.\n3. With `errorSummary`, focus goes to the **summary** instead: a `role=\"alert\"` box, \"Fix 2 fields before saving\", listing each invalid field as a link that focuses it.\n4. Fixing a field (on blur, when React Aria commits the edit) updates the summary; it disappears when nothing is left.\n5. Server errors: set `validationErrors` after the request; the matching fields show their message and the summary (if on) lists them and takes focus. Clear them on the next submit or edit.\n\nKeep the submit button enabled: a disabled submit gives no way to learn what is wrong."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The summary is `role=\"alert\"` with `tabindex=\"-1\"`, so it is announced and can take focus programmatically. The links use the field label (the required marker is excluded) and the field's error text.\n- Radio and checkbox groups count once, under the group's label.\n- Give the form a name (`aria-label` or `aria-labelledby`) when it should be a landmark."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- The summary title is \"Fix {n} fields before saving\" (pluralized; \"Fix 1 field before saving\"). `pt-BR` has \"Corrija {n} campos antes de salvar\".\n- Field messages say how to fix the problem (\"Enter a complete email address.\"). A static `errorMessage` text replaces only the browser's own constraint text (\"Please fill out this field\"); a server error from `validationErrors` (or a `validate` message) is shown instead of it. Use the function form when you need full control: `errorMessage={(v) => v.validationDetails.valueMissing ? 'Enter a name.' : v.validationErrors.join(' ')}`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Form aria-label=\"Profile\" errorSummary onSubmit={save} validationErrors={errors}>\n  <Input label=\"Full name\" name=\"name\" isRequired />\n  <FieldSet legend=\"Contact\">\n    <Input label=\"Email\" name=\"email\" type=\"email\" isRequired />\n  </FieldSet>\n  <Button type=\"submit\" variant=\"primary\">\n    Save changes\n  </Button>\n</Form>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff had no form-level validation; each field validated itself. Validation behavior is set once on the form, server errors are first-class, and the summary is new (GAP-003)."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Grid",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Grid } from 'loui-ds';",
      "folder": "src/components/Grid",
      "sourceFile": "src/components/Grid/Grid.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A CSS grid with equal columns: a fixed `columns` count, or an auto-fill grid that keeps each\nitem at least `minItemWidth` wide (the responsive choice for card grids). Gaps are space\ntoken keys.",
      "props": [
        {
          "name": "columns",
          "type": "1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12",
          "required": false,
          "description": "A fixed number of equal columns (1 to 12). Items can shrink below their content width.",
          "values": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
        },
        {
          "name": "minItemWidth",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Fills the row with as many equal columns as fit, each at least this wide: `sm` 160px,\n`md` 240px, `lg` 320px (`--ds-grid-min-item-*`). Collapses to one column on narrow screens.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "gap",
          "type": "1 | 2 | 3 | 4 | 5 | 6 | 8 | 12 | 16 | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '12' | '16'",
          "required": false,
          "default": "4",
          "description": "Space between rows and columns, as a key of the space scale (a number or its string form). Default `4`.",
          "values": ["1", "2", "3", "4", "5", "6", "8", "12", "16"]
        },
        {
          "name": "as",
          "type": "'div' | 'article' | 'aside' | 'footer' | 'form' | 'header' | 'li' | 'main' | 'nav' | 'ol' | 'section' | 'ul'",
          "required": false,
          "default": "'div'",
          "description": "Element to render, `div` by default. Use `ul` or `ol` for a list of `li` children: the\nlist reset is applied and `role=\"list\"` keeps the list semantics in Safari.",
          "values": [
            "div",
            "article",
            "aside",
            "footer",
            "form",
            "header",
            "li",
            "main",
            "nav",
            "ol",
            "section",
            "ul"
          ]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-grid-columns",
        "--ds-grid-gap",
        "--ds-grid-min-item-lg",
        "--ds-grid-min-item-md",
        "--ds-grid-min-item-sm",
        "--ds-space-4"
      ],
      "stories": {
        "title": "Layout/Grid",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-grid--default",
            "hasPlay": false
          },
          {
            "name": "TwoColumns",
            "id": "layout-grid--two-columns",
            "hasPlay": false
          },
          {
            "name": "AutoFillMedium",
            "id": "layout-grid--auto-fill-medium",
            "description": "As many columns as fit, each at least 240px wide. Resize the canvas to see it reflow.",
            "hasPlay": false
          },
          {
            "name": "AutoFillSmall",
            "id": "layout-grid--auto-fill-small",
            "hasPlay": false
          },
          {
            "name": "AutoFillLarge",
            "id": "layout-grid--auto-fill-large",
            "hasPlay": false
          },
          {
            "name": "NarrowContainer",
            "id": "layout-grid--narrow-container",
            "description": "In a container narrower than the minimum item width the grid is one column and never overflows.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "layout-grid--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-grid--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Grid/Grid.md",
        "summary": "A grid of equal columns: either a fixed number, or as many as fit with a minimum item width.",
        "whenToUse": [
          "A page of cards or stats: `minItemWidth` reflows from many columns to one without media queries.",
          "A form laid out in two or three equal columns: `columns`."
        ],
        "whenNotToUse": [
          "For one column of content: use `Stack`.",
          "For a row that wraps and does not need aligned columns: use `Inline`.",
          "For tabular data: use `DataTable`."
        ],
        "keyboard": [],
        "examples": [
          "<Grid minItemWidth=\"md\" gap={4}>\n  <StatCard … />\n  <StatCard … />\n  <StatCard … />\n</Grid>\n\n<Grid columns={2} gap={4}>\n  <Input label=\"First name\" />\n  <Input label=\"Last name\" />\n</Grid>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A page of cards or stats: `minItemWidth` reflows from many columns to one without media queries.\n- A form laid out in two or three equal columns: `columns`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For one column of content: use `Stack`.\n- For a row that wraps and does not need aligned columns: use `Inline`.\n- For tabular data: use `DataTable`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (grid, gap)\n  children (one per cell)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                                                                          | Default | Notes                                                                    |\n| -------------- | ----------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |\n| `columns`      | `1` to `12`                                                                   |         | Fixed equal columns. Pass this **or** `minItemWidth`                     |\n| `minItemWidth` | `'sm' \\| 'md' \\| 'lg'`                                                        |         | Auto-fill; items are at least 160 / 240 / 320px (`--ds-grid-min-item-*`) |\n| `gap`          | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6 \\| 8 \\| 12 \\| 16` or the same as a string (`\"4\"`) | `4`     | Space token key, used for rows and columns                               |\n| `as`           | `'div' \\| 'ul' \\| 'ol' …`                                                     | `'div'` | Lists get the reset and `role=\"list\"`                                    |\n\nThe types make `columns` and `minItemWidth` exclusive. Item widths are tokens, not pixels, like the gap."
          },
          {
            "id": "responsive-behavior",
            "heading": "Responsive behavior",
            "content": "- `minItemWidth` uses `repeat(auto-fill, minmax(min(<width>, 100%), 1fr))`: in a container narrower than the minimum, the grid is one column and never overflows.\n- `columns` does not collapse by itself. For a layout that changes by screen size, prefer `minItemWidth`, or switch `columns` from the app with a media query or `useMediaQuery`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- No semantics of its own; use `as=\"ul\"` when the cells are list items.\n- Source order is visual order for the row-first reading direction. Do not rely on CSS placement to reorder content.\n- Cells can shrink (`minmax(0, 1fr)`), so long words wrap instead of widening a column."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Grid minItemWidth=\"md\" gap={4}>\n  <StatCard … />\n  <StatCard … />\n  <StatCard … />\n</Grid>\n\n<Grid columns={2} gap={4}>\n  <Input label=\"First name\" />\n  <Input label=\"Last name\" />\n</Grid>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff hard-coded `grid-template-columns: repeat(N, 1fr)` per screen with raw pixel gaps and a 1180px minimum width. Here the minimum item width is a token and nothing needs a minimum viewport."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Stack",
            "reason": "For one column of content: use `Stack`."
          },
          {
            "name": "Inline",
            "reason": "For a row that wraps and does not need aligned columns: use `Inline`."
          },
          {
            "name": "DataTable",
            "reason": "For tabular data: use `DataTable`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Heading",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Heading } from 'loui-ds';",
      "folder": "src/components/Heading",
      "sourceFile": "src/components/Heading/Heading.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A heading whose semantic level and visual size are separate choices, so the document outline\nstays correct while the design picks the size.",
      "props": [
        {
          "name": "level",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2",
          "description": "Semantic level, `h1` to `h6`. It sets the page outline for assistive technology and has\nnothing to do with how big the text looks: keep levels sequential and use one `h1` per\npage. Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "size",
          "type": "'title' | 'display' | 'heading'",
          "required": false,
          "description": "Visual size, independent of `level`: `display` (34px, page hero), `title` (22px) or\n`heading` (17px). Default: `title` for level 1, `heading` for the others.",
          "values": ["title", "display", "heading"]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["title", "display", "heading"]
      },
      "cssVariables": [
        "--ds-font-sans",
        "--ds-fs-display",
        "--ds-fs-heading",
        "--ds-fs-title",
        "--ds-lh-display",
        "--ds-lh-heading",
        "--ds-lh-title",
        "--ds-text",
        "--ds-tracking-display",
        "--ds-tracking-title",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Typography/Heading",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "typography-heading--default",
            "hasPlay": true
          },
          {
            "name": "Display",
            "id": "typography-heading--display",
            "hasPlay": false
          },
          {
            "name": "Title",
            "id": "typography-heading--title",
            "hasPlay": false
          },
          {
            "name": "HeadingSize",
            "id": "typography-heading--heading-size",
            "hasPlay": false
          },
          {
            "name": "LevelIndependentOfSize",
            "id": "typography-heading--level-independent-of-size",
            "description": "The level is the outline, the size is the look: an h2 can be small and an h3 can be big.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "typography-heading--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "typography-heading--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Heading/Heading.md",
        "summary": "A heading whose semantic level (`h1` to `h6`) and visual size are separate choices.",
        "whenToUse": [
          "Any heading in page content. The level builds the outline; the size is chosen for the design."
        ],
        "whenNotToUse": [
          "For the page title inside the header: `TopBar` renders its own heading.",
          "For a card caption: `Card` renders its title as a mono caption heading.",
          "For bold text that is not a heading: use `Text`."
        ],
        "keyboard": [],
        "examples": [
          "<Heading level={1} size=\"display\">Orders</Heading>\n<Heading level={2}>Recent</Heading>\n<Heading level={3} size=\"title\">Details</Heading>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Any heading in page content. The level builds the outline; the size is chosen for the design."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For the page title inside the header: `TopBar` renders its own heading.\n- For a card caption: `Card` renders its title as a mono caption heading.\n- For bold text that is not a heading: use `Text`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nh{level}  (semibold, size from the type scale)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop    | Type                                | Default                                  | Notes                                                     |\n| ------- | ----------------------------------- | ---------------------------------------- | --------------------------------------------------------- |\n| `level` | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6`        | `2`                                      | Semantic level only                                       |\n| `size`  | `'display' \\| 'title' \\| 'heading'` | `title` for level 1, `heading` otherwise | 34/40 · 22/30 · 17/24 px; display and title have tracking |\n\nOther heading attributes and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Keep levels sequential and use one `h1` per page. Pick the level for the outline, then the size for the look: an `h1` can use `size=\"heading\"`, an `h4` can use `size=\"display\"`.\n- Headings use `text-wrap: balance` and break long words, so they wrap at 320px.\n- Color is `--ds-text` (≥ 4.5:1 on every base surface)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Sentence case. Short and specific (\"Recent orders\"), no trailing punctuation."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Heading level={1} size=\"display\">Orders</Heading>\n<Heading level={2}>Recent</Heading>\n<Heading level={3} size=\"title\">Details</Heading>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff styled headings with inline font sizes tied to the element. Level and size are independent and use the type tokens."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "TopBar",
            "reason": "For the page title inside the header: `TopBar` renders its own heading."
          },
          {
            "name": "Card",
            "reason": "For a card caption: `Card` renders its title as a mono caption heading."
          },
          {
            "name": "Text",
            "reason": "For bold text that is not a heading: use `Text`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Icon",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Icon } from 'loui-ds';",
      "folder": "src/components/Icon",
      "sourceFile": "src/components/Icon/Icon.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An inline SVG icon (Lucide, bundled) that inherits the text color and draws a 1.5px stroke.\nDecorative by default; pass `label` to make it a meaningful image.",
      "props": [
        {
          "name": "icon",
          "type": "LucideIcon",
          "required": true,
          "description": "The icon to draw, one of the curated set exported from this package (for example `SearchIcon`)."
        },
        {
          "name": "size",
          "type": "'xs' | 'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Size token: `xs` 12px, `sm` 14px, `md` 18px (default), `lg` 20px.",
          "values": ["xs", "sm", "md", "lg"]
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "Accessible name. Omit it when the icon only decorates text that already names the\ncontrol (the icon is then hidden from assistive technology). Set it only when the icon\ncarries meaning on its own, outside a control that has its own name."
        },
        {
          "name": "directional",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Mirror the icon horizontally in right-to-left languages. Set it for icons that point in a\nreading direction (chevrons, arrows); leave it off for symmetric or universal icons."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 50
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "SVGAttributes",
          "domGeneric": true,
          "count": 262
        }
      ],
      "variants": {
        "size": ["xs", "sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-icon-lg",
        "--ds-icon-md",
        "--ds-icon-sm",
        "--ds-icon-stroke-width",
        "--ds-icon-xs"
      ],
      "stories": {
        "title": "Components/Icon",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-icon--default",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-icon--sizes",
            "hasPlay": false
          },
          {
            "name": "WithLabel",
            "id": "components-icon--with-label",
            "description": "The icon carries meaning on its own, so it is an image with an accessible name.",
            "hasPlay": true
          },
          {
            "name": "InheritsColor",
            "id": "components-icon--inherits-color",
            "description": "Inherits the text color. Color never carries meaning alone: the text says what happened.",
            "hasPlay": false
          },
          {
            "name": "DirectionalRtl",
            "id": "components-icon--directional-rtl",
            "description": "Directional icons mirror in right-to-left languages; the second one does not.",
            "hasPlay": false
          },
          {
            "name": "Gallery",
            "id": "components-icon--gallery",
            "description": "Every icon of the curated set.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Icon/Icon.md",
        "summary": "An inline SVG icon from the curated Lucide set. It inherits the text color, draws a 1.5px stroke and is hidden from assistive technology unless you give it a `label`.\n\n- Replaces: the runtime `unpkg.com/lucide-static` CSS-mask loading (ICON-001), text glyphs used as icons (ICON-002) and the ad hoc icon rules (ICON-003).\n- Built on: `lucide-react`, bundled and tree-shakeable. No network access, no CDN.",
        "whenToUse": [
          "Next to a text label to reinforce it (button leading slot, menu item, alert, input affordance).",
          "Inside `IconButton`, which supplies the accessible name.",
          "On its own only when it carries meaning that no nearby text states. Then set `label`."
        ],
        "whenNotToUse": [
          "As the only carrier of meaning or status. Color and shape never replace text.",
          "For filled, colored or multi-color icons, emoji or Unicode glyphs. Use the set below.",
          "For a loading indicator that moves: use `Spinner`.",
          "For an icon that is not in the set. Add it to `icons.ts` with a documented use first."
        ],
        "keyboard": [],
        "examples": [
          "import { Button, CircleCheckIcon, ChevronRightIcon, Icon, PlusIcon } from 'loui-ds';\n\n<Button leading={<Icon icon={PlusIcon} size=\"sm\" />}>Create order</Button>;\n\n<Icon icon={CircleCheckIcon} label=\"Paid\" />;\n\n<Icon icon={ChevronRightIcon} directional />;"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Next to a text label to reinforce it (button leading slot, menu item, alert, input affordance).\n- Inside `IconButton`, which supplies the accessible name.\n- On its own only when it carries meaning that no nearby text states. Then set `label`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- As the only carrier of meaning or status. Color and shape never replace text.\n- For filled, colored or multi-color icons, emoji or Unicode glyphs. Use the set below.\n- For a loading indicator that moves: use `Spinner`.\n- For an icon that is not in the set. Add it to `icons.ts` with a documented use first."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "A single `<svg>` (24x24 viewBox) with a `currentColor` stroke and no fill. Size and stroke come from tokens: `--ds-icon-xs|sm|md|lg` and `--ds-icon-stroke-width`."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop          | Type                           | Default | Notes                                                                      |\n| ------------- | ------------------------------ | ------- | -------------------------------------------------------------------------- |\n| `icon`        | `IconComponent` (required)     | none    | One of the exports of the set, such as `SearchIcon`.                       |\n| `size`        | `'xs' \\| 'sm' \\| 'md' \\| 'lg'` | `'md'`  | 12, 14, 18, 20 px. Inside buttons use `sm`.                                |\n| `label`       | `string`                       | none    | Makes the icon `role=\"img\"` with this name. Without it the icon is hidden. |\n| `directional` | `boolean`                      | `false` | Mirrors the icon in RTL. Set it for chevrons and arrows.                   |\n| `className`   | `string`                       | none    | Merged with the component class.                                           |\n\nOther SVG attributes and `ref` pass through to the `<svg>`."
          },
          {
            "id": "the-curated-set",
            "heading": "The curated set",
            "content": "Import icons from the package, not from `lucide-react`: `import { Icon, SearchIcon } from 'loui-ds'`.\n\n`iconSet` maps the kebab-case names below to the components and `iconNames` documents the use of each. Both are the source of the allow-list in the manifest (AI-002).\n\narchive, arrow-down, arrow-left, arrow-right, arrow-up, bell, bot, calendar, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-up-down, circle-alert, circle-check, circle-x, copy, ellipsis, external-link, filter, info, link, loader, menu, message, minus, pencil, plus, printer, refresh, search, sparkles, star, thumbs-down, thumbs-up, trash, triangle-alert, users, x.\n\nGeneric actions added by GAP-028: `trash` (delete, always behind a confirmation), `pencil` (edit), `printer`, `archive`, `bell`, `link`, `filter`, `users`, `bot` and `sparkles`. Sort is already covered by `arrow-up`, `arrow-down` and `chevrons-up-down`. Domain icons (truck, wallet, cart, WhatsApp) belong to the consuming product, not to this set; a product keeps its own icons next to the core set and passes them to `Icon` (any Lucide-compatible component works)."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Decorative by default: `aria-hidden=\"true\"` and `focusable=\"false\"`.\n- With `label`: `role=\"img\"` and `aria-label`. Use short, specific names (\"Saved\", not \"check icon\").\n- An icon inside a control never names the control. Put the name on the control (`IconButton label`).\n- Forced colors: the stroke is `currentColor`, so it follows the system text color.\n- Contrast: icons that convey meaning need 3:1 against their background (WCAG 1.4.11).\n- Reduced motion: icons do not animate."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are sentence case and describe the meaning (\"Saved\"), never the picture (\"Green check\").\n- No emoji or text glyphs as icons (AGENTS.md section 7)."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nimport { Button, CircleCheckIcon, ChevronRightIcon, Icon, PlusIcon } from 'loui-ds';\n\n<Button leading={<Icon icon={PlusIcon} size=\"sm\" />}>Create order</Button>;\n\n<Icon icon={CircleCheckIcon} label=\"Paid\" />;\n\n<Icon icon={ChevronRightIcon} directional />;\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Bundled SVG instead of `unpkg.com/lucide-static@0.400.0` loaded through CSS `mask` at runtime.\n- Decorative by default; `label` is the only way to expose an icon.\n- Sizes are the `--ds-icon-*` tokens; the stroke is a token (1.5px, non-scaling) instead of the Lucide default.\n- A curated, documented set replaces free-form icon names."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Spinner",
            "reason": "For a loading indicator that moves: use `Spinner`."
          }
        ],
        "uses": [],
        "usedBy": [
          "Accordion",
          "AddCard",
          "AgentCard",
          "Alert",
          "ApprovalCard",
          "Banner",
          "Breadcrumb",
          "Checkbox",
          "ColorOption",
          "Combobox",
          "CommandPalette",
          "DataTable",
          "DatePicker",
          "IconButton",
          "Link",
          "MatrixGrid",
          "Menu",
          "MenuItem",
          "MenuSection",
          "MenuSeparator",
          "MoreActionsMenu",
          "NavItem",
          "Rating",
          "ResponseFeedback",
          "SearchInput",
          "Select",
          "StatCard",
          "StatusSteps",
          "Stepper",
          "SubMenu",
          "SuggestionChip",
          "Tag",
          "TagGroup",
          "Timeline",
          "Toggle",
          "ToolCallStep"
        ]
      }
    },
    {
      "name": "IconButton",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { IconButton } from 'loui-ds';",
      "folder": "src/components/IconButton",
      "sourceFile": "src/components/IconButton/IconButton.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A square button with a single icon and a required accessible name.\n\nThe `label` becomes `aria-label`; there is no `title` (it would duplicate the name and is\nnot keyboard accessible). Set `tooltip` to show the label in a `Tooltip` on hover and focus.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "required": true,
          "description": "Accessible name, required. Say what the button does, as a verb plus object when there is\none (\"Add order\", \"Close dialog\"), not what it looks like (\"Plus\")."
        },
        {
          "name": "icon",
          "type": "LucideIcon",
          "required": true,
          "description": "The icon to draw: one of the curated set (for example `PlusIcon`). Decorative inside the button."
        },
        {
          "name": "variant",
          "type": "'secondary' | 'ghost'",
          "required": false,
          "default": "'secondary'",
          "description": "Visual style: `secondary` (default, bordered) or `ghost` (no border at rest).",
          "values": ["secondary", "ghost"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Square size token: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "tooltip",
          "type": "boolean | IconButtonTooltipOptions",
          "required": false,
          "description": "Shows `label` in a `Tooltip` on hover and keyboard focus. Pass `true`, or an object to add a\nshortcut or change the side. Off by default. The `label` stays the accessible name; the\ntooltip only repeats it visually for sighted mouse users."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLButtonElement>",
          "required": false,
          "description": "Ref to the underlying `<button>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaBaseButtonProps",
          "domGeneric": false,
          "count": 16,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-current",
              "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"",
              "required": false
            },
            {
              "name": "aria-disabled",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-expanded",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "aria-pressed",
              "type": "boolean | \"true\" | \"false\" | \"mixed\"",
              "required": false
            },
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "formAction",
              "type": "string | ((formData: FormData) => void | Promise<void>)",
              "required": false
            },
            {
              "name": "formEncType",
              "type": "string",
              "required": false
            },
            {
              "name": "formMethod",
              "type": "string",
              "required": false
            },
            {
              "name": "formNoValidate",
              "type": "boolean",
              "required": false
            },
            {
              "name": "formTarget",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            },
            {
              "name": "preventFocusOnPress",
              "type": "boolean",
              "required": false
            },
            {
              "name": "type",
              "type": "'button' | 'submit' | 'reset'",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"button\", ButtonRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {
        "variant": ["secondary", "ghost"],
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2"
      ],
      "stories": {
        "title": "Components/IconButton",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-icon-button--default",
            "hasPlay": false
          },
          {
            "name": "Ghost",
            "id": "components-icon-button--ghost",
            "hasPlay": false
          },
          {
            "name": "Variants",
            "id": "components-icon-button--variants",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-icon-button--sizes",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-icon-button--disabled",
            "hasPlay": true
          },
          {
            "name": "Toolbar",
            "id": "components-icon-button--toolbar",
            "description": "Typical toolbar: every button is named, none shows a visible label.",
            "hasPlay": false
          },
          {
            "name": "AccessibleName",
            "id": "components-icon-button--accessible-name",
            "description": "The name is the `label`, there is no `title` attribute.",
            "hasPlay": true
          },
          {
            "name": "KeyboardActivation",
            "id": "components-icon-button--keyboard-activation",
            "description": "Keyboard: Tab focuses, Enter and Space press.",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-icon-button--focused",
            "hasPlay": true
          },
          {
            "name": "Hovered",
            "id": "components-icon-button--hovered",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/IconButton/IconButton.md",
        "summary": "A square button that holds one icon and has a required accessible name. Built on React Aria's `Button`.\n\n- Handoff source: `components/forms/IconButton` (CMP-FRM-02).",
        "whenToUse": [
          "A compact action whose meaning is clear from context and icon: close, add, more actions, search.",
          "Toolbars and card headers where a text button would not fit."
        ],
        "whenNotToUse": [
          "When there is room for a text label: use `Button` with a `leading` icon. A visible word is clearer than any icon.",
          "For navigation: use `Link`.",
          "For an action whose icon is ambiguous: write the label out in a `Button`."
        ],
        "keyboard": [
          "The name is `aria-label` only. The handoff set `title` too, which duplicated the name and was not available to keyboard or touch users. Set `tooltip` to show the label (and an optional shortcut) in a `Tooltip` on hover **and** focus, dismissible with `Esc`.",
          "Keys: `Tab` focuses, `Enter` and `Space` press."
        ],
        "examples": [
          "<IconButton label=\"Add order\" icon={PlusIcon} onPress={addOrder} />\n\n<IconButton variant=\"ghost\" size=\"sm\" label=\"More actions\" icon={EllipsisIcon} />\n\n<div role=\"toolbar\" aria-label=\"Order actions\">\n  <IconButton variant=\"ghost\" label=\"Search orders\" icon={SearchIcon} />\n  <IconButton variant=\"ghost\" label=\"Close panel\" icon={XIcon} />\n</div>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A compact action whose meaning is clear from context and icon: close, add, more actions, search.\n- Toolbars and card headers where a text button would not fit."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- When there is room for a text label: use `Button` with a `leading` icon. A visible word is clearer than any icon.\n- For navigation: use `Link`.\n- For an action whose icon is ambiguous: write the label out in a `Button`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "One native `<button>` containing a decorative `Icon`. No visible text, no `title`."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                       | Default                    | Notes                                                              |\n| ------------ | -------------------------- | -------------------------- | ------------------------------------------------------------------ |\n| `label`      | `string` (required)        | none                       | Accessible name, set as `aria-label`. Omitting it is a type error. |\n| `icon`       | `IconComponent` (required) | none                       | One of the curated set, such as `PlusIcon`.                        |\n| `variant`    | `'secondary' \\| 'ghost'`   | `'secondary'`              | Bordered, or borderless at rest.                                   |\n| `size`       | `'sm' \\| 'md' \\| 'lg'`     | `'md'`                     | 32, 40, 48 px square (`--ds-control-h-*`); icon 14, 18, 20 px.     |\n| `isDisabled` | `boolean`                  | `false`                    | Native `disabled`, 35% opacity.                                    |\n| `onPress`    | `(e: PressEvent) => void`  | none                       | React Aria press event.                                            |\n| `tooltip`    | `boolean                   | { shortcut?, placement? }` | `false`                                                            | Shows `label` in a `Tooltip` on hover and focus. |\n| `className`  | `string`                   | none                       | Merged with the component class.                                   |\n\nOther React Aria `Button` props and `ref` pass through. `children`, `style`, `isPending`, `aria-label` and `aria-labelledby` are not accepted: the label is the single source of the name."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `label` is required by the type (`label: string`), which enforces AGENTS.md section 8.\n- The name is `aria-label` only. The handoff set `title` too, which duplicated the name and was not available to keyboard or touch users. Set `tooltip` to show the label (and an optional shortcut) in a `Tooltip` on hover **and** focus, dismissible with `Esc`.\n- The icon is `aria-hidden` and `focusable=\"false\"`; the button is never named by its glyph.\n- Target size: 32 / 40 / 48 px, above the 24px minimum of WCAG 2.5.8 (`--ds-target-min` is also enforced as `min-inline-size` and `min-block-size`).\n- Keys: `Tab` focuses, `Enter` and `Space` press.\n- Focus: accent border plus 3px ring on keyboard focus; forced-colors mode repaints the transparent outline.\n- Contrast: the icon uses `--ds-text` (secondary) or `--ds-text-2` (ghost) on the surface, both above 4.5:1 and well above the 3:1 needed for graphics.\n- Reduced motion: only a 150ms background transition, neutralized by the global rule."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- `label`: verb plus object in sentence case, without the word \"button\" or \"icon\": \"Add order\", \"Close dialog\", \"More actions\".\n- Translate it like any other text; `IconButton` has no built-in strings."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<IconButton label=\"Add order\" icon={PlusIcon} onPress={addOrder} />\n\n<IconButton variant=\"ghost\" size=\"sm\" label=\"More actions\" icon={EllipsisIcon} />\n\n<div role=\"toolbar\" aria-label=\"Order actions\">\n  <IconButton variant=\"ghost\" label=\"Search orders\" icon={SearchIcon} />\n  <IconButton variant=\"ghost\" label=\"Close panel\" icon={XIcon} />\n</div>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `label` is enforced by types; `title` is removed.\n- Children are no longer text glyphs (`+`, `...`): pass an `icon` from the set.\n- Hover is CSS, not JS state; sizes use control tokens; `style` is gone.\n- `disabled` is `isDisabled`, and `onPress` replaces `onClick`.\n- Tooltip support is deferred to the `Tooltip` component."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Button",
            "reason": "When there is room for a text label: use `Button` with a `leading` icon. A visible word is clearer than any icon."
          },
          {
            "name": "Link",
            "reason": "For navigation: use `Link`."
          }
        ],
        "uses": ["Icon", "Tooltip"],
        "usedBy": [
          "Alert",
          "AppShell",
          "AppShellNavToggle",
          "Banner",
          "Calendar",
          "Drawer",
          "FileUpload",
          "LedgerList",
          "Menu",
          "MenuItem",
          "MenuSection",
          "MenuSeparator",
          "MoreActionsMenu",
          "NumberField",
          "Pagination",
          "Sheet",
          "SubMenu",
          "ToastRegion"
        ]
      }
    },
    {
      "name": "Inline",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Inline } from 'loui-ds';",
      "folder": "src/components/Inline",
      "sourceFile": "src/components/Inline/Inline.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Lays its children out in a horizontal row with a consistent gap. It wraps by default.\nDirection follows the writing mode, so it mirrors in right-to-left languages.",
      "props": [
        {
          "name": "gap",
          "type": "1 | 2 | 3 | 4 | 5 | 6 | 8 | 12 | 16 | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '12' | '16'",
          "required": false,
          "default": "2",
          "description": "Space between children, as a key of the space scale, a number or its string form (`2` = `--ds-space-2` = 8px). Default `2`.",
          "values": ["1", "2", "3", "4", "5", "6", "8", "12", "16"]
        },
        {
          "name": "align",
          "type": "'center' | 'start' | 'end' | 'baseline' | 'stretch'",
          "required": false,
          "default": "'center'",
          "description": "Cross-axis (vertical) alignment. Default `center`.",
          "values": ["center", "start", "end", "baseline", "stretch"]
        },
        {
          "name": "justify",
          "type": "'center' | 'start' | 'end' | 'between'",
          "required": false,
          "default": "'start'",
          "description": "Main-axis (horizontal) distribution. `between` pushes the first and last child to the edges. Default `start`.",
          "values": ["center", "start", "end", "between"]
        },
        {
          "name": "wrap",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Whether children wrap onto the next line when they do not fit. Default `true`, so a row never overflows a narrow screen."
        },
        {
          "name": "as",
          "type": "'div' | 'article' | 'aside' | 'footer' | 'form' | 'header' | 'li' | 'main' | 'nav' | 'ol' | 'section' | 'ul'",
          "required": false,
          "default": "'div'",
          "description": "Element to render, `div` by default. Use `ul` or `ol` for a list of `li` children: the\nlist reset is applied and `role=\"list\"` keeps the list semantics in Safari.",
          "values": [
            "div",
            "article",
            "aside",
            "footer",
            "form",
            "header",
            "li",
            "main",
            "nav",
            "ol",
            "section",
            "ul"
          ]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": ["--ds-inline-gap", "--ds-space-2"],
      "stories": {
        "title": "Layout/Inline",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-inline--default",
            "hasPlay": false
          },
          {
            "name": "LargeGap",
            "id": "layout-inline--large-gap",
            "hasPlay": false
          },
          {
            "name": "JustifyBetween",
            "id": "layout-inline--justify-between",
            "hasPlay": false
          },
          {
            "name": "JustifyEnd",
            "id": "layout-inline--justify-end",
            "hasPlay": false
          },
          {
            "name": "AlignBaseline",
            "id": "layout-inline--align-baseline",
            "hasPlay": false
          },
          {
            "name": "Wrapping",
            "id": "layout-inline--wrapping",
            "description": "Wraps onto a second line in a narrow container: a row never overflows a phone.",
            "hasPlay": false
          },
          {
            "name": "NoWrap",
            "id": "layout-inline--no-wrap",
            "description": "With wrap off, children stay on one line and the row scrolls or clips: use it only for short rows.",
            "hasPlay": false
          },
          {
            "name": "AsList",
            "id": "layout-inline--as-list",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "layout-inline--right-to-left",
            "description": "The row mirrors: the first child sits at the right in right-to-left text.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Inline/Inline.md",
        "summary": "A horizontal row of children with one consistent gap. It wraps by default, so a row never overflows a narrow screen.",
        "whenToUse": [
          "Buttons in a footer, chips, a number and its caption, a toolbar of controls.",
          "Anything that should flow left to right (right to left in RTL) and wrap when out of room."
        ],
        "whenNotToUse": [
          "For a column: use `Stack`.",
          "For equal columns that must line up across rows: use `Grid`.",
          "For a form row where fields share the width: use `Grid` with `columns`."
        ],
        "keyboard": [],
        "examples": [
          "<Inline justify=\"end\">\n  <Button>Cancel</Button>\n  <Button variant=\"primary\">Save changes</Button>\n</Inline>\n\n<Inline gap={3} align=\"baseline\">\n  <Text size=\"body\" mono>1,240</Text>\n  <Text tone=\"secondary\">orders this month</Text>\n</Inline>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Buttons in a footer, chips, a number and its caption, a toolbar of controls.\n- Anything that should flow left to right (right to left in RTL) and wrap when out of room."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a column: use `Stack`.\n- For equal columns that must line up across rows: use `Grid`.\n- For a form row where fields share the width: use `Grid` with `columns`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (flex row, wrap, gap)\n  children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop      | Type                                                                          | Default    | Notes                                                            |\n| --------- | ----------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------- |\n| `gap`     | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6 \\| 8 \\| 12 \\| 16` or the same as a string (`\"4\"`) | `2`        | Space token key (8px by default)                                 |\n| `align`   | `'start' \\| 'center' \\| 'end' \\| 'baseline' \\| 'stretch'`                     | `'center'` | Vertical alignment. Use `baseline` for a number beside its label |\n| `justify` | `'start' \\| 'center' \\| 'end' \\| 'between'`                                   | `'start'`  | `between` pushes the first and last child to the edges           |\n| `wrap`    | `boolean`                                                                     | `true`     | Turn off only for short, fixed rows                              |\n| `as`      | `'div' \\| 'section' \\| 'ul' \\| 'ol' …`                                        | `'div'`    | Lists get the reset and `role=\"list\"`                            |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- No semantics of its own: choose `as` for meaning.\n- Source order is visual order, so keyboard and reading order match. Do not reorder children with CSS.\n- Wrapping keeps content reachable at 320px and 200% zoom (WCAG 1.4.10 reflow).\n- Direction follows the writing mode, so the row mirrors in RTL."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Put the primary action last in a footer row (`justify=\"end\"`), after the secondary action."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Inline justify=\"end\">\n  <Button>Cancel</Button>\n  <Button variant=\"primary\">Save changes</Button>\n</Inline>\n\n<Inline gap={3} align=\"baseline\">\n  <Text size=\"body\" mono>1,240</Text>\n  <Text tone=\"secondary\">orders this month</Text>\n</Inline>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Replaces ad hoc inline-flex styles with a typed component using space tokens."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Stack",
            "reason": "For a column: use `Stack`."
          },
          {
            "name": "Grid",
            "reason": "For equal columns that must line up across rows: use `Grid`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "InlineConfirm",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { InlineConfirm } from 'loui-ds';",
      "folder": "src/components/InlineConfirm",
      "sourceFile": "src/components/InlineConfirm/InlineConfirm.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A two-step confirmation without a dialog: the trigger button turns into a question with\nCancel and a confirm button, in place. For a destructive action in a dense row or toolbar\nwhere a `ConfirmDialog` would be heavy.\n\nFocus moves to **Cancel** when the prompt opens (configurable), so Enter right after the\nclick cannot confirm. `Escape` cancels, but only while focus is inside the prompt (it is not a\nwindow listener, so it does not close a surrounding `Dialog` first). When the prompt closes,\nfocus returns to the trigger.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "required": true,
          "description": "Label of the trigger button: verb plus object (\"Delete order\")."
        },
        {
          "name": "onConfirm",
          "type": "() => void",
          "required": true,
          "description": "Called when the user confirms. The prompt closes afterwards."
        },
        {
          "name": "prompt",
          "type": "string",
          "required": false,
          "description": "The question, ideally with the consequence (\"Remove this line? This can't be undone.\").\nDefault from messages: \"Are you sure?\"."
        },
        {
          "name": "confirmLabel",
          "type": "string",
          "required": false,
          "description": "Label of the confirming button. Defaults to `label`, which already names the action."
        },
        {
          "name": "tone",
          "type": "'danger' | 'neutral'",
          "required": false,
          "default": "'danger'",
          "description": "`danger` (default): destructive styling. `neutral`: secondary trigger, primary confirm.",
          "values": ["danger", "neutral"]
        },
        {
          "name": "initialFocus",
          "type": "'cancel' | 'confirm'",
          "required": false,
          "default": "'cancel'",
          "description": "Which button gets the focus when the prompt opens. `cancel` (default) is the safe choice\nand the product principle for destructive actions.",
          "values": ["cancel", "confirm"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Button size, `md` by default.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled state: whether the prompt is showing. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the prompt opens or closes."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the trigger."
        },
        {
          "name": "messages",
          "type": "Partial<{ cancel: string; prompt: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLSpanElement>",
          "required": false,
          "description": "Ref to the root element."
        }
      ],
      "inheritedProps": [],
      "variants": {
        "size": ["sm", "md", "lg"],
        "tone": ["danger", "neutral"]
      },
      "messages": {
        "providerKey": "InlineConfirm",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "cancel",
            "kind": "string",
            "default": "Cancel",
            "description": "Label of the button that backs out of the confirmation."
          },
          {
            "key": "prompt",
            "kind": "string",
            "default": "Are you sure?",
            "description": "Default question shown next to the buttons. State the consequence when you can."
          }
        ]
      },
      "cssVariables": [
        "--ds-font-sans",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-space-2",
        "--ds-text-2"
      ],
      "stories": {
        "title": "Components/InlineConfirm",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-inline-confirm--default",
            "description": "Enter on the trigger opens the prompt with focus on Cancel; Enter again cancels, it never confirms.",
            "hasPlay": true
          },
          {
            "name": "Asking",
            "id": "components-inline-confirm--asking",
            "hasPlay": false
          },
          {
            "name": "EscapeCancels",
            "id": "components-inline-confirm--escape-cancels",
            "hasPlay": true
          },
          {
            "name": "ConfirmByKeyboard",
            "id": "components-inline-confirm--confirm-by-keyboard",
            "hasPlay": true
          },
          {
            "name": "FocusOnConfirm",
            "id": "components-inline-confirm--focus-on-confirm",
            "hasPlay": false
          },
          {
            "name": "Neutral",
            "id": "components-inline-confirm--neutral",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "components-inline-confirm--small",
            "hasPlay": false
          },
          {
            "name": "DefaultPrompt",
            "id": "components-inline-confirm--default-prompt",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-inline-confirm--disabled",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/InlineConfirm/InlineConfirm.md",
        "summary": "A two-step confirmation in place: a button that turns into a question with Cancel and a confirm button. No dialog.\n\n- Handoff source: `components/feedback/InlineConfirm` (CMP-FBK-07).",
        "whenToUse": [
          "A destructive action in a dense row, toolbar or card where a `ConfirmDialog` would be heavy (\"Delete line\", \"Remove member\")."
        ],
        "whenNotToUse": [
          "Deleting something important or large (a customer, an order): use `ConfirmDialog`, which can show the consequence in full.",
          "Reversible actions: do them and offer **Undo** in a `Toast`.",
          "Anything that needs more input: use `Dialog`."
        ],
        "keyboard": [
          "Cancel and confirm are normal buttons in the tab order: `Tab` from Cancel reaches confirm."
        ],
        "examples": [
          "<InlineConfirm\n  label=\"Delete line\"\n  prompt=\"Remove this line? This can't be undone.\"\n  onConfirm={() => removeLine(line.id)}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A destructive action in a dense row, toolbar or card where a `ConfirmDialog` would be heavy (\"Delete line\", \"Remove member\")."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Deleting something important or large (a customer, an order): use `ConfirmDialog`, which can show the consequence in full.\n- Reversible actions: do them and offer **Undo** in a `Toast`.\n- Anything that needs more input: use `Dialog`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan                       (inline-flex)\n  Button                   trigger: variant danger | secondary          (idle)\n─ or ─\nspan[role=group]           aria-labelledby = prompt                    (asking)\n  span                     prompt (text-2)\n  Button ghost             Cancel                                       <- initial focus\n  Button danger-solid | primary   confirm\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                      | Type                    | Default         | Notes                                           |\n| ----------------------------------------- | ----------------------- | --------------- | ----------------------------------------------- |\n| `label`                                   | `string` (required)     | none            | Trigger text: verb plus object (\"Delete line\"). |\n| `onConfirm`                               | `() => void` (required) | none            |                                                 |\n| `prompt`                                  | `string`                | \"Are you sure?\" | State the consequence when you can.             |\n| `confirmLabel`                            | `string`                | `label`         |                                                 |\n| `tone`                                    | `'danger' \\| 'neutral'` | `danger`        | `neutral`: secondary trigger, primary confirm.  |\n| `initialFocus`                            | `'cancel' \\| 'confirm'` | `cancel`        | Cancel is the safe default.                     |\n| `size`                                    | `'sm' \\| 'md' \\| 'lg'`  | `md`            |                                                 |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                         |                 | Controlled / uncontrolled prompt state.         |\n| `isDisabled`                              | `boolean`               | `false`         | Disables the trigger.                           |\n| `messages`                                | `{ cancel, prompt }`    |                 | Per-instance overrides (pt-BR ships).           |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- **Initial focus is Cancel** by default, so Enter or Space pressed right after the click cannot confirm. The handoff auto-focused the confirm button.\n- **Escape cancels, scoped.** The handler is on the prompt, not on `window`. It only fires while focus is inside the prompt, and it stops propagation, so an enclosing `Dialog` or `Sheet` stays open (tested); a second Escape closes the dialog. The handoff attached a window listener (ARCH-005).\n- **Focus returns to the trigger** after Cancel, Escape or confirm.\n- The prompt is a `group` named by the question; screen readers hear it when focus lands on Cancel. Button names are real text; color (red) is never the only signal.\n- Cancel and confirm are normal buttons in the tab order: `Tab` from Cancel reaches confirm.\n- With `defaultOpen`, focus moves to Cancel on mount; avoid it on page load."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Trigger and confirm: verb plus object, sentence case (\"Delete line\"). Not \"Yes\" or \"OK\".\n- Prompt: a short question; add the consequence (\"Remove this line? This can't be undone.\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<InlineConfirm\n  label=\"Delete line\"\n  prompt=\"Remove this line? This can't be undone.\"\n  onConfirm={() => removeLine(line.id)}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Initial focus on Cancel (configurable) instead of confirm.\n- Scoped Escape instead of a window listener; focus restored to the trigger.\n- `danger` boolean became `tone`; `cancelLabel` and the default prompt come from the message catalog (English and pt-BR).\n- Controlled and uncontrolled state; `isDisabled`; the prompt is a labelled group."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ConfirmDialog",
            "reason": "Deleting something important or large (a customer, an order): use `ConfirmDialog`, which can show the consequence in full."
          },
          {
            "name": "Dialog",
            "reason": "Anything that needs more input: use `Dialog`."
          }
        ],
        "uses": ["Button"],
        "usedBy": []
      }
    },
    {
      "name": "Input",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Input } from 'loui-ds';",
      "folder": "src/components/Input",
      "sourceFile": "src/components/Input/Input.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Single-line text field: label, control, description and error wired together\n(`for`, `aria-describedby`, `aria-invalid`), built on React Aria `TextField`.\n\nConsumer `onFocus`, `onBlur`, `onKeyDown` etc. are composed with the internal handlers, so\nthey never break the focus ring (a defect of the handoff).",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control (handoff `hint`). Linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text (handoff `error`). Shown while the field is invalid *and* the error is displayed:\nafter a submit attempt with the default `validationBehavior=\"native\"`, or at once when you\ncontrol `isInvalid`. A function receives the validation result."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field is empty. Never a substitute for the label."
        },
        {
          "name": "mono",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Geist Mono with tabular figures, for codes, identifiers and numbers."
        },
        {
          "name": "leading",
          "type": "ReactNode",
          "required": false,
          "description": "Decoration before the text, usually an `Icon`. Keep it decorative or give it its own name."
        },
        {
          "name": "trailing",
          "type": "ReactNode",
          "required": false,
          "description": "Content after the text: a unit, a shortcut hint (`Kbd`), or an `IconButton`."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root (label, control and messages)."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<input>` element."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTextFieldProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "aria-activedescendant",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-autocomplete",
              "type": "'none' | 'inline' | 'list' | 'both'",
              "required": false
            },
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"false\" | \"true\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "enterKeyHint",
              "type": "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TextFieldRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TextFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMProps",
          "domGeneric": false,
          "count": 8,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            },
            {
              "name": "autoCorrect",
              "type": "string",
              "required": false
            },
            {
              "name": "inputMode",
              "type": "'none' | 'search' | 'text' | 'url' | 'tel' | 'email' | 'numeric' | 'decimal'",
              "required": false
            },
            {
              "name": "maxLength",
              "type": "number",
              "required": false
            },
            {
              "name": "minLength",
              "type": "number",
              "required": false
            },
            {
              "name": "pattern",
              "type": "string",
              "required": false
            },
            {
              "name": "spellCheck",
              "type": "string",
              "required": false
            },
            {
              "name": "type",
              "type": "\"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | (string & {})",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: string) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "string",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: string) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-field-control-padding-inline",
        "--ds-font-mono",
        "--ds-space-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/Input",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-input--default",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-input--with-description",
            "hasPlay": true
          },
          {
            "name": "DescriptionTones",
            "id": "components-input--description-tones",
            "description": "A toned description for live checks (\"Matches\", \"Over by $5\"). The words carry the meaning;\nthe colour only reinforces it. It stays linked with `aria-describedby`.",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-input--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-input--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-input--disabled",
            "hasPlay": false
          },
          {
            "name": "ReadOnly",
            "id": "components-input--read-only",
            "hasPlay": true
          },
          {
            "name": "Mono",
            "id": "components-input--mono",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-input--sizes",
            "hasPlay": false
          },
          {
            "name": "WithAdornments",
            "id": "components-input--with-adornments",
            "hasPlay": false
          },
          {
            "name": "LongLabel",
            "id": "components-input--long-label",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-input--focused",
            "hasPlay": true
          },
          {
            "name": "FocusKeepsRingWithConsumerHandlers",
            "id": "components-input--focus-keeps-ring-with-consumer-handlers",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-input--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Input/Input.md",
        "summary": "Single-line text field with a label, helper text and error text wired together. Built on React Aria `TextField` + `Input`; see [Field](../Field/Field.md) for the shared states, spacing and validation timing.",
        "whenToUse": [
          "Short free text, codes, emails, passwords, search terms, numbers typed as text. For a password with a show/hide toggle, see the `PasswordField` recipe.",
          "Use `mono` for codes and identifiers (`ORD-1048`)."
        ],
        "whenNotToUse": [
          "Several lines of text: `Textarea`.",
          "A choice from a list: `Select`, `RadioGroup`, `Checkbox`.",
          "Currency or quantity entry that needs masking: a dedicated field (`CurrencyInput`, planned)."
        ],
        "keyboard": [
          "Keyboard: standard text editing. `Enter` submits the surrounding form. Focus is visible as the accent border plus ring; in forced-colors mode it is a system outline.",
          "Hotkeys: A consumer `onKeyDown` does not hide keys from `useHotkeys(..., { allowInEditable: true })` shortcuts: a key keeps bubbling unless your handler called `event.preventDefault()` on it. Recipe: `Enter` confirms and `Escape` cancels (both with `preventDefault()`), every other key stays a hotkey. See `docs/architecture/hotkeys.md`."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Short free text, codes, emails, passwords, search terms, numbers typed as text. For a password with a show/hide toggle, see the `PasswordField` recipe.\n- Use `mono` for codes and identifiers (`ORD-1048`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Several lines of text: `Textarea`.\n- A choice from a list: `Select`, `RadioGroup`, `Checkbox`.\n- Currency or quantity entry that needs masking: a dedicated field (`CurrencyInput`, planned)."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nLabel *                          required marker (aria-hidden)\n[ leading  value / placeholder  trailing ]\nDescription\nError\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                | Notes                                                                                                                            |\n| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| `label`                                                             | Visible label. Without it you must pass `aria-label` or `aria-labelledby` (type error otherwise).                                |\n| `description`                                                       | Helper text (handoff `hint`).                                                                                                    |\n| `descriptionTone`                                                   | `neutral` (default), `accent`, `success`, `warning`, `danger`: tints the description for a live check (\"Matches\"). See Field.md. |\n| `errorMessage`                                                      | Error text (handoff `error`), node or function of the validation result. Needs an invalid state.                                 |\n| `isInvalid` / `isRequired` / `isReadOnly` / `isDisabled`            | Standard states. `isInvalid` shows the error at once.                                                                            |\n| `size`                                                              | `sm` 32px, `md` 40px (default), `lg` 48px.                                                                                       |\n| `fullWidth`                                                         | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.                               |\n| `mono`                                                              | Geist Mono with tabular figures.                                                                                                 |\n| `leading` / `trailing`                                              | Adornments inside the box: an `Icon`, a unit, a `Kbd` hint, an `IconButton`.                                                     |\n| `value` / `defaultValue` / `onChange(value)`                        | Controlled and uncontrolled, React Aria naming (`onChange` receives the string, not the event).                                  |\n| `name`, `type`, `autoComplete`, `inputMode`, `maxLength`, `pattern` | Passed to the `<input>`; `name` and the value take part in form submission.                                                      |\n| `validate`, `validationBehavior`                                    | See Field.md.                                                                                                                    |\n| `inputRef` / `ref`                                                  | `inputRef` is the `<input>`, `ref` the field root.                                                                               |\n| `messages`                                                          | `requiredMark` override.                                                                                                         |\n\nAll other props of React Aria `TextField` are accepted (`onFocus`, `onBlur`, `onKeyDown`, `autoFocus`, `form`, ...). They are composed with the internal handlers, never replacing them."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Native `<input>` named by `<label for>`; description and error are in `aria-describedby`; `aria-invalid` when invalid; `required` when required.\n- Keyboard: standard text editing. `Enter` submits the surrounding form. Focus is visible as the accent border plus ring; in forced-colors mode it is a system outline.\n- The placeholder never replaces the label.\n- Hotkeys: A consumer `onKeyDown` does not hide keys from `useHotkeys(..., { allowInEditable: true })` shortcuts: a key keeps bubbling unless your handler called `event.preventDefault()` on it. Recipe: `Enter` confirms and `Escape` cancels (both with `preventDefault()`), every other key stays a hotkey. See `docs/architecture/hotkeys.md`.\n- Click anywhere in the box (including padding) focuses the input. Clicking a decorative adornment does not; make interactive adornments real buttons with their own names."
          },
          {
            "id": "content",
            "heading": "Content",
            "content": "- Label: sentence case noun (\"Workspace name\"). Error: how to fix it (\"Enter a complete email address.\").\n- Description: one short sentence, no exclamation marks."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Input label=\"Workspace name\" placeholder=\"e.g. Northwind\" description=\"Visible to everyone.\" isRequired />\n<Input label=\"Email\" type=\"email\" name=\"email\" errorMessage=\"Enter a complete email address.\" isRequired />\n<Input aria-label=\"Search orders\" leading={<Icon icon={SearchIcon} />} trailing={<Kbd>/</Kbd>} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Focus styling is CSS (`:focus-within`), not React state, and consumer `onFocus`/`onBlur` no longer break it (regression test).\n- `hint` is `description`, `error` is `errorMessage`; the error is only shown for an invalid field.\n- Ids are generated; `aria-invalid`, `aria-describedby`, `required` and `readOnly` are wired.\n- Disabled uses the shared opacity and a dashed border; read-only is new.\n- The handoff `style` / `inputStyle` escape hatches are replaced by `className` and `inputClassName`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Textarea",
            "reason": "Several lines of text: `Textarea`."
          },
          {
            "name": "Select",
            "reason": "A choice from a list: `Select`, `RadioGroup`, `Checkbox`."
          },
          {
            "name": "RadioGroup",
            "reason": "A choice from a list: `Select`, `RadioGroup`, `Checkbox`."
          },
          {
            "name": "Checkbox",
            "reason": "A choice from a list: `Select`, `RadioGroup`, `Checkbox`."
          },
          {
            "name": "CurrencyInput",
            "reason": "Currency or quantity entry that needs masking: a dedicated field (`CurrencyInput`, planned)."
          }
        ],
        "uses": ["FieldMessages", "Label"],
        "usedBy": ["CurrencyInput"]
      }
    },
    {
      "name": "Kbd",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Kbd } from 'loui-ds';",
      "folder": "src/components/Kbd",
      "sourceFile": "src/components/Kbd/Kbd.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A keyboard shortcut hint in mono type. With `keys` it renders `Mod` as the command symbol\non Apple platforms and as Ctrl elsewhere, and gives screen readers the spoken key names.",
      "props": [
        {
          "name": "keys",
          "type": "readonly string[]",
          "required": false,
          "description": "The keys of one shortcut, in order: `['Mod', 'K']`, `['Shift', 'Enter']`, `['Esc']`.\nRecognized names (case-insensitive): `Mod` (Command on Apple, Control elsewhere), `Cmd`,\n`Meta`, `Ctrl`, `Shift`, `Alt`, `Option`, `Enter`, `Esc`, `Tab`, `Backspace`, `Delete`,\n`Space`, arrows (`Up`, `ArrowUp`, ...), `Home`, `End`, `PageUp`, `PageDown`. Anything\nelse is shown as typed (`K`, `/`, `1`). Symbols are hidden from assistive technology and\nreplaced by the spoken name."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Free content shown as is (for example `⌘K`). Prefer `keys`, which is accessible and platform-aware."
        },
        {
          "name": "platform",
          "type": "'apple' | 'other'",
          "required": false,
          "description": "Forces the platform used to render `Mod` and the modifier symbols. By default it is\ndetected in the browser; during server rendering and the first client render it is\n`other` (Ctrl), then switches to `apple` (the command symbol) after hydration.",
          "values": ["apple", "other"]
        },
        {
          "name": "messages",
          "type": "Partial<{ name: (key: KbdKeyId) => string; label: (key: KbdKeyId) => string; }>",
          "required": false,
          "description": "Per-instance overrides of the spoken key names and short key labels."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Kbd",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "name",
            "kind": "function",
            "signature": "(key: KbdKeyId)"
          },
          {
            "key": "label",
            "kind": "function",
            "signature": "(key: KbdKeyId)"
          }
        ]
      },
      "cssVariables": [
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-kbd-border",
        "--ds-kbd-fg",
        "--ds-kbd-height",
        "--ds-kbd-padding-inline",
        "--ds-lh-caption",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Kbd",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-kbd--default",
            "description": "`Mod` follows the platform of the browser that views the story.",
            "hasPlay": false
          },
          {
            "name": "Apple",
            "id": "components-kbd--apple",
            "hasPlay": false
          },
          {
            "name": "Windows",
            "id": "components-kbd--windows",
            "hasPlay": false
          },
          {
            "name": "SingleKey",
            "id": "components-kbd--single-key",
            "hasPlay": false
          },
          {
            "name": "Arrows",
            "id": "components-kbd--arrows",
            "hasPlay": false
          },
          {
            "name": "WithChildren",
            "id": "components-kbd--with-children",
            "hasPlay": false
          },
          {
            "name": "InText",
            "id": "components-kbd--in-text",
            "hasPlay": false
          },
          {
            "name": "PortugueseLocale",
            "id": "components-kbd--portuguese-locale",
            "description": "Spoken names come from the `Kbd` message catalog and follow the `DSProvider` locale.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-kbd--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Kbd/Kbd.md",
        "summary": "A keyboard shortcut hint in mono type. It renders a real `<kbd>` element.",
        "whenToUse": [
          "Next to a command, menu item or button to show its shortcut: \"Search `Ctrl K`\".",
          "In help text and tooltips that name a key."
        ],
        "whenNotToUse": [
          "To register the shortcut: use `useHotkey`. `Kbd` only displays it.",
          "For code or file names: use `<code>`."
        ],
        "keyboard": [
          "When the visible text already is the name (`Enter`, `Tab`) nothing is duplicated."
        ],
        "examples": [
          "<Kbd keys={['Mod', 'K']} />\n<Kbd keys={['Shift', 'Enter']} />\n<Kbd keys={['Esc']} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Next to a command, menu item or button to show its shortcut: \"Search `Ctrl K`\".\n- In help text and tooltips that name a key."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To register the shortcut: use `useHotkey`. `Kbd` only displays it.\n- For code or file names: use `<code>`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nkbd.root (border, mono)\n  kbd.key   symbol or text   (aria-hidden when a spoken name replaces it)\n            VisuallyHidden   spoken name (\"Command\")\n  kbd.key   K\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop       | Type                       | Default  | Notes                                                                     |\n| ---------- | -------------------------- | -------- | ------------------------------------------------------------------------- |\n| `keys`     | `readonly string[]`        | -        | The keys of one shortcut, in order (`['Mod', 'K']`). Use this or children |\n| `children` | `ReactNode`                | -        | Free content shown as is. No accessible expansion                         |\n| `platform` | `'apple' \\| 'other'`       | detected | Forces how `Mod` and the modifier symbols render (tests, docs)            |\n| `messages` | `Partial<{ name; label }>` |          | Overrides the spoken names (`name(key)`) and short labels (`label(key)`)  |\n\nRecognized names in `keys` (case-insensitive): `Mod`, `Cmd`/`Meta`, `Ctrl`, `Shift`, `Alt`/`Option`, `Enter`, `Esc`, `Tab`, `Backspace`, `Delete`, `Space`, `Up`/`ArrowUp` (and Down, Left, Right), `Home`, `End`, `PageUp`, `PageDown`. Anything else (`K`, `/`, `1`) is shown as typed; single letters are upper-cased with the active locale."
          },
          {
            "id": "platform-awareness",
            "heading": "Platform awareness",
            "content": "`Mod` is Command on Apple platforms and Control elsewhere. The server and the first client render use `other` (Ctrl) so hydration matches; the component then reads the real platform (`useSyncExternalStore`) and re-renders with the command symbol on Apple devices. Pass `platform` to pin it. The same detection backs `useHotkey`, so the hint always matches the key that works.\n\nOn Apple platforms modifiers use symbols (`⌘ ⌃ ⇧ ⌥ ↵ ⎋ ⇥ ⌫ ⌦`); elsewhere they are short text (`Ctrl`, `Shift`, `Alt`, `Esc`, `Del`). Command and the arrows use their symbol everywhere."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Inside a `Button` the `Kbd` is `aria-hidden` (the button's `shortcut` prop exposes the keys once through `aria-keyshortcuts`), so the shortcut never doubles the accessible name.\n\n- Screen readers do not reliably read symbols such as `⌘`. Every symbol or abbreviation is `aria-hidden` and followed by a visually hidden spoken name from the message catalog: \"Command K\", \"Control Shift Escape\".\n- When the visible text already is the name (`Enter`, `Tab`) nothing is duplicated.\n- The element has no role and is not focusable: it is text. The shortcut itself must be announced where it works (for example `aria-keyshortcuts` on the control, which is the control's responsibility).\n- Text is `--ds-text-2`, which keeps at least 4.5:1 on the surface (the handoff used `--ds-text-3`)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Write the logical key (`Mod`), not the platform one.\n- One `Kbd` per shortcut. A sequence (\"G then I\") is two `Kbd`s with the word between them from your own copy; sequences are not a built-in prop."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Kbd keys={['Mod', 'K']} />\n<Kbd keys={['Shift', 'Enter']} />\n<Kbd keys={['Esc']} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `keys` replaces hand-typed `⌘K`; the platform decides the modifier.\n- Symbols are expanded for assistive technology with messages (English and pt-BR shipped).\n- Height and padding are tokens (`--ds-kbd-*`). No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["VisuallyHidden"],
        "usedBy": [
          "Button",
          "CommandPalette",
          "SearchInput",
          "ShortcutsHelp",
          "Tab",
          "TabList",
          "TabPanel",
          "Tabs"
        ]
      }
    },
    {
      "name": "Label",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Label } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "The visible label of a field. Must be rendered inside a React Aria field (TextField, Select,\nCheckboxGroup, RadioGroup, ...), which wires `for` / `aria-labelledby`.\nWith `isRequired` it appends a visible marker (text from messages) that is hidden from\nassistive technology: screen readers get \"required\" from the control itself (`required` /\n`aria-required`), so announcing the marker too would repeat it.",
      "props": [
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "isRequired",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Adds the required marker after the text. The control itself must also be required."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLLabelElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"label\", undefined>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "LabelProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "elementType",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "LabelHTMLAttributes",
          "domGeneric": true,
          "count": 2
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Field",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "requiredMark",
            "kind": "string",
            "default": "*",
            "description": "Visible marker after the label of a required field. Hidden from assistive technology."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": [
          "CheckboxGroup",
          "ChoiceTiles",
          "ColorField",
          "ColorOptionGroup",
          "Combobox",
          "DatePicker",
          "Input",
          "NumberField",
          "Radio",
          "RadioGroup",
          "Select",
          "Slider",
          "TagInput",
          "Textarea"
        ]
      }
    },
    {
      "name": "LedgerList",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { LedgerList } from 'loui-ds/patterns';",
      "folder": "src/patterns/LedgerList",
      "sourceFile": "src/patterns/LedgerList/LedgerList.tsx",
      "status": "stable",
      "statusSource": "doc",
      "description": "A list of signed ledger entries: date, title, optional sub line, a signed amount and an\noptional running balance. The sign is text (`+` / `−`) as well as color. Built as a `ul`.\n\nAmounts are integers in minor units (ADR-0006) formatted with `Intl`: pass `currency` for\nmoney, `format` for anything else, or neither for plain quantities.\n\nImport it from `loui-ds/patterns`.",
      "props": [
        {
          "name": "entries",
          "type": "readonly LedgerEntry[]",
          "required": true,
          "description": "The entries, in display order (usually newest first)."
        },
        {
          "name": "currency",
          "type": "string",
          "required": false,
          "description": "ISO 4217 currency code (\"USD\"). When set, amounts and balances are formatted as money with\n`formatCurrency` (minor units). Ignored when `format` is given."
        },
        {
          "name": "format",
          "type": "(value: number) => string",
          "required": false,
          "description": "Custom formatter for amounts and balances, receiving a number in the same unit as `delta`.\nThe amount of an entry is passed as an absolute value (the sign is shown separately).\nWithout `currency` or `format`, numbers are formatted for the locale."
        },
        {
          "name": "negativeTone",
          "type": "'neutral' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'danger'",
          "description": "Color of negative amounts: `neutral`, `success`, `warning` or `danger` (default). The `-`\nsign is always shown too. Pick the tone by what the subtraction means (an expense is\n`danger`, a payment out of a balance owed may be `success`).",
          "values": ["neutral", "success", "warning", "danger"]
        },
        {
          "name": "dateStyle",
          "type": "'short' | 'medium' | 'long' | 'full' | 'monthDay' | 'monthYear'",
          "required": false,
          "default": "'monthDay'",
          "description": "Format of each entry's date (`formatDate` styles). Default `monthDay`.",
          "values": ["short", "medium", "long", "full", "monthDay", "monthYear"]
        },
        {
          "name": "onDelete",
          "type": "(entry: LedgerEntry) => void",
          "required": false,
          "description": "Adds a \"Delete entry\" button to each row (except entries with `isDeletable: false`) and calls\nthis with the entry. The component only reports the request: the consumer must confirm it\n(dialog or inline confirmation) before data is removed, and offer Undo afterwards."
        },
        {
          "name": "onEdit",
          "type": "(entry: LedgerEntry) => void",
          "required": false,
          "description": "Adds an \"Edit entry\" button (pencil icon) before the delete button of each row (except entries\nwith `isEditable: false`) and calls this with the entry. The component only reports the\nrequest: the consumer shows the editing UI, either a `Sheet` or an inline field in the row's\nplace. When a sheet closes, focus returns to the edit button that opened it."
        },
        {
          "name": "messages",
          "type": "Partial<{ balance: (amount: string) => string; editEntry: (title: string) => string; deleteEntry: (title: string) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "LedgerList",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "balance",
            "kind": "function",
            "default": "Balance {amount}",
            "signature": "(amount: string)",
            "description": "Label of the running balance after an entry. `amount` is already formatted."
          },
          {
            "key": "editEntry",
            "kind": "function",
            "default": "Edit entry {title}",
            "signature": "(title: string)",
            "description": "Accessible name of the edit button of an entry; `title` is the entry's title."
          },
          {
            "key": "deleteEntry",
            "kind": "function",
            "default": "Delete entry {title}",
            "signature": "(title: string)",
            "description": "Accessible name of the delete button of an entry; `title` is the entry's title."
          }
        ]
      },
      "cssVariables": [
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-ledger-list-date-width",
        "--ds-ledger-list-gap",
        "--ds-ledger-list-padding-inline",
        "--ds-ledger-list-text-gap",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line-subtle",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Patterns/LedgerList",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-ledger-list--default",
            "hasPlay": true
          },
          {
            "name": "NegativeAsSuccess",
            "id": "patterns-ledger-list--negative-as-success",
            "hasPlay": false
          },
          {
            "name": "NegativeNeutral",
            "id": "patterns-ledger-list--negative-neutral",
            "hasPlay": false
          },
          {
            "name": "WithoutBalance",
            "id": "patterns-ledger-list--without-balance",
            "hasPlay": false
          },
          {
            "name": "Quantities",
            "id": "patterns-ledger-list--quantities",
            "description": "Plain quantities: no currency, numbers formatted for the locale.",
            "hasPlay": false
          },
          {
            "name": "CustomFormat",
            "id": "patterns-ledger-list--custom-format",
            "description": "A custom formatter receives the absolute amount in the same unit as `delta`.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-ledger-list--long-text",
            "hasPlay": false
          },
          {
            "name": "NarrowEditableAndDeletable",
            "id": "patterns-ledger-list--narrow-editable-and-deletable",
            "description": "384 px with date, amount, balance, edit and delete: the date moves under the title.",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "patterns-ledger-list--empty",
            "hasPlay": false
          },
          {
            "name": "Deletable",
            "id": "patterns-ledger-list--deletable",
            "description": "The consumer is responsible for confirming before data is removed.",
            "hasPlay": true
          },
          {
            "name": "KeyboardDelete",
            "id": "patterns-ledger-list--keyboard-delete",
            "hasPlay": true
          },
          {
            "name": "EditableAndDeletable",
            "id": "patterns-ledger-list--editable-and-deletable",
            "description": "Edit and delete side by side; edit comes first. Fixed rows keep the column.",
            "hasPlay": true
          },
          {
            "name": "EditOnly",
            "id": "patterns-ledger-list--edit-only",
            "description": "Edit only, without delete.",
            "hasPlay": false
          },
          {
            "name": "EditInSheet",
            "id": "patterns-ledger-list--edit-in-sheet",
            "description": "The consumer owns the editing UI: here a `Sheet` with a `CurrencyInput`. When the sheet\ncloses, focus returns to the edit button that opened it.",
            "hasPlay": true
          },
          {
            "name": "EditInline",
            "id": "patterns-ledger-list--edit-inline",
            "description": "Inline editing stays with the consumer too: `onEdit` reveals a `CurrencyInput` (here under the\nlist; it could replace the row) and the consumer moves focus into it.",
            "hasPlay": true
          },
          {
            "name": "MixedDeletableAndFixed",
            "id": "patterns-ledger-list--mixed-deletable-and-fixed",
            "description": "`isDeletable: false` marks fixed rows: no button, and the amounts stay aligned.",
            "hasPlay": true
          },
          {
            "name": "WithoutDate",
            "id": "patterns-ledger-list--without-date",
            "description": "Entries without a date keep the date column when other rows have one.",
            "hasPlay": true
          },
          {
            "name": "NoDatesAtAll",
            "id": "patterns-ledger-list--no-dates-at-all",
            "description": "When no entry has a date, the date column is dropped entirely.",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "patterns-ledger-list--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "patterns-ledger-list--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "patterns-ledger-list--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/LedgerList/LedgerList.md",
        "summary": "A list of signed entries: date, title, optional sub line, a signed amount and an optional running balance, with optional edit and delete buttons per row. Rows can be fixed (no buttons) and the date is optional. The sign is shown as text and color. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/erp/LedgerList.jsx` (TODO `CMP-PAT-12`).",
        "whenToUse": [
          "A short history of changes to a balance or a quantity: account activity, wallet movements, stock adjustments.",
          "When the running balance after each entry matters."
        ],
        "whenNotToUse": [
          "Large, sortable or filterable data: use `DataTable`.",
          "A feed of events with no amounts: use a plain list."
        ],
        "keyboard": [
          "Delete buttons are `IconButton`s named \"Delete entry {title}\" (one per deletable row, distinguishable); they are 32px. Rows with `isDeletable: false` have no button, so they are skipped by `Tab`."
        ],
        "examples": [
          "import { LedgerList } from 'loui-ds/patterns';\n\n<LedgerList\n  aria-label=\"Account activity\"\n  currency=\"USD\"\n  entries={[\n    { id: '2', date: '2026-03-12', title: 'Payment received', delta: 5000, balance: 15000 },\n    { id: '1', date: '2026-03-01', title: 'Service fee', delta: -1000, balance: 10000 },\n  ]}\n  onDelete={askToDelete}\n/>;",
          "<LedgerList\n  aria-label=\"Account activity\"\n  currency=\"USD\"\n  entries={[\n    { id: '2', date: '2026-03-12', title: 'Payment received', delta: 5000, balance: 15000 },\n    { id: '1', title: 'Opening balance', delta: 10000, balance: 10000, isDeletable: false },\n  ]}\n  onDelete={askToDelete}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A short history of changes to a balance or a quantity: account activity, wallet movements, stock adjustments.\n- When the running balance after each entry matters."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Large, sortable or filterable data: use `DataTable`.\n- A feed of events with no amounts: use a plain list."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nul aria-label\n  li[data-direction=positive|negative|zero]\n    time[datetime]                 formatDate(date, locale, dateStyle)   (only when the entry has a date)\n    title · sub\n    amount                         \"+\" | \"−\" text sign + number   (color by negativeTone)\n    balance                        \"Balance $125.00\"              (optional)\n    button \"Edit entry …\"          (only with onEdit and isEditable !== false)\n    button \"Delete entry …\"        (only with onDelete and isDeletable !== false)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type                                              | Default      | Notes                                                                                                         |\n| -------------- | ------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------- |\n| `entries`      | `LedgerEntry[]`                                   | required     | `{ id, date?, title, sub?, delta, balance?, isEditable?, isDeletable? }`; `delta` and `balance` are integers. |\n| `currency`     | `string`                                          | none         | ISO 4217 code: amounts are minor units formatted with `formatCurrency`.                                       |\n| `format`       | `(value: number) => string`                       | none         | Custom formatter; wins over `currency`. Amounts are passed as absolute values.                                |\n| `negativeTone` | `'neutral' \\| 'success' \\| 'warning' \\| 'danger'` | `'danger'`   | Color of negative amounts; the minus sign is always shown.                                                    |\n| `dateStyle`    | `DateStyle`                                       | `'monthDay'` | Any `formatDate` style.                                                                                       |\n| `onEdit`       | `(entry) => void`                                 | none         | Adds an edit button (pencil) before the delete button of each editable row (see below).                       |\n| `onDelete`     | `(entry) => void`                                 | none         | Adds a delete button to each deletable row (see below).                                                       |\n| `messages`     | `{ balance, editEntry, deleteEntry }`             | none         | Overrides the texts for this instance.                                                                        |\n\nWith neither `currency` nor `format`, numbers are formatted for the locale (plain quantities).\n\n### Entry fields added for mixed lists\n\n| Field         | Type      | Default | Notes                                                                     |\n| ------------- | --------- | ------- | ------------------------------------------------------------------------- |\n| `date`        | `ISODate` | none    | Optional. Without it the row renders no `time` element.                   |\n| `isEditable`  | `boolean` | `true`  | `false` renders no edit button on that row. Ignored without `onEdit`.     |\n| `isDeletable` | `boolean` | `true`  | `false` renders no delete button on that row. Ignored without `onDelete`. |\n\n### Mixed deletable and fixed rows\n\nPass `onDelete` and set `isDeletable: false` on the entries that must stay (an opening balance, a system adjustment). Those rows render no button at all, so there is nothing to focus or announce on them, and the delete column is still reserved so their amounts align with the rows above and below. Omitting `isDeletable` keeps the previous behavior: every row is deletable when `onDelete` is set.\n\n### Editing entries\n\nPass `onEdit` to add an \"Edit entry {title}\" button (pencil icon) before the delete button of every row, or only of rows that are not `isEditable: false`. The buttons share one fixed actions column (one or two 32px buttons, depending on which handlers the list has), so amounts stay aligned between rows with both, one or no button. The component only calls `onEdit(entry)`; the editing UI belongs to the consumer, either way:\n\n- **Sheet.** Keep the entry in state and open a controlled `Sheet` with a `CurrencyInput` (see the `EditInSheet` story). When the sheet closes, the overlay restores focus to the edit button that opened it; do not move focus yourself.\n- **Inline.** Reveal a field (a `CurrencyInput`) in place of or under the row (see `EditInline`), move focus into it, save on `Enter` and return focus to the edit button after saving or cancelling with `Escape`.\n\nSaving should show a \"Saved\"-style toast or inline confirmation; editing is reversible, so there is no confirmation dialog.\n\n### Entries without a date\n\n- If at least one entry has a date, the date column is kept: dated rows show it and undated rows leave it empty, so titles and amounts stay in the same columns.\n- If no entry has a date, the column is dropped and titles start at the left edge.\n- Reading order is unchanged: title, sub line, amount, balance, delete button; the date comes first only on rows that have one."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A `ul` of `li`; the list should be named with `aria-label` or `aria-labelledby`.\n- The sign is text (`+` or the minus sign U+2212), so meaning never depends on color. Positive amounts use ink, negative ones the chosen tone, zero uses secondary text.\n- Dates are `<time datetime>`. The balance is labelled by text (\"Balance $125.00\"), not by position.\n- Edit buttons are `IconButton`s named \"Edit entry {title}\" and come before the delete button in the tab order. Rows with `isEditable: false` have none.\n- Delete buttons are `IconButton`s named \"Delete entry {title}\" (one per deletable row, distinguishable); they are 32px. Rows with `isDeletable: false` have no button, so they are skipped by `Tab`.\n- The component only calls `onDelete(entry)`; it never removes anything or asks for confirmation (AGENTS.md section 5.5). The consumer opens a confirmation dialog (title \"Delete X?\", the consequence, a danger button naming the action, initial focus on Cancel) in `onDelete`, removes the entry on confirm and then shows a toast with Undo.\n- Text truncates with an ellipsis; long titles do not push the amounts out."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Titles say what happened in sentence case (\"Payment received\"); the sub line holds the reference.\n- Amounts are integers in minor units (ADR-0006); never pre-format them."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nimport { LedgerList } from 'loui-ds/patterns';\n\n<LedgerList\n  aria-label=\"Account activity\"\n  currency=\"USD\"\n  entries={[\n    { id: '2', date: '2026-03-12', title: 'Payment received', delta: 5000, balance: 15000 },\n    { id: '1', date: '2026-03-01', title: 'Service fee', delta: -1000, balance: 10000 },\n  ]}\n  onDelete={askToDelete}\n/>;\n```\n\nA drawer that mixes a fixed opening balance with deletable payments, where `askToDelete` opens the confirmation dialog:\n\n```tsx\n<LedgerList\n  aria-label=\"Account activity\"\n  currency=\"USD\"\n  entries={[\n    { id: '2', date: '2026-03-12', title: 'Payment received', delta: 5000, balance: 15000 },\n    { id: '1', title: 'Opening balance', delta: 10000, balance: 10000, isDeletable: false },\n  ]}\n  onDelete={askToDelete}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A `div` of `div`s becomes a `ul` / `li` list; dates are `time` elements formatted with `formatDate` (the handoff printed a raw string).\n- Amounts go through `Intl` (`formatCurrency` or `formatNumber`) from minor units, or a `format` prop; there is no hand-built number formatting.\n- \"saldo\" and \"Excluir lançamento\" were hard-coded Portuguese; they are messages (`en`, `pt-BR`), and the delete label includes the entry title.\n- `negativeTone` defaults to `danger` (the handoff defaulted to `success`, which is product specific) and takes `neutral | success | warning | danger`.\n- The edit and delete buttons are the shared `IconButton` (focus ring, 32px target) instead of a styled `×` glyph; layout values are tokens (`--ds-ledger-list-*`)."
          },
          {
            "id": "narrow-containers",
            "heading": "Narrow containers",
            "content": "The list is a size container. At 480 px or less the date moves under the title as a meta line, the padding shrinks and the amount, balance and actions columns stay fixed, so titles keep a readable width. The DOM and reading order do not change."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Large, sortable or filterable data: use `DataTable`."
          }
        ],
        "uses": ["IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "Legend",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Legend } from 'loui-ds';",
      "folder": "src/components/Legend",
      "sourceFile": "src/components/Legend/Legend.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A list of the series of a chart: swatch, name and, optionally, a value. With `interactive`\neach item is a toggle button that hides or shows its series, and the legend reports the\nhidden ids so the chart can leave those series out (`StackedBar` does this itself; with\n`Chart` keep `hiddenIds` in state and filter your data).\n\nThe swatch is never the only identification: every item has its text label, a hatch pattern\ntells apart series beyond the fourth, and a hidden series shows a hollow swatch and a struck\nlabel in addition to the pressed state read by screen readers.",
      "props": [
        {
          "name": "items",
          "type": "readonly LegendItem[]",
          "required": true,
          "description": "The series, in the same order as in the chart (the order sets the swatch of items without `color`)."
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'",
          "description": "`horizontal` (default) wraps items in a row; `vertical` stacks them, one per line.",
          "values": ["horizontal", "vertical"]
        },
        {
          "name": "interactive",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Turns each item into a toggle button (`aria-pressed`: pressed means the series is shown),\nso readers can hide series in the chart that consumes the legend. Off by default: a plain list."
        },
        {
          "name": "hiddenIds",
          "type": "readonly string[]",
          "required": false,
          "description": "Controlled list of hidden series ids (interactive legends)."
        },
        {
          "name": "defaultHiddenIds",
          "type": "readonly string[]",
          "required": false,
          "default": "[]",
          "description": "Initial hidden series ids when uncontrolled."
        },
        {
          "name": "onHiddenIdsChange",
          "type": "(hiddenIds: string[]) => void",
          "required": false,
          "description": "Called with the new list of hidden ids whenever the user toggles an item."
        },
        {
          "name": "showShare",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Prints the share of each item (its `amount` over the denominator) in a column of its own,\nformatted with `Intl` for the active locale. When an item also has a `value`, both show, in\ntwo aligned columns (value, then share). Hidden series print no share and leave the\ndenominator. Percentages are rounded one by one, so the printed shares may sum to 99% or\n101%; raise `shareDigits` when that matters, the legend never adjusts them."
        },
        {
          "name": "shareOf",
          "type": "number",
          "required": false,
          "description": "Denominator of the shares, in the unit of `amount`. Default: the sum of the amounts of the\nvisible (not hidden) items. Pass the real total when the items are only part of a whole."
        },
        {
          "name": "shareDigits",
          "type": "number",
          "required": false,
          "default": "0",
          "description": "Decimals of the printed shares. Default `0`."
        },
        {
          "name": "messages",
          "type": "Partial<{ label: string; toggleHint: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Legend",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "label",
            "kind": "string",
            "default": "Legend",
            "description": "Accessible name of the legend list, unless the consumer passes `aria-label` or `aria-labelledby`."
          },
          {
            "key": "toggleHint",
            "kind": "string",
            "default": "Press a series to show or hide it.",
            "description": "Accessible description of an interactive legend: what pressing an item does. Read after the name of the list."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-chart-series-1",
        "--ds-chart-series-2",
        "--ds-chart-series-3",
        "--ds-chart-series-4",
        "--ds-chart-series-hatch-size",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-legend-color",
        "--ds-legend-gap",
        "--ds-legend-share-col-w",
        "--ds-legend-swatch-size",
        "--ds-lh-small",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-surface-hover",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/Legend",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-legend--default",
            "description": "A plain list: swatch, name and an optional value.",
            "hasPlay": true
          },
          {
            "name": "Vertical",
            "id": "components-legend--vertical",
            "hasPlay": false
          },
          {
            "name": "NamesOnly",
            "id": "components-legend--names-only",
            "hasPlay": false
          },
          {
            "name": "EightSeries",
            "id": "components-legend--eight-series",
            "description": "Beyond four series the ramp repeats with a hatch, so no two swatches look alike.",
            "hasPlay": false
          },
          {
            "name": "ExplicitColors",
            "id": "components-legend--explicit-colors",
            "description": "Explicit colors are for when the neutral ramp is not enough; the label still names every series.",
            "hasPlay": false
          },
          {
            "name": "Interactive",
            "id": "components-legend--interactive",
            "description": "Interactive items are toggle buttons (`aria-pressed`: pressed means shown). A hidden series\nshows a hollow swatch and a struck label, not only a color change.",
            "hasPlay": true
          },
          {
            "name": "InteractiveWithHiddenSeries",
            "id": "components-legend--interactive-with-hidden-series",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-legend--long-labels",
            "hasPlay": false
          },
          {
            "name": "WithChart",
            "id": "components-legend--with-chart",
            "description": "The legend is not tied to a chart: keep the hidden ids in state and filter your data.",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-legend--portuguese-brazil",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-legend--right-to-left",
            "hasPlay": false
          },
          {
            "name": "WithShare",
            "id": "components-legend--with-share",
            "description": "`showShare` prints the share of each `amount` for the active locale, in a column next to the value.",
            "hasPlay": true
          },
          {
            "name": "ShareOfTotal",
            "id": "components-legend--share-of-total",
            "description": "Without a `value` and with `shareOf`, the items are only part of a whole of 4,000.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Legend/Legend.md",
        "summary": "A list of the series of a chart: swatch, name and, optionally, a value. With `interactive` each item is a toggle button that hides or shows its series, and the legend reports which ones are hidden so the chart can leave them out.",
        "whenToUse": [
          "Naming the series of a multi-series chart (`StackedBar` renders one itself).",
          "Letting readers hide series to compare the rest.",
          "Next to a `Chart` when you plot one series at a time and let the reader pick which."
        ],
        "whenNotToUse": [
          "One series: name the chart instead.",
          "Choosing between views or filters unrelated to a chart: use `SegmentedControl`, `ToggleGroup` or `Tabs`.",
          "Showing the parts of a single bar with percentages: `ProportionBar` has its own legend."
        ],
        "keyboard": [
          "Interactive items are native buttons (React Aria `ToggleButton`): `Tab` moves between them, `Space` and `Enter` toggle. `aria-pressed=\"true\"` means the series is **shown**. The list is described by a hint (\"Press a series to show or hide it.\")."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Naming the series of a multi-series chart (`StackedBar` renders one itself).\n- Letting readers hide series to compare the rest.\n- Next to a `Chart` when you plot one series at a time and let the reader pick which."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- One series: name the chart instead.\n- Choosing between views or filters unrelated to a chart: use `SegmentedControl`, `ToggleGroup` or `Tabs`.\n- Showing the parts of a single bar with percentages: `ProportionBar` has its own legend."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nul[aria-label=\"Legend\"]\n  li  swatch (aria-hidden) · name · value (optional)\n  li  button[aria-pressed] (interactive)  swatch · name · value\nVisuallyHidden  hint, linked with aria-describedby (interactive only)\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                | Type                                       | Default        | Notes                                                      |\n| ------------------- | ------------------------------------------ | -------------- | ---------------------------------------------------------- |\n| `items`             | `{ id, label, value?, amount?, color? }[]` | -              | Same order as the chart: the position sets the swatch      |\n| `showShare`         | `boolean`                                  | `false`        | Prints each item's share of the `amount`s (needs `amount`) |\n| `shareOf`           | `number`                                   | sum of visible | Denominator of the shares                                  |\n| `shareDigits`       | `number`                                   | `0`            | Decimals of the printed shares                             |\n| `orientation`       | `'horizontal' \\| 'vertical'`               | `'horizontal'` | Row that wraps, or one item per line                       |\n| `interactive`       | `boolean`                                  | `false`        | Items become toggle buttons                                |\n| `hiddenIds`         | `readonly string[]`                        |                | Controlled hidden series                                   |\n| `defaultHiddenIds`  | `readonly string[]`                        | `[]`           | Initial hidden series when uncontrolled                    |\n| `onHiddenIdsChange` | `(ids: string[]) => void`                  |                | Called on every toggle                                     |\n| `messages`          | `Partial<messages>`                        |                | `label` (list name), `toggleHint`                          |"
          },
          {
            "id": "showing-the-share",
            "heading": "Showing the share",
            "content": "Give each item a numeric `amount` and set `showShare`: the legend prints `amount / denominator` with `formatPercent` for the provider locale. The denominator is the sum of the visible items (hidden series leave it, and print no share) unless you pass `shareOf` because the items are only part of a whole. `value` still works as a free node; with both, a vertical legend lines value and share up in two columns (tabular mono). Percentages are rounded one by one, so three equal items show 33% each and the shares may sum to 99% or 101%. The legend never fudges them; raise `shareDigits` if that matters. `StackedBar` uses this through its `legendShare` prop."
          },
          {
            "id": "telling-series-apart-without-color",
            "heading": "Telling series apart without color",
            "content": "The swatch follows the same neutral ramp as `StackedBar` and `ProportionBar` (`--ds-chart-series-1` to `-4`, which are `text`, `text-2`, `text-3` and `line-control`). From the fifth series on the ramp repeats with a hatch pattern, so up to eight series look different. Beyond that, pass `color` or group the tail into \"Other\". The text label always identifies the series; the swatch only helps sighted users.\n\nA hidden series is shown with a hollow swatch and a struck-through label, in addition to `aria-pressed=\"false\"`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A real list named \"Legend\" (override with `aria-label` or `aria-labelledby`).\n- Interactive items are native buttons (React Aria `ToggleButton`): `Tab` moves between them, `Space` and `Enter` toggle. `aria-pressed=\"true\"` means the series is **shown**. The list is described by a hint (\"Press a series to show or hide it.\").\n- Toggle buttons are at least 24 px tall (target size 2.5.8) and have a visible focus ring; forced colors use system colors for swatches and focus.\n- The chart that consumes the legend must update visibly when a series is toggled. If it can end up with nothing to show, say so in text (`StackedBar` does)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Series names are short nouns in sentence case (\"Online\", \"Store\").\n- Values are formatted text; use `Intl` helpers for the locale."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\nconst [hidden, setHidden] = useState<string[]>([]);\n\n<Legend\n  interactive\n  items={[\n    { id: 'online', label: 'Online' },\n    { id: 'store', label: 'Store' },\n  ]}\n  hiddenIds={hidden}\n  onHiddenIdsChange={setHidden}\n/>;\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "Choosing between views or filters unrelated to a chart: use `SegmentedControl`, `ToggleGroup` or `Tabs`."
          },
          {
            "name": "ToggleGroup",
            "reason": "Choosing between views or filters unrelated to a chart: use `SegmentedControl`, `ToggleGroup` or `Tabs`."
          },
          {
            "name": "Tabs",
            "reason": "Choosing between views or filters unrelated to a chart: use `SegmentedControl`, `ToggleGroup` or `Tabs`."
          },
          {
            "name": "ProportionBar",
            "reason": "Showing the parts of a single bar with percentages: `ProportionBar` has its own legend."
          }
        ],
        "uses": ["VisuallyHidden"],
        "usedBy": ["StackedBar"]
      }
    },
    {
      "name": "Link",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Link } from 'loui-ds';",
      "folder": "src/components/Link",
      "sourceFile": "src/components/Link/Link.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A link built on React Aria's `Link`. With `href` it renders a real `<a>` and uses the router\nset on `DSProvider` for client-side navigation. Links that open a new tab (`target=\"_blank\"`)\nget `rel=\"noopener noreferrer\"`, an external-link icon and a hidden note for screen readers.",
      "props": [
        {
          "name": "variant",
          "type": "'inline' | 'standalone'",
          "required": false,
          "default": "'inline'",
          "description": "- `inline` (default): underlined, for links inside running text. Color alone does not\n  identify a link (WCAG 1.4.1), so the underline is part of the design.\n- `standalone`: no underline at rest (underlined on hover and focus). Only where the context\n  makes it obvious that it is a link: a navigation list, a \"View all\" line under a heading.",
          "values": ["inline", "standalone"]
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Link text. Describe the destination or action (\"Pricing details\"), never \"click here\"."
        },
        {
          "name": "messages",
          "type": "Partial<{ opensInNewTab: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLAnchorElement>",
          "required": false,
          "description": "Ref to the underlying `<a>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaLinkOptions",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaLinkProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-current",
              "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "LinkDOMProps",
          "domGeneric": false,
          "count": 8,
          "props": [
            {
              "name": "download",
              "type": "string | boolean",
              "required": false
            },
            {
              "name": "href",
              "type": "string",
              "required": false
            },
            {
              "name": "hrefLang",
              "type": "string",
              "required": false
            },
            {
              "name": "ping",
              "type": "string",
              "required": false
            },
            {
              "name": "referrerPolicy",
              "type": "'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'",
              "required": false
            },
            {
              "name": "rel",
              "type": "string",
              "required": false
            },
            {
              "name": "routerOptions",
              "type": "never",
              "required": false
            },
            {
              "name": "target",
              "type": "HTMLAttributeAnchorTarget",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "PossibleLinkDOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "(props: DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement> | DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, renderProps: LinkRenderProps) => ReactElement<...>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {
        "variant": ["inline", "standalone"]
      },
      "messages": {
        "providerKey": "Link",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "opensInNewTab",
            "kind": "string",
            "default": "(opens in a new tab)"
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-focus-ring",
        "--ds-opacity-disabled",
        "--ds-radius-sm",
        "--ds-space-1"
      ],
      "stories": {
        "title": "Components/Link",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-link--default",
            "hasPlay": true
          },
          {
            "name": "InText",
            "id": "components-link--in-text",
            "description": "In running text the underline is part of the design: color alone does not identify a link.",
            "hasPlay": false
          },
          {
            "name": "Standalone",
            "id": "components-link--standalone",
            "description": "No underline at rest. Use it only where the context makes the link obvious.",
            "hasPlay": true
          },
          {
            "name": "External",
            "id": "components-link--external",
            "description": "Opens a new tab: `rel` is set, an icon shows it, and screen readers hear it.",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-link--disabled",
            "hasPlay": true
          },
          {
            "name": "KeyboardActivation",
            "id": "components-link--keyboard-activation",
            "description": "Keyboard: Tab focuses, Enter follows the link through the router set on DSProvider.",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-link--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Link/Link.md",
        "summary": "A link to another page, section or site. Built on React Aria's `Link`: a real `<a href>` that uses the router set on `DSProvider` for client-side navigation.\n\n- Handoff source: none (the handoff only had underlined `<a>` styles); part of GAP-002.",
        "whenToUse": [
          "To go somewhere: another route, a section (`#id`), a document or an external site.",
          "Inside running text (`inline`), or as a quiet navigation line such as \"View all orders\" (`standalone`)."
        ],
        "whenNotToUse": [
          "To run an action in place (save, delete, open a dialog): use `Button`.",
          "For a styled call to action that happens to navigate: still a link semantically, but use a `Button` with `href` when it is the primary action of the screen (planned with the button-as-link variant)."
        ],
        "keyboard": [
          "Keys: `Tab` focuses, `Enter` follows. (`Space` does not activate a link, by design.)"
        ],
        "examples": [
          "<p>\n  Read the <Link href=\"/pricing\">pricing details</Link> before you change a plan.\n</p>\n\n<Link variant=\"standalone\" href=\"/orders\">View all orders</Link>\n\n<Link href=\"https://example.com/docs\" target=\"_blank\">Documentation</Link>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- To go somewhere: another route, a section (`#id`), a document or an external site.\n- Inside running text (`inline`), or as a quiet navigation line such as \"View all orders\" (`standalone`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To run an action in place (save, delete, open a dialog): use `Button`.\n- For a styled call to action that happens to navigate: still a link semantically, but use a `Button` with `href` when it is the primary action of the screen (planned with the button-as-link variant)."
          },
          {
            "id": "variants",
            "heading": "Variants",
            "content": "| Variant      | At rest   | Hover and keyboard focus | Use                                                                      |\n| ------------ | --------- | ------------------------ | ------------------------------------------------------------------------ |\n| `inline`     | underline | thicker underline        | Links inside sentences. Default.                                         |\n| `standalone` | none      | underline                | Navigation lists, \"View all\" lines. Only where context makes it obvious. |\n\nColor alone never identifies a link (WCAG 1.4.1), so inline links are always underlined. The accent color (`--ds-accent`) is reserved for focus, selection and links."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                         | Default    | Notes                                                                       |\n| ------------ | ---------------------------- | ---------- | --------------------------------------------------------------------------- |\n| `href`       | `string`                     | none       | Destination. Without it the element is a non-navigating `role=\"link\"` span. |\n| `variant`    | `'inline' \\| 'standalone'`   | `'inline'` | See above.                                                                  |\n| `target`     | `string`                     | none       | `_blank` adds `rel`, the icon and the screen reader note.                   |\n| `rel`        | `string`                     | none       | Merged with `noopener noreferrer` for `_blank`.                             |\n| `isDisabled` | `boolean`                    | `false`    | `aria-disabled`, 35% opacity, presses ignored.                              |\n| `onPress`    | `(e: PressEvent) => void`    | none       | React Aria press event, fired before navigation.                            |\n| `messages`   | `{ opensInNewTab?: string }` | none       | Per-instance message override (ADR-0005).                                   |\n| `className`  | `string`                     | none       | Merged with the component class.                                            |\n\nOther React Aria `Link` props (`download`, `routerOptions`, `hrefLang`, `aria-*`, ...) and `ref` pass through."
          },
          {
            "id": "routing",
            "heading": "Routing",
            "content": "Wrap the app once: `<DSProvider navigate={router.push} useHref={router.resolve}>`. Links with a relative `href` then navigate without a page reload; modified clicks (Ctrl/Cmd, middle button) and `target=\"_blank\"` keep the browser behavior."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Real `<a href>`: opens in a new tab with the context menu, can be copied, works without JavaScript.\n- Keys: `Tab` focuses, `Enter` follows. (`Space` does not activate a link, by design.)\n- Focus: 3px accent ring through `data-focus-visible`; forced-colors mode repaints the transparent outline and system link colors apply.\n- Links that open a new tab: the name ends with \"(opens in a new tab)\" for screen readers (hidden text from the `Link` messages) and a decorative icon shows it visually. `rel=\"noopener noreferrer\"` is added automatically.\n- Contrast: accent `#8AB4FF` on the dark surfaces is well above 4.5:1.\n- Target size: inline links in text are exempt from WCAG 2.5.8; standalone links should sit in a 24px-high row.\n- Reduced motion: hover changes decoration thickness only, no animation."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Link text describes the destination or action and makes sense out of context: \"Pricing details\", \"Download invoice\". Never \"click here\" or \"read more\".\n- Sentence case, no trailing punctuation inside the link, no emoji.\n- Do not repeat the word \"link\". The role is announced."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<p>\n  Read the <Link href=\"/pricing\">pricing details</Link> before you change a plan.\n</p>\n\n<Link variant=\"standalone\" href=\"/orders\">View all orders</Link>\n\n<Link href=\"https://example.com/docs\" target=\"_blank\">Documentation</Link>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- New component (GAP-002 part); there was no handoff `Link`. Unstyled `<a>` underlines in the handoff foundation are now this component.\n- Adds the external-link behavior (`rel`, icon, screen reader note) and a standalone variant."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Button",
            "reason": "To run an action in place (save, delete, open a dialog): use `Button`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": ["Citation", "SourceList"]
      }
    },
    {
      "name": "MatrixGrid",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { MatrixGrid } from 'loui-ds/patterns';",
      "folder": "src/patterns/MatrixGrid",
      "sourceFile": "src/patterns/MatrixGrid/MatrixGrid.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A matrix of toggle cells: rows by columns, each cell linking or unlinking one row with one\ncolumn. It is an ARIA `grid` with roving focus: the grid is one tab stop, the arrow keys move\nbetween cells (`Home` / `End` within a row, `Ctrl+Home` / `Ctrl+End` to the first / last\ncell), and `Space` or `Enter` toggles. Arrow keys follow the reading direction.\n\nThe row and the column of the focused cell get a subtle surface step (a visual aid; the focus\nring and the cell name carry the meaning). The column header row and the row header column stay\nfixed while the region scrolls; give the root a height (`className`) to scroll vertically.\n`onToggle` may return a promise for a per-cell pending state (see `onToggle`).\n\nEach cell is a button named \"{row} · {column}: linked\" / \"not linked\" (messages). The grid\nscrolls horizontally inside a labelled, focusable region.\n\nImport it from `loui-ds/patterns`.",
      "props": [
        {
          "name": "aria-label",
          "type": "string",
          "required": true,
          "description": "Accessible name of the grid (\"Product colors\"). The scroll region around it is named from it too."
        },
        {
          "name": "rows",
          "type": "readonly MatrixGridRow[]",
          "required": true,
          "description": "The rows, in display order. Sort them yourself (alphabetically by default)."
        },
        {
          "name": "columns",
          "type": "readonly MatrixGridColumn[]",
          "required": true,
          "description": "The columns, in display order."
        },
        {
          "name": "isLinked",
          "type": "(rowId: string, columnId: string) => boolean",
          "required": true,
          "description": "Whether a row and a column are linked. Called for every cell on each render; keep it cheap."
        },
        {
          "name": "onToggle",
          "type": "(rowId: string, columnId: string) => unknown",
          "required": true,
          "description": "Called with the row and column ids when the user toggles a cell. Update your state in it.\n\nIt may return a promise (a request to save the link). While it is pending the cell shows a\nspinner, is `aria-busy` and ignores further toggles. The grid never flips a cell by itself:\nthe cell shows what `isLinked` returns, so if you update optimistically, revert in\n`onToggleError`. A rejected promise calls `onToggleError`."
        },
        {
          "name": "onToggleError",
          "type": "(error: unknown, rowId: string, columnId: string) => void",
          "required": false,
          "description": "Called when the promise returned by `onToggle` rejects, with the error and the cell's ids.\nRevert an optimistic update here and tell the user (a toast). The grid also announces\n\"Couldn't update {row} · {column}. Try again.\" in a visually hidden status."
        },
        {
          "name": "cornerLabel",
          "type": "string",
          "required": false,
          "description": "Caption of the first (row header) column. Without it a visually hidden \"Row\" is used."
        },
        {
          "name": "countLabel",
          "type": "string",
          "required": false,
          "description": "Adds a last column with the number of linked cells per row; this is its header (\"Linked\")."
        },
        {
          "name": "hint",
          "type": "ReactNode",
          "required": false,
          "description": "Help text under the grid, linked to it with `aria-describedby`."
        },
        {
          "name": "messages",
          "type": "Partial<{ cell: (row: string, column: string, linked: boolean) => string; cellPending: (row: string, column: string, linked: boolean) => string; toggleFailed: (row: string, column: string) => string; scrollRegion: (name: string) => string; rowHeader: string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "MatrixGrid",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "cell",
            "kind": "function",
            "default": "{row} · {column}: not linked",
            "signature": "(row: string, column: string, linked: boolean)",
            "description": "Accessible name of a toggle cell: the row, the column and the state of the link."
          },
          {
            "key": "cellPending",
            "kind": "function",
            "default": "{row} · {column}: not linked, saving",
            "signature": "(row: string, column: string, linked: boolean)",
            "description": "Accessible name of a cell while its toggle request is running."
          },
          {
            "key": "toggleFailed",
            "kind": "function",
            "default": "Couldn't update {row} · {column}. Try again.",
            "signature": "(row: string, column: string)",
            "description": "Status announced when a toggle request fails; the cell keeps its previous state."
          },
          {
            "key": "scrollRegion",
            "kind": "function",
            "default": "{name}, scrollable",
            "signature": "(name: string)",
            "description": "Accessible name of the focusable scroll region around the grid. `name` is the grid's name."
          },
          {
            "key": "rowHeader",
            "kind": "string",
            "default": "Row",
            "description": "Hidden header text of the first column when no `cornerLabel` is given."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-matrix-grid-cell-size",
        "--ds-matrix-grid-color",
        "--ds-matrix-grid-column-header-padding-inline",
        "--ds-matrix-grid-column-width",
        "--ds-matrix-grid-columns",
        "--ds-matrix-grid-count-width",
        "--ds-matrix-grid-label-width",
        "--ds-matrix-grid-padding-inline",
        "--ds-matrix-grid-row-header-gap",
        "--ds-matrix-grid-row-min-height",
        "--ds-matrix-grid-sticky-z-index",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface",
        "--ds-surface-row-hover",
        "--ds-swatch-ring",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-text-inverse",
        "--ds-tracking-caption",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Patterns/MatrixGrid",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-matrix-grid--default",
            "hasPlay": true
          },
          {
            "name": "WithCountAndHint",
            "id": "patterns-matrix-grid--with-count-and-hint",
            "hasPlay": false
          },
          {
            "name": "CheckContrast",
            "id": "patterns-matrix-grid--check-contrast",
            "description": "Bright, dark and mid-tone colors: the check mark switches by WCAG contrast.",
            "hasPlay": false
          },
          {
            "name": "WithoutColors",
            "id": "patterns-matrix-grid--without-colors",
            "hasPlay": false
          },
          {
            "name": "ManyColumns",
            "id": "patterns-matrix-grid--many-columns",
            "description": "The region scrolls horizontally and is focusable and labelled.",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "patterns-matrix-grid--long-labels",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "patterns-matrix-grid--empty",
            "hasPlay": false
          },
          {
            "name": "KeyboardNavigation",
            "id": "patterns-matrix-grid--keyboard-navigation",
            "description": "One tab stop for the grid; arrows, Home/End and Ctrl+Home/End move; Space and Enter toggle.",
            "hasPlay": true
          },
          {
            "name": "AsyncToggle",
            "id": "patterns-matrix-grid--async-toggle",
            "description": "`onToggle` returns a promise: the cell shows a spinner and ignores repeat toggles while it\nruns. \"Brick red\" always fails, so `onToggleError` runs (here it only logs; use a toast).",
            "hasPlay": true
          },
          {
            "name": "StickyHeaders",
            "id": "patterns-matrix-grid--sticky-headers",
            "description": "The header row and the row header stay in view while the region scrolls in both directions.",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "patterns-matrix-grid--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "patterns-matrix-grid--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "patterns-matrix-grid--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/MatrixGrid/MatrixGrid.md",
        "summary": "A rows-by-columns matrix of toggle cells, where each cell links or unlinks one row with one column (for example which colors a product comes in). It is an ARIA `grid` with roving focus and full arrow-key navigation. Import it from `loui-ds/patterns`.\n\n- Handoff source: `components/erp/MatrixGrid.jsx` (TODO `CMP-PAT-11`).",
        "whenToUse": [
          "Editing many-to-many relations where seeing the whole matrix helps (items by options, users by roles, products by regions).",
          "When every cell is a quick on/off decision."
        ],
        "whenNotToUse": [
          "Reading or sorting data: use a table (`DataTable`).",
          "One list of independent switches: use `CheckboxGroup` or `Switch`.",
          "Very large matrices (hundreds of rows): this component renders every cell."
        ],
        "keyboard": [
          "Keyboard: the grid is one tab stop (roving tabindex; the scroll region is the stop before it). `←` `→` `↑` `↓` move one cell (horizontal arrows follow the reading direction and stop at the edges); `Home` / `End` go to the first / last cell of the row; `Ctrl+Home` / `Ctrl+End` to the first / last cell of the grid; `Space` and `Enter` toggle."
        ],
        "examples": [
          "import { MatrixGrid } from 'loui-ds/patterns';\n\n<MatrixGrid\n  aria-label=\"Product colors\"\n  cornerLabel=\"Product\"\n  countLabel=\"Linked\"\n  rows={products}\n  columns={[{ id: 'navy', label: 'Navy', color: '#1f3a93' }]}\n  isLinked={(productId, colorId) => links.has(`${productId}:${colorId}`)}\n  onToggle={toggleLink}\n  hint=\"Select a cell to link or unlink a color.\"\n/>;"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Editing many-to-many relations where seeing the whole matrix helps (items by options, users by roles, products by regions).\n- When every cell is a quick on/off decision."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Reading or sorting data: use a table (`DataTable`).\n- One list of independent switches: use `CheckboxGroup` or `Switch`.\n- Very large matrices (hundreds of rows): this component renders every cell."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  div[role=region] aria-label=\"… scrollable\" tabindex=0      (horizontal scroll)\n    div[role=grid] aria-label aria-describedby=hint\n      div[role=row]\n        columnheader (corner caption) · columnheader (swatch + label) × N · columnheader (count)\n      div[role=row] × rows\n        rowheader (label + meta) · gridcell > button \"{row} · {column}: linked\" × N · gridcell (count)\n  p.hint\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop            | Type                                   | Default      | Notes                                                                      |\n| --------------- | -------------------------------------- | ------------ | -------------------------------------------------------------------------- |\n| `aria-label`    | `string`                               | required     | Names the grid; the scroll region is named \"{label}, scrollable\".          |\n| `rows`          | `{ id, label, meta? }[]`               | required     | Display order is yours (sort alphabetically by default).                   |\n| `columns`       | `{ id, label, color? }[]`              | required     | `color` is `#rgb` / `#rrggbb`; fills linked cells and shows a swatch.      |\n| `isLinked`      | `(rowId, columnId) => boolean`         | required     | Source of truth is yours.                                                  |\n| `onToggle`      | `(rowId, columnId) => void \\| Promise` | required     | Update your state here. A returned promise gives the cell a pending state. |\n| `onToggleError` | `(error, rowId, columnId) => void`     | none         | Runs when the promise from `onToggle` rejects.                             |\n| `cornerLabel`   | `string`                               | hidden \"Row\" | Caption of the row header column.                                          |\n| `countLabel`    | `string`                               | none         | Adds a count column (\"Linked\") with the per-row number of links.           |\n| `hint`          | `ReactNode`                            | none         | Help text under the grid, linked with `aria-describedby`.                  |"
          },
          {
            "id": "async-toggle-sticky-headers-and-focus-guides",
            "heading": "Async toggle, sticky headers and focus guides",
            "content": "- **Async toggle.** `onToggle` may return a promise (saving the link on a server). While it is pending the cell is `aria-busy` (on the `gridcell`), shows a small spinner, is named \"{row} · {column}: linked, saving\" (message `cellPending`) and ignores further toggles; focus stays on it and other cells keep working. The grid never flips a cell by itself: it shows what `isLinked` says. If you update your state optimistically, revert it in `onToggleError`, which runs when the promise rejects (also when `onToggle` throws). Use it for a toast: `onToggleError={() => toast.error('Could not save the link')}`. The grid also announces \"Couldn't update {row} · {column}. Try again.\" in a visually hidden `role=\"status\"` (message `toggleFailed`).\n- **Sticky headers.** The column header row and the row header column are `position: sticky`; the corner sits above both. For vertical scrolling give the root a height or `max-block-size` through `className` / `style` (the scroll region fills it). `scroll-padding` keeps a focused cell out from behind the sticky parts (WCAG 2.4.11).\n- **Focus guides.** While a cell has focus, its row, its column and the column header get one surface step (`--ds-surface-row-hover`). It is a visual aid only: the focus ring and the cell name carry the meaning, nothing changes in the accessibility tree."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Semantics: `region` (focusable scroll container) > `grid` > `row` > `columnheader` / `rowheader` / `gridcell`. Each cell holds a native button named \"{row} · {column}: linked\" or \"not linked\" (messages, `en` and `pt-BR`); the state is in the name, so there is no separate `aria-pressed`.\n- Keyboard: the grid is one tab stop (roving tabindex; the scroll region is the stop before it). `←` `→` `↑` `↓` move one cell (horizontal arrows follow the reading direction and stop at the edges); `Home` / `End` go to the first / last cell of the row; `Ctrl+Home` / `Ctrl+End` to the first / last cell of the grid; `Space` and `Enter` toggle.\n- Linked state is not color alone: linked cells are solid with a check, unlinked cells are dashed with a plus; the name says it too.\n- The check color is chosen with `readableTextOn` (WCAG relative luminance) on the column color, and the choice is tested at 3:1 for a set of colors.\n- Targets are 40x40px (the minimum is 24px). Unlinked cell borders use `--ds-line-control` (3:1).\n- Motion: the hover scale (1.06) exists only inside `@media (hover: hover) and (prefers-reduced-motion: no-preference)`.\n- Forced colors: borders use `ButtonText`, linked cells `Highlight`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Row and column labels are short nouns in sentence case; they are read in every cell name, so keep them unique within their axis.\n- `countLabel` names what is counted (\"Linked\", \"Colors\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nimport { MatrixGrid } from 'loui-ds/patterns';\n\n<MatrixGrid\n  aria-label=\"Product colors\"\n  cornerLabel=\"Product\"\n  countLabel=\"Linked\"\n  rows={products}\n  columns={[{ id: 'navy', label: 'Navy', color: '#1f3a93' }]}\n  isLinked={(productId, colorId) => links.has(`${productId}:${colorId}`)}\n  onToggle={toggleLink}\n  hint=\"Select a cell to link or unlink a color.\"\n/>;\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Plain `div`s with `aria-pressed` buttons become an ARIA grid with arrow-key navigation and roving focus; the handoff had only Tab through every cell.\n- The mouse-only Portuguese `title` tooltip is replaced by an accessible name from messages.\n- The 0.299/0.587/0.114 luma shortcut is replaced by `readableTextOn` (WCAG 2.x contrast).\n- The scroll container is a labelled, focusable region.\n- Sizes (cell 40, column 64, label 220, count 64, row 60) are tokens `--ds-matrix-grid-*`; `labelWidth` and `cell` props are gone. Column colors are applied via the `--ds-matrix-grid-color` custom property, not inline fills.\n- The hover scale respects reduced motion. Column headers use a decorative `Swatch`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Reading or sorting data: use a table (`DataTable`)."
          },
          {
            "name": "CheckboxGroup",
            "reason": "One list of independent switches: use `CheckboxGroup` or `Switch`."
          },
          {
            "name": "Switch",
            "reason": "One list of independent switches: use `CheckboxGroup` or `Switch`."
          }
        ],
        "uses": ["Icon", "Spinner", "Swatch", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Menu",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Menu } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A menu of commands that opens from a trigger, built on React Aria `MenuTrigger`, `Popover`\nand `Menu`.\n\nKeys: the trigger opens it with Enter, Space or Arrow Down (focus goes to the first item;\nArrow Up focuses the last); Arrow Up/Down, Home/End and typeahead move; Enter or Space\nactivates; Escape closes and focus returns to the trigger. Selection modes\n(`selectionMode=\"single\"` or `\"multiple\"`) turn items into radio or checkbox items.",
      "props": [
        {
          "name": "trigger",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": true,
          "description": "The element that opens the menu: a `Button` or `IconButton`. It receives `aria-haspopup`,\n`aria-expanded` and names the menu (`aria-labelledby`), so give it a clear name."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "`MenuItem`, `MenuSection`, `MenuSeparator` and `SubMenu` elements."
        },
        {
          "name": "placement",
          "type": "'bottom' | 'bottom left' | 'bottom right' | 'bottom start' | 'bottom end' | 'top' | 'top left' | 'top right' | 'top start' | 'top end' | 'left' | 'left top' | 'left bottom' | 'start' | 'start top' | 'start bottom' | 'right' | 'right top' | 'right bottom' | 'end' | 'end top' | 'end bottom'",
          "required": false,
          "default": "'bottom start'",
          "description": "Side and alignment relative to the trigger. Default `bottom start`. Flips on collision.",
          "values": [
            "bottom",
            "bottom left",
            "bottom right",
            "bottom start",
            "bottom end",
            "top",
            "top left",
            "top right",
            "top start",
            "top end",
            "left",
            "left top",
            "left bottom",
            "start",
            "start top",
            "start bottom",
            "right",
            "right top",
            "right bottom",
            "end",
            "end top",
            "end bottom"
          ]
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the menu opens or closes."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the menu list."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the menu."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the menu list element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaMenuProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "escapeKeyBehavior",
              "type": "'clearSelection' | 'none'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            },
            {
              "name": "items",
              "type": "Iterable<object>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "CollectionProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "dependencies",
              "type": "readonly any[]",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean | FocusStrategy",
              "required": false
            },
            {
              "name": "onAction",
              "type": "(key: Key, value: object) => void",
              "required": false
            },
            {
              "name": "onClose",
              "type": "() => void",
              "required": false
            },
            {
              "name": "shouldFocusWrap",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "renderEmptyState",
              "type": "() => ReactNode",
              "required": false
            },
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "MultipleSelection",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "defaultSelectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "disallowEmptySelection",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onSelectionChange",
              "type": "(keys: Selection) => void",
              "required": false
            },
            {
              "name": "selectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "selectionMode",
              "type": "'none' | 'single' | 'multiple'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Menu",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "moreActions",
            "kind": "string",
            "default": "More actions",
            "description": "Name of the overflow button of `MoreActionsMenu`."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "MenuItem",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { MenuItem } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "One command. Inside a menu with `selectionMode` it becomes a checkbox or radio item and shows\nits selected state with a check or a dot. Disabled items are dimmed and skipped by keyboard\nnavigation.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The label: a verb plus object in sentence case (\"Duplicate order\")."
        },
        {
          "name": "tone",
          "type": "'neutral' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "`danger` marks a destructive command (red text and tint). It does not confirm anything:\nopen a `ConfirmDialog` from `onAction`.",
          "values": ["neutral", "danger"]
        },
        {
          "name": "shortcut",
          "type": "string",
          "required": false,
          "description": "Shortcut hint in mono type (\"Ctrl+D\"). Visual only; it does not bind the key."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "LinkDOMProps",
          "domGeneric": false,
          "count": 8,
          "props": [
            {
              "name": "download",
              "type": "string | boolean",
              "required": false
            },
            {
              "name": "href",
              "type": "string",
              "required": false
            },
            {
              "name": "hrefLang",
              "type": "string",
              "required": false
            },
            {
              "name": "ping",
              "type": "string",
              "required": false
            },
            {
              "name": "referrerPolicy",
              "type": "'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'",
              "required": false
            },
            {
              "name": "rel",
              "type": "string",
              "required": false
            },
            {
              "name": "routerOptions",
              "type": "never",
              "required": false
            },
            {
              "name": "target",
              "type": "HTMLAttributeAnchorTarget",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "MenuItemProps",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "id",
              "type": "Key",
              "required": false
            },
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onAction",
              "type": "() => void",
              "required": false
            },
            {
              "name": "textValue",
              "type": "string",
              "required": false
            },
            {
              "name": "value",
              "type": "object",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {
        "tone": ["neutral", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "MenuSection",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { MenuSection } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A group of items with an optional caption. Give a section its own `selectionMode` to make a\nradio group or a set of checkbox items inside a menu that also holds plain commands.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "`MenuItem` elements."
        },
        {
          "name": "title",
          "type": "string",
          "required": false,
          "description": "Caption of the group, announced as its name. Mono uppercase by CSS. Omit it for an unnamed group."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "items",
              "type": "Iterable<object>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "CollectionProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "dependencies",
              "type": "readonly any[]",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "MultipleSelection",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "defaultSelectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "disallowEmptySelection",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onSelectionChange",
              "type": "(keys: Selection) => void",
              "required": false
            },
            {
              "name": "selectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "selectionMode",
              "type": "'none' | 'single' | 'multiple'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SectionProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "id",
              "type": "Key",
              "required": false
            },
            {
              "name": "value",
              "type": "object",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "SectionProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "MenuSeparator",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { MenuSeparator } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A 1px line between groups of items.",
      "props": [
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "Meter",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Meter } from 'loui-ds';",
      "folder": "src/components/Progress",
      "sourceFile": "src/components/Progress/Progress.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A bar that shows a quantity within a known range (disk used, quota, score), as opposed to\nprogress of a task. Built on React Aria `Meter` (`role=\"meter\"`).",
      "props": [
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label, wired to the bar with `aria-labelledby`. Required unless you set `aria-label`\nor `aria-labelledby`: a bar without a name is an accessibility bug (React Aria warns in development)."
        },
        {
          "name": "meta",
          "type": "ReactNode",
          "required": false,
          "description": "Text on the right of the label, in mono type: \"6.2 / 10 GB\". It is visual only. If it says\nmore than the percentage, also pass `valueLabel` so screen readers hear the same thing."
        },
        {
          "name": "showValue",
          "type": "boolean",
          "required": false,
          "description": "Shows the formatted percentage (`62%`, from `Intl` with the active locale) as `meta` when `meta` is not given."
        },
        {
          "name": "tone",
          "type": "'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'accent'",
          "description": "Color of the fill. Color is never the only signal: pair it with a label or `meta`. Default `accent`.",
          "values": ["accent", "success", "warning", "danger"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ProgressBarBaseProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "formatOptions",
              "type": "NumberFormatOptions",
              "required": false
            },
            {
              "name": "maxValue",
              "type": "number",
              "required": false
            },
            {
              "name": "minValue",
              "type": "number",
              "required": false
            },
            {
              "name": "value",
              "type": "number",
              "required": false
            },
            {
              "name": "valueLabel",
              "type": "ReactNode",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "tone": ["accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-duration-progress",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-progress-height",
        "--ds-progress-indeterminate-duration",
        "--ds-progress-indeterminate-width",
        "--ds-radius-pill",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-success",
        "--ds-surface-hover",
        "--ds-text-2",
        "--ds-warning"
      ],
      "stories": {
        "title": "Components/Progress",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-progress--default",
            "hasPlay": false
          },
          {
            "name": "WithoutLabel",
            "id": "components-progress--without-label",
            "hasPlay": false
          },
          {
            "name": "ShowValue",
            "id": "components-progress--show-value",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-progress--empty",
            "hasPlay": false
          },
          {
            "name": "Complete",
            "id": "components-progress--complete",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-progress--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-progress--danger",
            "hasPlay": false
          },
          {
            "name": "Indeterminate",
            "id": "components-progress--indeterminate",
            "hasPlay": false
          },
          {
            "name": "AllTones",
            "id": "components-progress--all-tones",
            "hasPlay": false
          },
          {
            "name": "AsMeter",
            "id": "components-progress--as-meter",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-progress--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Progress/Progress.md",
        "summary": "A thin bar (4 px) with a label and a value in mono type. `Progress` shows the progress of a task, determinate or indeterminate (React Aria `ProgressBar`, `role=\"progressbar\"`). `Meter` shows a quantity within a range (React Aria `Meter`, `role=\"meter\"`) and shares the same look.",
        "whenToUse": [
          "`Progress`: an upload, an import, a multi-step job. Use indeterminate when the amount is unknown.",
          "`Meter`: disk used, quota consumed, a score. The value is a measurement, not a task that will finish."
        ],
        "whenNotToUse": [
          "Waiting with no known structure and no label: use `Spinner`.",
          "Content that is loading: use `Skeleton`."
        ],
        "keyboard": [],
        "examples": [
          "<Progress value={62} label=\"Storage\" meta=\"6.2 / 10 GB\" valueLabel=\"6.2 of 10 GB\" />\n<Progress isIndeterminate label=\"Importing orders\" />\n<Meter value={82} tone=\"warning\" label=\"Quota\" showValue />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- `Progress`: an upload, an import, a multi-step job. Use indeterminate when the amount is unknown.\n- `Meter`: disk used, quota consumed, a score. The value is a measurement, not a task that will finish."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Waiting with no known structure and no label: use `Spinner`.\n- Content that is loading: use `Skeleton`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=progressbar|meter]\n  div.header      Label (left) · meta (right, mono)\n  div.track       4px, pill\n    div.fill      width = percentage (or a sweeping segment when indeterminate)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                            | Type                                             | Default    | Notes                                                                                              |\n| ------------------------------- | ------------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------- |\n| `value`                         | `number`                                         | `0`        | Clamped to the range                                                                               |\n| `minValue`, `maxValue`          | `number`                                         | `0`, `100` |                                                                                                    |\n| `isIndeterminate`               | `boolean` (`Progress` only)                      |            | No `aria-valuenow`; the fill sweeps                                                                |\n| `label`                         | `ReactNode`                                      |            | Visible label, wired as the accessible name                                                        |\n| `aria-label`, `aria-labelledby` | `string`                                         |            | Use when there is no visible label. One of the three is required (React Aria warns in development) |\n| `meta`                          | `ReactNode`                                      |            | Visual text on the right (\"6.2 / 10 GB\")                                                           |\n| `showValue`                     | `boolean`                                        | `false`    | Shows the formatted percentage as `meta`; hidden when indeterminate                                |\n| `valueLabel`                    | `ReactNode`                                      |            | The spoken value (`aria-valuetext`). Pass it when `meta` says more than a percentage               |\n| `formatOptions`                 | `Intl.NumberFormatOptions`                       | percent    | Formatting of the automatic value text                                                             |\n| `tone`                          | `'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'accent'` | Fill color                                                                                         |\n\nOther React Aria props, `className` and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The bar is named by the visible label (`aria-labelledby`) or `aria-label`; React Aria warns in development when a bar has neither.\n- `aria-valuenow`, `aria-valuemin` and `aria-valuemax` are set, and `aria-valuetext` is the percentage formatted with `Intl` for the active locale (\"62%\", \"62,5%\" in pt-BR). If you show \"6.2 / 10 GB\", set `valueLabel=\"6.2 of 10 GB\"` so screen readers hear the same thing.\n- An indeterminate bar has no `aria-valuenow`, so assistive technology reports it as busy without a number.\n- Progress does not announce changes by itself. For long tasks, announce milestones and completion in a live region (for example with a `Toast`).\n- Color is not the only signal: the label and the value text carry the meaning. The fill is 4 px high but the track and fill are a graphic that keeps at least 3:1 against the surface; the track is a neutral surface step.\n- **Reduced motion:** the width change has no transition and the indeterminate sweep becomes a static segment centered in the track. **Forced colors:** the track gets a border and the fill uses the text color."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns or short phrases in sentence case (\"Storage\", \"Importing orders\").\n- Put units and ratios in `meta`, in the formatted form of the active locale."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Progress value={62} label=\"Storage\" meta=\"6.2 / 10 GB\" valueLabel=\"6.2 of 10 GB\" />\n<Progress isIndeterminate label=\"Importing orders\" />\n<Meter value={82} tone=\"warning\" label=\"Quota\" showValue />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria: `role`, name, `aria-valuetext` and the label wiring come from it. The handoff `div role=\"progressbar\"` had no name.\n- Indeterminate mode, `valueLabel`, `showValue` and `Meter` are new.\n- Width changes use the motion tokens and are disabled under reduced motion. Height and sweep settings are component tokens (`--ds-progress-*`).\n- `Progress` no longer takes a `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Spinner",
            "reason": "Waiting with no known structure and no label: use `Spinner`."
          },
          {
            "name": "Skeleton",
            "reason": "Content that is loading: use `Skeleton`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "MoreActionsMenu",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { MoreActionsMenu } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The common \"more actions\" menu: an icon button with three dots whose name comes from the\nmessage catalog (\"More actions\"), opening a `Menu`. Use it for the row or card actions that\ndo not deserve their own button.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "`MenuItem`, `MenuSection`, `MenuSeparator` and `SubMenu` elements."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Size of the trigger button. Default `md`.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "messages",
          "type": "Partial<{ moreActions: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the menu list."
        },
        {
          "name": "placement",
          "type": "'bottom' | 'bottom left' | 'bottom right' | 'bottom start' | 'bottom end' | 'top' | 'top left' | 'top right' | 'top start' | 'top end' | 'left' | 'left top' | 'left bottom' | 'start' | 'start top' | 'start bottom' | 'right' | 'right top' | 'right bottom' | 'end' | 'end top' | 'end bottom'",
          "required": false,
          "default": "'bottom end'",
          "description": "Side and alignment relative to the trigger. Default `bottom start`. Flips on collision.",
          "values": [
            "bottom",
            "bottom left",
            "bottom right",
            "bottom start",
            "bottom end",
            "top",
            "top left",
            "top right",
            "top start",
            "top end",
            "left",
            "left top",
            "left bottom",
            "start",
            "start top",
            "start bottom",
            "right",
            "right top",
            "right bottom",
            "end",
            "end top",
            "end bottom"
          ]
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the menu opens or closes."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the menu."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the menu list element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaMenuProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "escapeKeyBehavior",
              "type": "'clearSelection' | 'none'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            },
            {
              "name": "items",
              "type": "Iterable<object>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "CollectionProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "dependencies",
              "type": "readonly any[]",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean | FocusStrategy",
              "required": false
            },
            {
              "name": "onAction",
              "type": "(key: Key, value: object) => void",
              "required": false
            },
            {
              "name": "onClose",
              "type": "() => void",
              "required": false
            },
            {
              "name": "shouldFocusWrap",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "MenuProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "renderEmptyState",
              "type": "() => ReactNode",
              "required": false
            },
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "MultipleSelection",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "defaultSelectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "disallowEmptySelection",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onSelectionChange",
              "type": "(keys: Selection) => void",
              "required": false
            },
            {
              "name": "selectedKeys",
              "type": "Iterable<Key> | \"all\"",
              "required": false
            },
            {
              "name": "selectionMode",
              "type": "'none' | 'single' | 'multiple'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Menu",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "moreActions",
            "kind": "string",
            "default": "More actions",
            "description": "Name of the overflow button of `MoreActionsMenu`."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": ["ActionList"]
      }
    },
    {
      "name": "NavItem",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { NavItem } from 'loui-ds';",
      "folder": "src/components/NavItem",
      "sourceFile": "src/components/NavItem/NavItem.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "One entry of a navigation list. A real link when it has an `href`, a button when it only has\n`onPress`. The active item is marked with `aria-current=\"page\"`, a raised background and a\n2px ink bar at the start edge.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "Label: a noun in sentence case (\"Orders\"). Truncated with an ellipsis when it does not fit."
        },
        {
          "name": "href",
          "type": "string",
          "required": false,
          "description": "Destination. With `href` the item is a real link (`<a>`) and navigates through the router\nset on `DSProvider`. Without it the item is a `<button>` that runs `onPress`."
        },
        {
          "name": "icon",
          "type": "LucideIcon",
          "required": false,
          "description": "Icon from the curated set, drawn at 18px before the label. Decorative."
        },
        {
          "name": "isActive",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Marks the item for the page or view being shown: sets `aria-current=\"page\"`, the raised\nbackground and the 2px ink bar. Exactly one item of a navigation should be active."
        },
        {
          "name": "count",
          "type": "string | number",
          "required": false,
          "description": "A count shown in a pill at the end of the item. `0` shows no pill. Numbers are formatted\nfor the active locale."
        },
        {
          "name": "countLabel",
          "type": "string",
          "required": false,
          "description": "The text a screen reader hears instead of the bare number, with what is being counted\n(\"3 pending\", \"12 unread\"). Always set it together with `count`; without it a generic\n\"Count: 3\" from the message catalog is used."
        },
        {
          "name": "countTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Tone of the count pill. Color never carries the meaning alone: `countLabel` does.\nDefault `neutral`.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "messages",
          "type": "Partial<{ count: (value: string) => string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the underlying `<a>` (with `href`) or `<button>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaLinkOptions",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "LinkDOMProps",
          "domGeneric": false,
          "count": 7,
          "props": [
            {
              "name": "download",
              "type": "string | boolean",
              "required": false
            },
            {
              "name": "hrefLang",
              "type": "string",
              "required": false
            },
            {
              "name": "ping",
              "type": "string",
              "required": false
            },
            {
              "name": "referrerPolicy",
              "type": "'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'",
              "required": false
            },
            {
              "name": "rel",
              "type": "string",
              "required": false
            },
            {
              "name": "routerOptions",
              "type": "never",
              "required": false
            },
            {
              "name": "target",
              "type": "HTMLAttributeAnchorTarget",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "PossibleLinkDOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "(props: DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement> | DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, renderProps: LinkRenderProps) => ReactElement<...>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "NavItem",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "count",
            "kind": "function",
            "default": "Count: {value}",
            "signature": "(value: string)",
            "description": "Spoken text for a count when the consumer gives no `countLabel`."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-small",
        "--ds-nav-item-count-padding-inline",
        "--ds-nav-item-count-size",
        "--ds-nav-item-icon-opacity",
        "--ds-nav-item-indicator-width",
        "--ds-nav-item-padding-inline",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-3",
        "--ds-success",
        "--ds-success-soft",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/NavItem",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-nav-item--default",
            "hasPlay": true
          },
          {
            "name": "Active",
            "id": "components-nav-item--active",
            "hasPlay": true
          },
          {
            "name": "AsButton",
            "id": "components-nav-item--as-button",
            "hasPlay": true
          },
          {
            "name": "WithoutIcon",
            "id": "components-nav-item--without-icon",
            "hasPlay": false
          },
          {
            "name": "WithCount",
            "id": "components-nav-item--with-count",
            "hasPlay": true
          },
          {
            "name": "CountTones",
            "id": "components-nav-item--count-tones",
            "hasPlay": false
          },
          {
            "name": "ZeroCountHidden",
            "id": "components-nav-item--zero-count-hidden",
            "description": "A count of 0 shows no pill.",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-nav-item--disabled",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-nav-item--long-label",
            "hasPlay": false
          },
          {
            "name": "RouterNavigation",
            "id": "components-nav-item--router-navigation",
            "description": "With DSProvider routing, pressing the link calls navigate instead of reloading the page.",
            "hasPlay": true
          },
          {
            "name": "PortugueseCount",
            "id": "components-nav-item--portuguese-count",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-nav-item--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-nav-item--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/NavItem/NavItem.md",
        "summary": "One entry of a navigation list: a real link (or a button) with an optional icon and count. Built on React Aria `Link` and `Button`.\n\n- Handoff source: `components/navigation/NavItem.jsx` (TODO `CMP-NAV-03`).",
        "whenToUse": [
          "Inside the `Sidebar`, or any vertical navigation list.",
          "To go to a page or view (with `href`), or to run a navigation-like command such as \"Log out\" (with `onPress`)."
        ],
        "whenNotToUse": [
          "For actions inside a page: use `Button`.",
          "For switching panels on a page: use `Tabs`.",
          "For a link inside text: use `Link`."
        ],
        "keyboard": [
          "Keys: `Tab` to focus; `Enter` follows a link; `Enter` and `Space` press a button."
        ],
        "examples": [
          "<nav aria-label=\"Main\">\n  <NavItem\n    href=\"/orders\"\n    icon={StarIcon}\n    isActive\n    count={3}\n    countLabel=\"3 pending\"\n    countTone=\"warning\"\n  >\n    Orders\n  </NavItem>\n  <NavItem href=\"/customers\" icon={SearchIcon}>\n    Customers\n  </NavItem>\n  <NavItem onPress={signOut}>Log out</NavItem>\n</nav>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Inside the `Sidebar`, or any vertical navigation list.\n- To go to a page or view (with `href`), or to run a navigation-like command such as \"Log out\" (with `onPress`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For actions inside a page: use `Button`.\n- For switching panels on a page: use `Tabs`.\n- For a link inside text: use `Link`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\na | button .root           (36px high, --ds-item-h)\n  Icon                     (18px, optional, 0.7 opacity when inactive)\n  label                    (truncates with an ellipsis)\n  count                    (optional pill with tone)\n    \"3\"                    (aria-hidden)\n    \"3 pending\"            (visually hidden text alternative)\n```"
          },
          {
            "id": "states",
            "heading": "States",
            "content": "| State    | Look                                                                          | Semantics             |\n| -------- | ----------------------------------------------------------------------------- | --------------------- |\n| Rest     | transparent, `--ds-text-2`                                                    | link or button        |\n| Hover    | raised background (`--ds-surface-raised`), ink text                           | none                  |\n| Active   | raised background, ink text, 2px ink bar at the start edge, full-opacity icon | `aria-current=\"page\"` |\n| Focus    | accent ring                                                                   |                       |\n| Disabled | `--ds-opacity-disabled`                                                       | `aria-disabled`       |"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                                                          | Default     | Notes                                                                               |\n| ------------ | ------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------- |\n| `children`   | `ReactNode`                                                   | required    | The label.                                                                          |\n| `href`       | `string`                                                      | none        | With it: a real `<a>` routed through `DSProvider`. Without it: a `<button>`.        |\n| `icon`       | `IconComponent`                                               | none        | From the curated set. Drawn at `--ds-icon-md` (18px), decorative.                   |\n| `isActive`   | `boolean`                                                     | `false`     | Sets `aria-current=\"page\"` and the active look. One per navigation.                 |\n| `count`      | `number \\| string`                                            | none        | Pill at the end. `0` shows nothing. Numbers are formatted for the locale.           |\n| `countLabel` | `string`                                                      | catalog     | Spoken text for the count (\"3 pending\"). Always set it; the fallback is \"Count: 3\". |\n| `countTone`  | `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'neutral'` | Tone of the pill.                                                                   |\n| `onPress`    | `(e: PressEvent) => void`                                     | none        | Fired for both links and buttons.                                                   |\n| `isDisabled` | `boolean`                                                     | `false`     |                                                                                     |\n| `messages`   | `{ count?: (value: string) => string }`                       | catalog     | Per-instance override of the fallback count text (ADR-0005).                        |\n\n`ref` points to the `<a>` or `<button>`. Other React Aria `Link` props (`target`, `routerOptions`, `aria-*`) pass through for links."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Real `<a href>` (or `<button>`), so opening in a new tab, copying the URL and screen reader link lists work. The handoff used `div role=\"link\"` with no `href`.\n- `aria-current=\"page\"` on the active item, on links and on buttons.\n- The count pill is not enough on its own: a screen reader hears the label then the text alternative (\"Orders 3 pending\"). Color (tone) is only reinforcement.\n- Keys: `Tab` to focus; `Enter` follows a link; `Enter` and `Space` press a button.\n- Contrast: label `--ds-text-2` on `--ds-bg`, ink on the raised surface. The pill text meets 4.5:1 on its soft background.\n- Forced colors: the active bar and the pill border use system colors."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns in sentence case: \"Orders\", \"Customers\". Long labels are truncated.\n- `countLabel` states what is counted and its state: \"3 pending\", \"12 unread\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<nav aria-label=\"Main\">\n  <NavItem\n    href=\"/orders\"\n    icon={StarIcon}\n    isActive\n    count={3}\n    countLabel=\"3 pending\"\n    countTone=\"warning\"\n  >\n    Orders\n  </NavItem>\n  <NavItem href=\"/customers\" icon={SearchIcon}>\n    Customers\n  </NavItem>\n  <NavItem onPress={signOut}>Log out</NavItem>\n</nav>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `div role=\"link\"` becomes a real `<a>` (router-aware through `DSProvider`) or a `<button>`; `label` became `children`, `active` became `isActive`, `onClick` became `onPress`.\n- Icons are `Icon` components (the handoff fetched SVGs from unpkg); the inactive opacity (.7) is the token `--ds-nav-item-icon-opacity`.\n- Hover is CSS. The active bar is a pseudo-element (logical, mirrors in RTL) instead of `box-shadow`, so it combines with the focus ring.\n- Count pills have a text alternative and localized numbers."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Button",
            "reason": "For actions inside a page: use `Button`."
          },
          {
            "name": "Tabs",
            "reason": "For switching panels on a page: use `Tabs`."
          },
          {
            "name": "Link",
            "reason": "For a link inside text: use `Link`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": ["Sidebar"]
      }
    },
    {
      "name": "NumberField",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { NumberField } from 'loui-ds';",
      "folder": "src/components/NumberField",
      "sourceFile": "src/components/NumberField/NumberField.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A number field with minus and plus steppers (the handoff `Stepper`), built on React Aria\n`NumberField`: a text `<input>` with `aria-roledescription=\"Number field\"` (React Aria\ndeliberately drops `role=\"spinbutton\"`: VoiceOver cannot focus a spin button with typed\ntext). You can type a value or step it: `ArrowUp` / `ArrowDown` by `step`,\n`PageUp` / `PageDown` like the arrows (React Aria has no larger step), `Home` / `End` to the\nlimits (when `minValue` / `maxValue` are set), the mouse wheel while focused, and press and hold on a button repeats.\nThe typed text is parsed with the locale of `DSProvider` (`1.234,5` in pt-BR) and committed on\nblur or `Enter`; values are clamped to the limits and snapped to `step`.\n\n`onChange` receives a number, `NaN` while the field is empty. For money use `CurrencyInput`.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control, linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field is empty. Never a substitute for the label."
        },
        {
          "name": "suffix",
          "type": "ReactNode",
          "required": false,
          "description": "A unit shown after the number inside the box (\"m\", \"kg\", \"pcs\"). Visual only: for a spoken\nunit use `formatOptions={{ style: 'unit', unit: 'meter' }}`, which also formats the number."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Height of the box and of both stepper buttons: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<input>` element."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ decrease: string; increase: string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `decrease`, `increase`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaNumberFieldProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "decrementAriaLabel",
              "type": "string",
              "required": false
            },
            {
              "name": "incrementAriaLabel",
              "type": "string",
              "required": false
            },
            {
              "name": "isWheelDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "NumberFieldProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "commitBehavior",
              "type": "'validate' | 'snap'",
              "required": false
            },
            {
              "name": "formatOptions",
              "type": "NumberFormatOptions",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "RangeInputBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "maxValue",
              "type": "number",
              "required": false
            },
            {
              "name": "minValue",
              "type": "number",
              "required": false
            },
            {
              "name": "step",
              "type": "number",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: number) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "number",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: number) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "number",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "NumberField",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "decrease",
            "kind": "string",
            "default": "Decrease",
            "description": "Accessible name of the \"minus\" stepper button."
          },
          {
            "key": "increase",
            "kind": "string",
            "default": "Increase",
            "description": "Accessible name of the \"plus\" stepper button."
          }
        ]
      },
      "cssVariables": [
        "--ds-field-control-padding-inline",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-number-field-value-min-width",
        "--ds-space-1",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/NumberField",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-number-field--default",
            "hasPlay": true
          },
          {
            "name": "KeyboardContract",
            "id": "components-number-field--keyboard-contract",
            "hasPlay": true
          },
          {
            "name": "TypingClampsToLimits",
            "id": "components-number-field--typing-clamps-to-limits",
            "hasPlay": true
          },
          {
            "name": "WithSuffix",
            "id": "components-number-field--with-suffix",
            "hasPlay": false
          },
          {
            "name": "FormattedUnit",
            "id": "components-number-field--formatted-unit",
            "hasPlay": false
          },
          {
            "name": "WithDescription",
            "id": "components-number-field--with-description",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-number-field--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-number-field--invalid",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-number-field--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-number-field--read-only",
            "hasPlay": true
          },
          {
            "name": "AtLimit",
            "id": "components-number-field--at-limit",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-number-field--sizes",
            "hasPlay": false
          },
          {
            "name": "Controlled",
            "id": "components-number-field--controlled",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-number-field--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/NumberField/NumberField.md",
        "summary": "A number input with minus and plus steppers; the successor of the handoff `Stepper`. Built on React Aria `NumberField` (`Group`, `Input`, two `Button`s in the `decrement` / `increment` slots), styled like [Input](../Input/Input.md). Task: CMP-PAT-06.",
        "whenToUse": [
          "A bounded or unbounded count or measure that users type or nudge: quantity, length, number of copies."
        ],
        "whenNotToUse": [
          "Money: `CurrencyInput` (integer minor units, currency formatting).",
          "An approximate value on a range: `Slider`.",
          "Digits that are an identifier (postal code, phone): `Input` with `mono`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A bounded or unbounded count or measure that users type or nudge: quantity, length, number of copies."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Money: `CurrencyInput` (integer minor units, currency formatting).\n- An approximate value on a range: `Slider`.\n- Digits that are an identifier (postal code, phone): `Input` with `mono`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "`[ - ] [ value + suffix ] [ + ]`: three boxes of the same height (`sm` 32 / `md` 40 / `lg` 48). The middle box is the `Input` look (`--ds-line-control` border, focus ring, mono, centered); the steppers are `IconButton`s."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                          | Notes                                                                                                |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`                    | One is required.                                                                                     |\n| `value` / `defaultValue` / `onChange(value)`                  | RAC naming. `onChange` receives a `number`, `NaN` while the field is empty.                          |\n| `minValue`, `maxValue`, `step`                                | Typed and stepped values are clamped and snapped to `step`.                                          |\n| `formatOptions`                                               | `Intl.NumberFormat` options (`{ style: 'unit', unit: 'kilogram' }`, `{ maximumFractionDigits: 2 }`). |\n| `suffix`                                                      | A visual unit after the number (\"m\", \"pcs\"). For a spoken unit use `formatOptions`.                  |\n| `size`                                                        | `sm` / `md` (default) / `lg`; the steppers follow.                                                   |\n| `fullWidth`                                                   | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.   |\n| `description`, `descriptionTone`, `errorMessage`              | As in `Input`.                                                                                       |\n| `name`, `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | Standard. `name` submits the raw number (`1234.5`), not the formatted text.                          |\n| `messages`                                                    | `decrease`, `increase`, `requiredMark`.                                                              |"
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                       | Behavior                                                                  |\n| ------------------------- | ------------------------------------------------------------------------- |\n| `ArrowUp` / `ArrowDown`   | Step up / down                                                            |\n| `PageUp` / `PageDown`     | Same as the arrows (React Aria has no larger step)                        |\n| `Home` / `End`            | `minValue` / `maxValue` (only when set)                                   |\n| Typing                    | Parsed with the locale (`1.234,5` in pt-BR), committed on `Enter` or blur |\n| Press and hold a stepper  | Repeats (React Aria)                                                      |\n| Mouse wheel while focused | Steps                                                                     |\n\nThe input is a text input with `aria-roledescription=\"Number field\"`: React Aria deliberately does not use `role=\"spinbutton\"` because VoiceOver cannot focus a spin button that accepts typed text. The stepper buttons are named \"Decrease\" / \"Increase\" from messages and, through `aria-labelledby`, followed by the field label (\"Increase Quantity\"); they are out of the tab order (the input handles the keys) and disabled at the limits. The value is not a live region (RAC announces changes)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the noun (\"Quantity\"). Put the unit in `suffix` or `formatOptions`, not in the label. Error text says how to fix: \"Enter a quantity between 0 and 10.\""
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<NumberField label=\"Quantity\" name=\"quantity\" defaultValue={1} minValue={1} maxValue={99} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `Stepper` became `NumberField`: direct typing, locale parsing, press-and-hold, min/max/step, form `name`, label wiring, description and error, sizes.\n- The `aria-live` on the value was dropped.\n- Button labels come from messages (en \"Decrease\" / \"Increase\", pt-BR \"Diminuir\" / \"Aumentar\").\n- No `role=\"spinbutton\"` (see Accessibility); the handoff `+` and `-` keys do not step."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "CurrencyInput",
            "reason": "Money: `CurrencyInput` (integer minor units, currency formatting)."
          },
          {
            "name": "Slider",
            "reason": "An approximate value on a range: `Slider`."
          },
          {
            "name": "Input",
            "reason": "Digits that are an identifier (postal code, phone): `Input` with `mono`."
          }
        ],
        "uses": ["FieldMessages", "IconButton", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "Pagination",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Pagination } from 'loui-ds';",
      "folder": "src/components/Pagination",
      "sourceFile": "src/components/Pagination/Pagination.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Previous and next buttons, numbered page buttons with ellipses, and a text that says where\nyou are (\"11–20 of 42\", or \"Page 2 of 5\" when only the page count is known).\n\nThe text is a polite live region, so a screen reader announces the new range when the page\nchanges. The current page button has `aria-current=\"page\"`. The buttons that cannot be used\n(previous on page 1) are disabled; when pressing a button disables it, focus moves to the\ncurrent page button so it is never lost.",
      "props": [
        {
          "name": "page",
          "type": "number",
          "required": false,
          "description": "Current page, 1-based (controlled)."
        },
        {
          "name": "defaultPage",
          "type": "number",
          "required": false,
          "default": "1",
          "description": "Page shown first (uncontrolled). Default `1`."
        },
        {
          "name": "onChange",
          "type": "(page: number) => void",
          "required": false,
          "description": "Called with the new page when the user picks one."
        },
        {
          "name": "pageCount",
          "type": "number",
          "required": false,
          "description": "Number of pages. Optional when `total` and `pageSize` are given (then it is derived from\nthem). Default `1`."
        },
        {
          "name": "total",
          "type": "number",
          "required": false,
          "description": "Total number of items. With `pageSize` it enables the \"1–10 of 42\" range text."
        },
        {
          "name": "pageSize",
          "type": "number",
          "required": false,
          "description": "Items per page."
        },
        {
          "name": "messages",
          "type": "Partial<{ navLabel: string; previous: string; next: string; page: (page: string) => string; range: (from: string, to: string, total: string) => string; empty: string; pageOf: (page: string, count: string) => string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Pagination",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "navLabel",
            "kind": "string",
            "default": "Pagination",
            "description": "Accessible name of the pagination navigation landmark."
          },
          {
            "key": "previous",
            "kind": "string",
            "default": "Previous page"
          },
          {
            "key": "next",
            "kind": "string",
            "default": "Next page"
          },
          {
            "key": "page",
            "kind": "function",
            "default": "Page {page}",
            "signature": "(page: string)",
            "description": "Accessible name of a page button; the button shows only the number."
          },
          {
            "key": "range",
            "kind": "function",
            "default": "{from}–{to} of {total}",
            "signature": "(from: string, to: string, total: string)",
            "description": "Visible and announced range of items. All arguments are already formatted for the locale."
          },
          {
            "key": "empty",
            "kind": "string",
            "default": "No results",
            "description": "Visible and announced text when `total` is 0."
          },
          {
            "key": "pageOf",
            "kind": "function",
            "default": "Page {page} of {count}",
            "signature": "(page: string, count: string)",
            "description": "Visible and announced text when only the page count is known."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-control-h-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-line-strong",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-5",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2"
      ],
      "stories": {
        "title": "Components/Pagination",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-pagination--default",
            "hasPlay": true
          },
          {
            "name": "WithRange",
            "id": "components-pagination--with-range",
            "hasPlay": true
          },
          {
            "name": "MiddlePage",
            "id": "components-pagination--middle-page",
            "hasPlay": true
          },
          {
            "name": "LastPage",
            "id": "components-pagination--last-page",
            "hasPlay": true
          },
          {
            "name": "FewPages",
            "id": "components-pagination--few-pages",
            "hasPlay": false
          },
          {
            "name": "SinglePage",
            "id": "components-pagination--single-page",
            "hasPlay": true
          },
          {
            "name": "NoResults",
            "id": "components-pagination--no-results",
            "hasPlay": true
          },
          {
            "name": "KeyboardNavigation",
            "id": "components-pagination--keyboard-navigation",
            "description": "Tab reaches the buttons; Enter or Space pick a page and the range text updates.",
            "hasPlay": true
          },
          {
            "name": "FocusStaysAfterLastPage",
            "id": "components-pagination--focus-stays-after-last-page",
            "description": "Pressing Next on the second to last page disables it; focus moves to the current page.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-pagination--controlled",
            "hasPlay": true
          },
          {
            "name": "Portuguese",
            "id": "components-pagination--portuguese",
            "hasPlay": true
          },
          {
            "name": "NarrowContainer",
            "id": "components-pagination--narrow-container",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-pagination--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-pagination--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Pagination/Pagination.md",
        "summary": "Previous and next buttons, numbered page buttons with ellipses and a text that says where you are (\"11–20 of 42\"). The list logic is a tested pure function, `getPageItems`.\n\n- Handoff source: `components/data/Pagination.jsx` (TODO `CMP-DAT-02`).",
        "whenToUse": [
          "Below a table or list that is split into pages on the server or the client."
        ],
        "whenNotToUse": [
          "For endless feeds: use a \"Load more\" `Button`.",
          "For a handful of items that fit on one screen.",
          "For moving through steps: use a stepper."
        ],
        "keyboard": ["Keys: `Tab` to move; `Enter` and `Space` press."],
        "examples": [
          "<Pagination total={42} pageSize={10} page={page} onChange={setPage} />\n\n<Pagination pageCount={8} defaultPage={3} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Below a table or list that is split into pages on the server or the client."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For endless feeds: use a \"Load more\" `Button`.\n- For a handful of items that fit on one screen.\n- For moving through steps: use a stepper."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  span[role=status, aria-live=polite]   \"11–20 of 42\"  or  \"Page 2 of 5\"\n  nav[aria-label=\"Pagination\"]\n    ul\n      li  IconButton  Previous page\n      li  button      1    (aria-label \"Page 1\", aria-current=\"page\" on the current one)\n      li  …                (aria-hidden)\n      li  IconButton  Next page\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                   | Type                     | Default | Notes                                                                                   |\n| ---------------------- | ------------------------ | ------- | --------------------------------------------------------------------------------------- |\n| `page` / `defaultPage` | `number`                 | `1`     | Current page (1-based), controlled / uncontrolled. Clamped into range.                  |\n| `onChange`             | `(page: number) => void` | none    | Called only when the page changes.                                                      |\n| `pageCount`            | `number`                 | derived | Needed unless `total` and `pageSize` are given.                                         |\n| `total`, `pageSize`    | `number`                 | none    | Together they enable the range text and derive `pageCount`.                             |\n| `messages`             | partial catalog          | catalog | `navLabel`, `previous`, `next`, `page(n)`, `range(from, to, total)`, `empty`, `pageOf`. |\n\nExported helpers: `getPageItems(current, pageCount)`, `getPageCount(total, pageSize)`, `getItemRange(page, pageSize, total)`."
          },
          {
            "id": "page-list-algorithm",
            "heading": "Page list algorithm",
            "content": "Up to 7 pages every page is shown. Beyond that: the first page, the last page, the current page and its two neighbours; each gap becomes an ellipsis. A gap that would hide exactly one page shows that page instead (the handoff showed an ellipsis for it, which is wider than the page it hides). The page list never exceeds 7 slots."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `nav` named \"Pagination\" (from the catalog). Page buttons have `aria-label=\"Page 3\"` and show only the number; the current one has `aria-current=\"page\"`.\n- The summary is a polite live region: when the page changes, a screen reader announces the new range (\"11–20 of 42\"). It is not announced on first render.\n- Previous and next are disabled at the ends. When pressing one disables it, focus moves to the current page button, so focus is never lost to the page body.\n- Ellipses are `aria-hidden` list items. In right-to-left locales the previous and next chevrons swap direction.\n- Targets are 32px; page numbers are mono with tabular figures. Numbers are formatted for the locale (\"1.248\" in pt-BR).\n- Keys: `Tab` to move; `Enter` and `Space` press."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Do not repeat the total elsewhere. Keep the labels from the catalog unless your product uses another word for the items (\"rows\", \"orders\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Pagination total={42} pageSize={10} page={page} onChange={setPage} />\n\n<Pagination pageCount={8} defaultPage={3} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Labels, range and the page text come from the message catalog (the handoff was hard-coded pt-BR); numbers use `Intl`.\n- Buttons are React Aria / `IconButton` components; hover and disabled are CSS; the disabled opacity is the shared token.\n- `onChange` is only called for real changes; `page` can be controlled; focus is kept when an edge button becomes disabled.\n- Not included yet: page size selector (P2)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Button",
            "reason": "For endless feeds: use a \"Load more\" `Button`."
          }
        ],
        "uses": ["IconButton"],
        "usedBy": ["DataTable"]
      }
    },
    {
      "name": "Popover",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Popover } from 'loui-ds';",
      "folder": "src/components/Popover",
      "sourceFile": "src/components/Popover/Popover.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A floating surface for interactive content (filters, a small form, a color picker), built on\nReact Aria `DialogTrigger`, `Popover` and `Dialog`.\n\nIt is not for lists of commands (use `Menu`), for choosing one option (use `Select`) or for\nplain hints (use `Tooltip`). `Escape` and an outside press close it and focus returns to the\ntrigger; when the content has focusable elements, `Tab` stays inside until it is closed.",
      "props": [
        {
          "name": "trigger",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": true,
          "description": "The element that opens the popover: a `Button`, `IconButton` or any React Aria pressable.\nIt receives `aria-expanded` and `aria-haspopup=\"dialog\"`."
        },
        {
          "name": "label",
          "type": "string",
          "required": true,
          "description": "Accessible name of the popover (for example \"Filters\"). The popover is a `dialog`, so it\nneeds a name; a visible heading inside it can be linked with `aria-labelledby` instead."
        },
        {
          "name": "children",
          "type": "ReactNode | ((options: { close: () => void; }) => ReactNode)",
          "required": true,
          "description": "Interactive content. A function receives `close` to dismiss the popover from inside."
        },
        {
          "name": "placement",
          "type": "'bottom' | 'bottom left' | 'bottom right' | 'bottom start' | 'bottom end' | 'top' | 'top left' | 'top right' | 'top start' | 'top end' | 'left' | 'left top' | 'left bottom' | 'start' | 'start top' | 'start bottom' | 'right' | 'right top' | 'right bottom' | 'end' | 'end top' | 'end bottom'",
          "required": false,
          "default": "'bottom start'",
          "description": "Side and alignment relative to the trigger. Default `bottom start`. Flips on collision.",
          "values": [
            "bottom",
            "bottom left",
            "bottom right",
            "bottom start",
            "bottom end",
            "top",
            "top left",
            "top right",
            "top start",
            "top end",
            "left",
            "left top",
            "left bottom",
            "start",
            "start top",
            "start bottom",
            "right",
            "right top",
            "right bottom",
            "end",
            "end top",
            "end bottom"
          ]
        },
        {
          "name": "showArrow",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Draws an arrow that points at the trigger."
        },
        {
          "name": "isNonModal",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Keeps the page outside the popover available to assistive technology and does not hide it.\nLeave it off unless the popover is a long-lived tool the user returns to; React Aria's\nguidance is that most popovers should stay modal for screen readers."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the popover opens or closes."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the popover surface."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the popover surface."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-duration",
        "--ds-ease",
        "--ds-font-sans",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-popover-arrow-size",
        "--ds-popover-min-width",
        "--ds-popover-padding",
        "--ds-radius-lg",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-surface-raised",
        "--ds-text"
      ],
      "stories": {
        "title": "Components/Popover",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-popover--default",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-popover--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "WithArrow",
            "id": "components-popover--with-arrow",
            "hasPlay": false
          },
          {
            "name": "AboveTrigger",
            "id": "components-popover--above-trigger",
            "hasPlay": false
          },
          {
            "name": "AlignedToEnd",
            "id": "components-popover--aligned-to-end",
            "hasPlay": false
          },
          {
            "name": "KeyboardOpenAndRestoreFocus",
            "id": "components-popover--keyboard-open-and-restore-focus",
            "hasPlay": true
          },
          {
            "name": "CloseFromInside",
            "id": "components-popover--close-from-inside",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Popover/Popover.md",
        "summary": "A floating surface that opens from a trigger and holds interactive content. Built on React Aria `DialogTrigger`, `Popover` and `Dialog`.\n\n- Handoff source: `components/navigation/Popover` (CMP-NAV-04).",
        "whenToUse": [
          "Filters, a small form, a date range, a color picker: content the user operates and then dismisses."
        ],
        "whenNotToUse": [
          "A list of commands: use `Menu`.",
          "Choosing one option from a list: use `Select` or `Combobox`.",
          "A hint on hover or focus: use `Tooltip`.",
          "A decision that blocks the task: use `Dialog`."
        ],
        "keyboard": [
          "Keys: `Enter`/`Space` on the trigger opens it and moves focus inside; `Tab` moves through the content; `Esc` closes it and focus returns to the trigger."
        ],
        "examples": [
          "<Popover trigger={<Button>Filters</Button>} label=\"Filters\" placement=\"bottom end\">\n  {({ close }) => (\n    <>\n      <Input label=\"Customer\" />\n      <Button variant=\"primary\" onPress={close}>\n        Apply filters\n      </Button>\n    </>\n  )}\n</Popover>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Filters, a small form, a date range, a color picker: content the user operates and then dismisses."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A list of commands: use `Menu`.\n- Choosing one option from a list: use `Select` or `Combobox`.\n- A hint on hover or focus: use `Tooltip`.\n- A decision that blocks the task: use `Dialog`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nDialogTrigger\n  trigger            Button | IconButton            aria-haspopup=dialog, aria-expanded, aria-controls\n  Popover            raised surface, strong line, radius lg, overlay shadow, z popover\n    OverlayArrow     optional\n    Dialog           role=dialog, aria-label = label\n      children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                     | Type                                    | Default        | Notes                                                   |\n| ------------------------ | --------------------------------------- | -------------- | ------------------------------------------------------- |\n| `trigger`                | `ReactElement`                          | none           | A `Button`, `IconButton` or other React Aria pressable. |\n| `label`                  | `string` (required)                     | none           | Accessible name of the dialog.                          |\n| `children`               | `ReactNode \\| ({ close }) => ReactNode` | none           | Interactive content.                                    |\n| `placement`              | React Aria `Placement`                  | `bottom start` | Flips when there is no room.                            |\n| `showArrow`              | `boolean`                               | `false`        | Arrow pointing at the trigger.                          |\n| `isNonModal`             | `boolean`                               | `false`        | See Accessibility.                                      |\n| `isOpen` / `defaultOpen` | `boolean`                               |                | Controlled / uncontrolled.                              |\n| `onOpenChange`           | `(isOpen: boolean) => void`             |                |                                                         |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The trigger gets `aria-haspopup=\"dialog\"`, `aria-expanded` and `aria-controls`. The handoff used a `span` with a click handler and no ARIA.\n- The surface is a `dialog` named by `label`. A menu is **not** a dialog: use `Menu` for commands.\n- Keys: `Enter`/`Space` on the trigger opens it and moves focus inside; `Tab` moves through the content; `Esc` closes it and focus returns to the trigger.\n- Outside press closes it. React Aria hides the rest of the page from assistive technology while it is open (`isNonModal` turns that off; React Aria recommends it only for long-lived tool popovers).\n- Focus is restored to the trigger on close.\n- Placement is collision-aware. Forced colors: the arrow uses `CanvasText`/`Canvas`.\n- Layer: `--ds-z-popover`. A popover opened from inside a `Dialog` or `Sheet` sits **below** them in the current z-index scale (see the Dialog doc, \"Layers\")."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- `label` is a noun phrase: \"Filters\", \"Date range\".\n- Put the confirming action inside the content (\"Apply filters\"); verb plus object."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Popover trigger={<Button>Filters</Button>} label=\"Filters\" placement=\"bottom end\">\n  {({ close }) => (\n    <>\n      <Input label=\"Customer\" />\n      <Button variant=\"primary\" onPress={close}>\n        Apply filters\n      </Button>\n    </>\n  )}\n</Popover>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Real positioning with collision handling instead of `position: absolute` offsets.\n- `role` is `dialog` with a name; ARIA attributes on the trigger; focus moves in and returns.\n- Outside press and `Escape` are handled by React Aria (the handoff used `mousedown` and a window `keydown` listener per instance).\n- The handoff's `width` and `style` props are gone: size the content, not the surface."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Menu",
            "reason": "A list of commands: use `Menu`."
          },
          {
            "name": "Select",
            "reason": "Choosing one option from a list: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing one option from a list: use `Select` or `Combobox`."
          },
          {
            "name": "Tooltip",
            "reason": "A hint on hover or focus: use `Tooltip`."
          },
          {
            "name": "Dialog",
            "reason": "A decision that blocks the task: use `Dialog`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Progress",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Progress } from 'loui-ds';",
      "folder": "src/components/Progress",
      "sourceFile": "src/components/Progress/Progress.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A thin bar for a task that makes progress: determinate with a `value`, or indeterminate\nwhen the amount is unknown. Built on React Aria `ProgressBar` (`role=\"progressbar\"`).\nFor a measured quantity that is not a task (disk used, quota) use `Meter`.",
      "props": [
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label, wired to the bar with `aria-labelledby`. Required unless you set `aria-label`\nor `aria-labelledby`: a bar without a name is an accessibility bug (React Aria warns in development)."
        },
        {
          "name": "meta",
          "type": "ReactNode",
          "required": false,
          "description": "Text on the right of the label, in mono type: \"6.2 / 10 GB\". It is visual only. If it says\nmore than the percentage, also pass `valueLabel` so screen readers hear the same thing."
        },
        {
          "name": "showValue",
          "type": "boolean",
          "required": false,
          "description": "Shows the formatted percentage (`62%`, from `Intl` with the active locale) as `meta` when `meta` is not given."
        },
        {
          "name": "tone",
          "type": "'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'accent'",
          "description": "Color of the fill. Color is never the only signal: pair it with a label or `meta`. Default `accent`.",
          "values": ["accent", "success", "warning", "danger"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ProgressBarBaseProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "formatOptions",
              "type": "NumberFormatOptions",
              "required": false
            },
            {
              "name": "maxValue",
              "type": "number",
              "required": false
            },
            {
              "name": "minValue",
              "type": "number",
              "required": false
            },
            {
              "name": "value",
              "type": "number",
              "required": false
            },
            {
              "name": "valueLabel",
              "type": "ReactNode",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ProgressBarProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isIndeterminate",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "tone": ["accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-duration-progress",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-progress-height",
        "--ds-progress-indeterminate-duration",
        "--ds-progress-indeterminate-width",
        "--ds-radius-pill",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-success",
        "--ds-surface-hover",
        "--ds-text-2",
        "--ds-warning"
      ],
      "stories": {
        "title": "Components/Progress",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-progress--default",
            "hasPlay": false
          },
          {
            "name": "WithoutLabel",
            "id": "components-progress--without-label",
            "hasPlay": false
          },
          {
            "name": "ShowValue",
            "id": "components-progress--show-value",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-progress--empty",
            "hasPlay": false
          },
          {
            "name": "Complete",
            "id": "components-progress--complete",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-progress--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-progress--danger",
            "hasPlay": false
          },
          {
            "name": "Indeterminate",
            "id": "components-progress--indeterminate",
            "hasPlay": false
          },
          {
            "name": "AllTones",
            "id": "components-progress--all-tones",
            "hasPlay": false
          },
          {
            "name": "AsMeter",
            "id": "components-progress--as-meter",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-progress--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Progress/Progress.md",
        "summary": "A thin bar (4 px) with a label and a value in mono type. `Progress` shows the progress of a task, determinate or indeterminate (React Aria `ProgressBar`, `role=\"progressbar\"`). `Meter` shows a quantity within a range (React Aria `Meter`, `role=\"meter\"`) and shares the same look.",
        "whenToUse": [
          "`Progress`: an upload, an import, a multi-step job. Use indeterminate when the amount is unknown.",
          "`Meter`: disk used, quota consumed, a score. The value is a measurement, not a task that will finish."
        ],
        "whenNotToUse": [
          "Waiting with no known structure and no label: use `Spinner`.",
          "Content that is loading: use `Skeleton`."
        ],
        "keyboard": [],
        "examples": [
          "<Progress value={62} label=\"Storage\" meta=\"6.2 / 10 GB\" valueLabel=\"6.2 of 10 GB\" />\n<Progress isIndeterminate label=\"Importing orders\" />\n<Meter value={82} tone=\"warning\" label=\"Quota\" showValue />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- `Progress`: an upload, an import, a multi-step job. Use indeterminate when the amount is unknown.\n- `Meter`: disk used, quota consumed, a score. The value is a measurement, not a task that will finish."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Waiting with no known structure and no label: use `Spinner`.\n- Content that is loading: use `Skeleton`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=progressbar|meter]\n  div.header      Label (left) · meta (right, mono)\n  div.track       4px, pill\n    div.fill      width = percentage (or a sweeping segment when indeterminate)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                            | Type                                             | Default    | Notes                                                                                              |\n| ------------------------------- | ------------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------- |\n| `value`                         | `number`                                         | `0`        | Clamped to the range                                                                               |\n| `minValue`, `maxValue`          | `number`                                         | `0`, `100` |                                                                                                    |\n| `isIndeterminate`               | `boolean` (`Progress` only)                      |            | No `aria-valuenow`; the fill sweeps                                                                |\n| `label`                         | `ReactNode`                                      |            | Visible label, wired as the accessible name                                                        |\n| `aria-label`, `aria-labelledby` | `string`                                         |            | Use when there is no visible label. One of the three is required (React Aria warns in development) |\n| `meta`                          | `ReactNode`                                      |            | Visual text on the right (\"6.2 / 10 GB\")                                                           |\n| `showValue`                     | `boolean`                                        | `false`    | Shows the formatted percentage as `meta`; hidden when indeterminate                                |\n| `valueLabel`                    | `ReactNode`                                      |            | The spoken value (`aria-valuetext`). Pass it when `meta` says more than a percentage               |\n| `formatOptions`                 | `Intl.NumberFormatOptions`                       | percent    | Formatting of the automatic value text                                                             |\n| `tone`                          | `'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'accent'` | Fill color                                                                                         |\n\nOther React Aria props, `className` and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The bar is named by the visible label (`aria-labelledby`) or `aria-label`; React Aria warns in development when a bar has neither.\n- `aria-valuenow`, `aria-valuemin` and `aria-valuemax` are set, and `aria-valuetext` is the percentage formatted with `Intl` for the active locale (\"62%\", \"62,5%\" in pt-BR). If you show \"6.2 / 10 GB\", set `valueLabel=\"6.2 of 10 GB\"` so screen readers hear the same thing.\n- An indeterminate bar has no `aria-valuenow`, so assistive technology reports it as busy without a number.\n- Progress does not announce changes by itself. For long tasks, announce milestones and completion in a live region (for example with a `Toast`).\n- Color is not the only signal: the label and the value text carry the meaning. The fill is 4 px high but the track and fill are a graphic that keeps at least 3:1 against the surface; the track is a neutral surface step.\n- **Reduced motion:** the width change has no transition and the indeterminate sweep becomes a static segment centered in the track. **Forced colors:** the track gets a border and the fill uses the text color."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns or short phrases in sentence case (\"Storage\", \"Importing orders\").\n- Put units and ratios in `meta`, in the formatted form of the active locale."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Progress value={62} label=\"Storage\" meta=\"6.2 / 10 GB\" valueLabel=\"6.2 of 10 GB\" />\n<Progress isIndeterminate label=\"Importing orders\" />\n<Meter value={82} tone=\"warning\" label=\"Quota\" showValue />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria: `role`, name, `aria-valuetext` and the label wiring come from it. The handoff `div role=\"progressbar\"` had no name.\n- Indeterminate mode, `valueLabel`, `showValue` and `Meter` are new.\n- Width changes use the motion tokens and are disabled under reduced motion. Height and sweep settings are component tokens (`--ds-progress-*`).\n- `Progress` no longer takes a `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Spinner",
            "reason": "Waiting with no known structure and no label: use `Spinner`."
          },
          {
            "name": "Skeleton",
            "reason": "Content that is loading: use `Skeleton`."
          }
        ],
        "uses": [],
        "usedBy": ["FileUpload"]
      }
    },
    {
      "name": "ProportionBar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ProportionBar } from 'loui-ds';",
      "folder": "src/components/ProportionBar",
      "sourceFile": "src/components/ProportionBar/ProportionBar.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A single bar split into the parts of a whole (payment methods, composition). It is an image\nwith a text summary (\"Payment methods: Pix 47%, Cash 16%…\"), and an optional legend with the\npercentage and the value of each part.\n\nNeutral by default: signal colors are not used for categories. Parts are separated by a\ngap, and beyond four parts the ramp repeats with a hatch so they never look alike.",
      "props": [
        {
          "name": "segments",
          "type": "readonly ProportionSegment[]",
          "required": true,
          "description": "The parts of the whole, in display order. Without `color` they use a neutral ramp of four\nsteps (`text`, `text-2`, `text-3`, `line-control`); from the fifth part on, the ramp\nrepeats with a hatch pattern so no two parts look the same (up to eight; group the tail\ninto \"Other\" or give explicit colors beyond that). The bar never sorts."
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "Name of the whole (\"Payment methods\"). It starts the summary read for the bar."
        },
        {
          "name": "legend",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Shows a list under the bar with swatch, label, percentage and formatted value for each part."
        },
        {
          "name": "total",
          "type": "number",
          "required": false,
          "description": "The size of the whole, in the unit of the segment values. When it is larger than the sum of\nthe segments, the difference is the remaining part: an unlabelled track after the segments,\nnamed \"Remaining\" in the summary and in the legend. This is how a single segment becomes a\n\"part of the whole\" indicator (\"Paid 1,240 of 2,000\"). When smaller than the sum it is ignored."
        },
        {
          "name": "legendColumns",
          "type": "readonly ProportionLegendColumn[]",
          "required": false,
          "default": "['share', 'value']",
          "description": "Columns of the legend, in order: `share` (percentage), `value` (formatted with `format`)\nand `count` (the segment `count`). Default `['share', 'value']`. Percentages are rounded one\nby one and may not add up to exactly 100%."
        },
        {
          "name": "format",
          "type": "(value: number) => ReactNode",
          "required": false,
          "description": "Formats each value in the legend. Default: `formatNumber` with the active locale. Use `formatCurrency` for money."
        },
        {
          "name": "percentDigits",
          "type": "number",
          "required": false,
          "default": "0",
          "description": "Decimals of the percentages in the summary and the legend. Default `0`."
        },
        {
          "name": "messages",
          "type": "Partial<{ segment: (label: string, percent: string) => string; summary: (items: string, label?: string) => string; remaining: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ProportionBar",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "segment",
            "kind": "function",
            "default": "{label} {percent}",
            "signature": "(label: string, percent: string)",
            "description": "One segment in the summary read for the bar: \"Cash 16%\". Both arguments are already formatted."
          },
          {
            "key": "summary",
            "kind": "function",
            "default": "{label?}: {items}",
            "signature": "(items: string, label?: string)",
            "description": "The whole summary. `items` is the list of segments joined for the locale (\"Cash 16%, Card 11% and Pix 73%\"); `label` names the bar, if given."
          },
          {
            "key": "remaining",
            "kind": "string",
            "default": "Remaining",
            "description": "Name of the unfilled part when `total` is larger than the sum of the segments: read in the summary and printed in the legend."
          }
        ]
      },
      "cssVariables": [
        "--ds-bg",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-control",
        "--ds-line-subtle",
        "--ds-proportion-bar-color",
        "--ds-proportion-bar-columns",
        "--ds-proportion-bar-hatch-size",
        "--ds-proportion-bar-height",
        "--ds-proportion-bar-percent-col-w",
        "--ds-proportion-bar-segment-gap",
        "--ds-proportion-bar-share",
        "--ds-proportion-bar-swatch-size",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-3",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/ProportionBar",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-proportion-bar--default",
            "description": "The bar is an image named by a text summary, so the percentages are never only visual.",
            "hasPlay": true
          },
          {
            "name": "WithLegend",
            "id": "components-proportion-bar--with-legend",
            "hasPlay": true
          },
          {
            "name": "EightParts",
            "id": "components-proportion-bar--eight-parts",
            "description": "Beyond four parts the neutral ramp repeats with a hatch, so no two parts look alike (up to eight).",
            "hasPlay": false
          },
          {
            "name": "ExplicitColors",
            "id": "components-proportion-bar--explicit-colors",
            "description": "Explicit colors are for when the neutral ramp is not enough; the legend still names every part.",
            "hasPlay": false
          },
          {
            "name": "ZeroPart",
            "id": "components-proportion-bar--zero-part",
            "hasPlay": false
          },
          {
            "name": "AllZero",
            "id": "components-proportion-bar--all-zero",
            "hasPlay": false
          },
          {
            "name": "OneDecimalPercent",
            "id": "components-proportion-bar--one-decimal-percent",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-proportion-bar--long-labels",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-proportion-bar--portuguese-brazil",
            "description": "Percentages, numbers and the list in the summary follow the provider locale.",
            "hasPlay": true
          },
          {
            "name": "LegendColumns",
            "id": "components-proportion-bar--legend-columns",
            "description": "Share, value and count columns, in the order given. Each legend item shares its `id` with the bar segment.",
            "hasPlay": true
          },
          {
            "name": "PartOfWhole",
            "id": "components-proportion-bar--part-of-whole",
            "description": "One segment plus `total`: \"Paid 1,240 of 2,000\". The rest is an unlabelled track named \"Remaining\" for assistive technology.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ProportionBar/ProportionBar.md",
        "summary": "One bar split into the parts of a whole (payment methods, composition), with a text summary for assistive technology and an optional legend with the percentage and value of each part.",
        "whenToUse": [
          "Showing how a total divides into a few parts (2 to 8) at a glance.",
          "Next to or inside a `StatCard`."
        ],
        "whenNotToUse": [
          "Progress of a task: use `Progress`. A quantity within a range: use `Meter`.",
          "Comparing separate values or many categories: use `Chart` or `RankList`.",
          "Exact figures the user must read: show the legend (or a table)."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Showing how a total divides into a few parts (2 to 8) at a glance.\n- Next to or inside a `StatCard`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Progress of a task: use `Progress`. A quantity within a range: use `Meter`.\n- Comparing separate values or many categories: use `Chart` or `RankList`.\n- Exact figures the user must read: show the legend (or a table)."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  div[role=img][aria-label=\"Payment methods: Pix 53%, Cash 18%…\"]   the track, 10px, pill radius\n    div  one segment per non-zero part (flex share = its share of the whole, 2px gap)\n  ul (legend, optional)\n    li  swatch (aria-hidden) · label · percentage · value\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop            | Type                                      | Default | Notes                                                                                                                                               |\n| --------------- | ----------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `segments`      | `{ id?, label, value, count?, color? }[]` | -       | Display order; never sorted. Negative and non-finite values count as 0. `id` links the segment to its legend item, `count` feeds the `count` column |\n| `total`         | `number`                                  |         | Size of the whole. When larger than the sum, the rest is an unlabelled track named \"Remaining\"                                                      |\n| `legendColumns` | `('share'                                 | 'value' | 'count')[]`                                                                                                                                         | `['share', 'value']` | Columns of the legend, in order |\n| `label`         | `string`                                  |         | Name of the whole; starts the summary                                                                                                               |\n| `legend`        | `boolean`                                 | `false` | List with swatch, label, percentage and formatted value                                                                                             |\n| `format`        | `(value) => ReactNode`                    | `Intl`  | Formats legend values. Use `formatCurrency` for money                                                                                               |\n| `percentDigits` | `number`                                  | `0`     | Decimals of the percentages                                                                                                                         |\n| `messages`      | `Partial<messages>`                       |         | `segment(label, percent)`, `summary(items, label?)`, `remaining`                                                                                    |"
          },
          {
            "id": "part-of-the-whole",
            "heading": "Part of the whole",
            "content": "A single segment plus `total` is the \"Paid 1,240 of 2,000\" indicator; there is no separate component:\n\n```tsx\n<ProportionBar\n  label=\"Invoice\"\n  total={2000}\n  legend\n  format={(value) => formatCurrency(value * 100, 'USD', locale)}\n  segments={[{ id: 'paid', label: 'Paid', value: 1240 }]}\n/>\n```\n\nThe summary reads \"Invoice: Paid 62% and Remaining 38%\" (the word comes from the `remaining` message, `Restante` in pt-BR). The remaining part is drawn as the empty track, so it carries no color; in the legend it has a hollow, dashed swatch. A `total` smaller than the sum of the segments is ignored. Use `DonutMeter` instead when the share is the only message and a ring fits better."
          },
          {
            "id": "linking-segments-and-legend-items",
            "heading": "Linking segments and legend items",
            "content": "Each segment can carry a stable `id`. The bar segment gets `data-segment=\"<id>\"` and its legend item gets the same attribute and the DOM id `<bar id>-<id>`, so tests and consumer code can find the pair. For people the link is the order, the label and the matching swatch (color is never the only link). The bar itself is one `role=\"img\"` whose children are presentational, so the accessible link is the summary: it names every part in the same order as the legend, and the legend repeats the same labels."
          },
          {
            "id": "legend-columns",
            "heading": "Legend columns",
            "content": "`legendColumns` picks and orders the columns after the label: `share` (percentage), `value` (formatted with `format`) and `count` (the segment `count`, formatted with `Intl`). All rows share one grid, so the columns line up (tabular mono figures). Shares are rounded one by one and may not add up to 100%."
          },
          {
            "id": "distinguishing-the-parts",
            "heading": "Distinguishing the parts",
            "content": "Neutral by default: signal colors are not used for categories.\n\n- The ramp has four solid steps: `text`, `text-2`, `text-3`, `line-control`. All reach 3:1 against the track background.\n- Neighbours are always separated by a 2px gap.\n- From the fifth part on, the ramp **repeats with a diagonal hatch**, so up to eight parts look different. This is the only place a gradient is used, as a pattern, not as decoration.\n- Beyond eight parts, group the smallest into \"Other\" or pass explicit `color`s. In development a warning appears when more than eight parts have no color.\n- Even so, the legend and the summary carry the identity of every part; the fill only helps sighted users."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The bar is `role=\"img\"` with an `aria-label` that lists every part with its percentage: \"Payment methods: Pix 53%, Cash 18%, Debit 17%, and Credit 11%\". The list is joined with `Intl.ListFormat` for the locale; the per-part text comes from messages.\n- The legend is a real list. It repeats the summary on purpose: it is the content sighted users read, and it stays available when you need more than the bar.\n- Zero parts are listed in the legend (with 0%) but not drawn.\n- Forced colors: segments use system colors (`CanvasText`, `GrayText`, `LinkText`, `Highlight`) and the track gets a border."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Part labels are short nouns (\"Cash\", \"Pix\"); the whole gets a label when the bar is not obvious from context.\n- Percentages are rounded; they may not add up to exactly 100%. Use `percentDigits` if that matters."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<ProportionBar\n  label=\"Payment methods\"\n  legend\n  format={(value) => formatCurrency(value, 'USD', locale)}\n  segments={[\n    { label: 'Pix', value: 1693000 },\n    { label: 'Cash', value: 573400 },\n    { label: 'Debit', value: 538800 },\n    { label: 'Credit', value: 359600 },\n  ]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A text alternative: `role=\"img\"` with a summary; the handoff relied on `title` tooltips.\n- The ramp's fourth step is `line-control` (3:1) instead of `line-strong`, and parts beyond four are hatched instead of repeating the same four colors.\n- `height` is a token (`--ds-proportion-bar-height`), not a prop; `RAMP` is not exported.\n- Legend values and percentages use `Intl`; the legend is a list.\n- Segment widths use a flex share, so they are exact even when values are fractions."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Progress",
            "reason": "Progress of a task: use `Progress`. A quantity within a range: use `Meter`."
          },
          {
            "name": "Meter",
            "reason": "Progress of a task: use `Progress`. A quantity within a range: use `Meter`."
          },
          {
            "name": "Chart",
            "reason": "Comparing separate values or many categories: use `Chart` or `RankList`."
          },
          {
            "name": "RankList",
            "reason": "Comparing separate values or many categories: use `Chart` or `RankList`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Radio",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Radio } from 'loui-ds';",
      "folder": "src/components/RadioGroup",
      "sourceFile": "src/components/RadioGroup/RadioGroup.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "One option of a `RadioGroup`. `value` is required and identifies the option.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Secondary text under the label. Linked with `aria-describedby`, not part of the name."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the option root."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the option root `<div>`. Use `inputRef` for the `<input>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", RadioGroupRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RadioFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "inputRef",
              "type": "Ref<HTMLInputElement | null>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "RadioProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": true
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-choice-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radio-dot-size",
        "--ds-radio-size",
        "--ds-radius-pill",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-5",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/RadioGroup",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-radio-group--default",
            "hasPlay": true
          },
          {
            "name": "WithSelection",
            "id": "components-radio-group--with-selection",
            "hasPlay": true
          },
          {
            "name": "Horizontal",
            "id": "components-radio-group--horizontal",
            "hasPlay": false
          },
          {
            "name": "WithDescriptions",
            "id": "components-radio-group--with-descriptions",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-radio-group--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-radio-group--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-radio-group--disabled",
            "hasPlay": false
          },
          {
            "name": "DisabledOption",
            "id": "components-radio-group--disabled-option",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-radio-group--read-only",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-radio-group--long-labels",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/RadioGroup/RadioGroup.md",
        "summary": "A labelled set of mutually exclusive options with native radio inputs. Built on React Aria `RadioGroup` + `RadioField` + `RadioButton` (the non-deprecated pair that supports a description per option). Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": ["Choose exactly one of 2 to 5 options that users want to compare."],
        "whenNotToUse": [
          "Many options: `Select`.",
          "Independent choices: `Checkbox`.",
          "Immediate on/off: `Switch`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Choose exactly one of 2 to 5 options that users want to compare."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Many options: `Select`.\n- Independent choices: `Checkbox`.\n- Immediate on/off: `Switch`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "`RadioGroup`:\n\n| Prop                                                  | Notes                                                                                                                  |\n| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `label`                                               | Group name. Required: pass `label`, `aria-label` or `aria-labelledby` (type error otherwise; development warning too). |\n| `description`, `errorMessage`                         | Linked to the group by `aria-describedby`.                                                                             |\n| `orientation`                                         | `vertical` (default) or `horizontal`.                                                                                  |\n| `value` / `defaultValue` / `onChange(value)`          | React Aria naming; `value` is a string.                                                                                |\n| `name`                                                | Shared by every radio; the selected value is submitted.                                                                |\n| `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | Standard states.                                                                                                       |\n\n`Radio`: `value` (required), `label`, `description`, `isDisabled`, `className`, `inputRef`."
          },
          {
            "id": "look",
            "heading": "Look",
            "content": "- 18px circle (`--ds-radio-size`), `--ds-line-control` border at 3:1 or better in every state, accent border and a 9px accent dot when selected. Hover brightens the border.\n- The option row is at least 24px tall and the whole row is clickable.\n- A disabled group dims once; a single disabled option uses `--ds-opacity-disabled`."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                        | Behavior                                                                                        |\n| -------------------------- | ----------------------------------------------------------------------------------------------- |\n| `Tab`                      | One tab stop: lands on the selected radio, or the first one when nothing is selected            |\n| `ArrowDown` / `ArrowRight` | Next radio, selects it, wraps from the last to the first (directions follow the text direction) |\n| `ArrowUp` / `ArrowLeft`    | Previous radio, selects it, wraps                                                               |\n| `Space`                    | Selects the focused radio                                                                       |\n\nRoving focus is handled by React Aria, so only one radio is in the tab order (the handoff made every radio tabbable). `role=\"radiogroup\"` is named by the label; the description and error belong to the group; `aria-required` and `aria-invalid` are set."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<RadioGroup\n  label=\"Region\"\n  name=\"region\"\n  defaultValue=\"eu\"\n  description=\"You cannot change this later.\"\n>\n  <Radio value=\"eu\" label=\"EU: Frankfurt\" />\n  <Radio value=\"us\" label=\"US: Virginia\" description=\"Default for new workspaces.\" />\n</RadioGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Native radios with roving focus replace `div role=\"radio\"` with a tab stop each; the group is labelled, can be required or invalid, and has a description and error.\n- Options are `Radio` children instead of an `options` array (a description per option is possible); `direction=\"row\"` is `orientation=\"horizontal\"`, and the default is vertical.\n- `onChange(value)` and `value` follow React Aria."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Many options: `Select`."
          },
          {
            "name": "Checkbox",
            "reason": "Independent choices: `Checkbox`."
          },
          {
            "name": "Switch",
            "reason": "Immediate on/off: `Switch`."
          }
        ],
        "uses": ["Description", "FieldMessages", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "RadioGroup",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { RadioGroup } from 'loui-ds';",
      "folder": "src/components/RadioGroup",
      "sourceFile": "src/components/RadioGroup/RadioGroup.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A labelled `role=\"radiogroup\"` of mutually exclusive options with native radio inputs.\nThe group is one tab stop; arrow keys move and select (roving focus is handled by React Aria).\n`value`, `defaultValue` and `onChange(value)` follow React Aria; `name` is shared by every radio.\nA group needs a name: pass `label`, `aria-label` or `aria-labelledby`.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The `Radio` elements of the group."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the label. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text for the group, shown while it is invalid and the error is displayed (after a\nsubmit attempt with the default native validation, or at once with `isInvalid`)."
        },
        {
          "name": "orientation",
          "type": "'vertical' | 'horizontal'",
          "required": false,
          "default": "'vertical'",
          "description": "Layout of the options. Default `vertical`; arrow keys follow the reading direction either way.",
          "values": ["vertical", "horizontal"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the group root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the group root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", RadioGroupRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: string) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "string | null",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: string) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-choice-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radio-dot-size",
        "--ds-radio-size",
        "--ds-radius-pill",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-space-5",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/RadioGroup",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-radio-group--default",
            "hasPlay": true
          },
          {
            "name": "WithSelection",
            "id": "components-radio-group--with-selection",
            "hasPlay": true
          },
          {
            "name": "Horizontal",
            "id": "components-radio-group--horizontal",
            "hasPlay": false
          },
          {
            "name": "WithDescriptions",
            "id": "components-radio-group--with-descriptions",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-radio-group--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-radio-group--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-radio-group--disabled",
            "hasPlay": false
          },
          {
            "name": "DisabledOption",
            "id": "components-radio-group--disabled-option",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-radio-group--read-only",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-radio-group--long-labels",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/RadioGroup/RadioGroup.md",
        "summary": "A labelled set of mutually exclusive options with native radio inputs. Built on React Aria `RadioGroup` + `RadioField` + `RadioButton` (the non-deprecated pair that supports a description per option). Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": ["Choose exactly one of 2 to 5 options that users want to compare."],
        "whenNotToUse": [
          "Many options: `Select`.",
          "Independent choices: `Checkbox`.",
          "Immediate on/off: `Switch`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Choose exactly one of 2 to 5 options that users want to compare."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Many options: `Select`.\n- Independent choices: `Checkbox`.\n- Immediate on/off: `Switch`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "`RadioGroup`:\n\n| Prop                                                  | Notes                                                                                                                  |\n| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `label`                                               | Group name. Required: pass `label`, `aria-label` or `aria-labelledby` (type error otherwise; development warning too). |\n| `description`, `errorMessage`                         | Linked to the group by `aria-describedby`.                                                                             |\n| `orientation`                                         | `vertical` (default) or `horizontal`.                                                                                  |\n| `value` / `defaultValue` / `onChange(value)`          | React Aria naming; `value` is a string.                                                                                |\n| `name`                                                | Shared by every radio; the selected value is submitted.                                                                |\n| `isRequired`, `isInvalid`, `isDisabled`, `isReadOnly` | Standard states.                                                                                                       |\n\n`Radio`: `value` (required), `label`, `description`, `isDisabled`, `className`, `inputRef`."
          },
          {
            "id": "look",
            "heading": "Look",
            "content": "- 18px circle (`--ds-radio-size`), `--ds-line-control` border at 3:1 or better in every state, accent border and a 9px accent dot when selected. Hover brightens the border.\n- The option row is at least 24px tall and the whole row is clickable.\n- A disabled group dims once; a single disabled option uses `--ds-opacity-disabled`."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                        | Behavior                                                                                        |\n| -------------------------- | ----------------------------------------------------------------------------------------------- |\n| `Tab`                      | One tab stop: lands on the selected radio, or the first one when nothing is selected            |\n| `ArrowDown` / `ArrowRight` | Next radio, selects it, wraps from the last to the first (directions follow the text direction) |\n| `ArrowUp` / `ArrowLeft`    | Previous radio, selects it, wraps                                                               |\n| `Space`                    | Selects the focused radio                                                                       |\n\nRoving focus is handled by React Aria, so only one radio is in the tab order (the handoff made every radio tabbable). `role=\"radiogroup\"` is named by the label; the description and error belong to the group; `aria-required` and `aria-invalid` are set."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<RadioGroup\n  label=\"Region\"\n  name=\"region\"\n  defaultValue=\"eu\"\n  description=\"You cannot change this later.\"\n>\n  <Radio value=\"eu\" label=\"EU: Frankfurt\" />\n  <Radio value=\"us\" label=\"US: Virginia\" description=\"Default for new workspaces.\" />\n</RadioGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Native radios with roving focus replace `div role=\"radio\"` with a tab stop each; the group is labelled, can be required or invalid, and has a description and error.\n- Options are `Radio` children instead of an `options` array (a description per option is possible); `direction=\"row\"` is `orientation=\"horizontal\"`, and the default is vertical.\n- `onChange(value)` and `value` follow React Aria."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Many options: `Select`."
          },
          {
            "name": "Checkbox",
            "reason": "Independent choices: `Checkbox`."
          },
          {
            "name": "Switch",
            "reason": "Immediate on/off: `Switch`."
          }
        ],
        "uses": ["Description", "FieldMessages", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "RankList",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { RankList } from 'loui-ds';",
      "folder": "src/components/RankList",
      "sourceFile": "src/components/RankList/RankList.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An ordered list of items with a value, a bar relative to the largest value and an optional\ndetail. Rows can be plain or buttons.\n\nThe list renders the items as given; pass `sort` and `limit` for a \"top N\". Put it in a\n`Card` with `padding=\"none\"`.",
      "props": [
        {
          "name": "items",
          "type": "readonly RankItem[]",
          "required": true,
          "description": "The rows, rendered **in the order given** unless `sort` is set. This differs from the\nhandoff, which always sorted descending and kept only six rows: sorting and truncating\nsilently changes the data, so both are opt-in."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the list (\"Top products\"). Optional: a list inside a titled `Card` is already named by it."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the list."
        },
        {
          "name": "sort",
          "type": "'desc' | 'asc'",
          "required": false,
          "description": "Sorts by `value` (`desc` largest first, `asc` smallest first) before `limit` is applied. Ties keep their input order.",
          "values": ["desc", "asc"]
        },
        {
          "name": "limit",
          "type": "number",
          "required": false,
          "description": "Shows only the first `limit` rows (after `sort`). Default: all rows."
        },
        {
          "name": "format",
          "type": "(value: number) => ReactNode",
          "required": false,
          "description": "Formats each value. Default: `formatNumber` with the active locale; use `formatCurrency` for money."
        },
        {
          "name": "showPosition",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Shows the position (1, 2, 3...) before each row. The list is ordered either way. Default `true`."
        },
        {
          "name": "emptyState",
          "type": "ReactNode",
          "required": false,
          "description": "Content when there are no rows. Default: the \"No data for this period\" message."
        },
        {
          "name": "messages",
          "type": "Partial<{ empty: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "RankList",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "empty",
            "kind": "string",
            "default": "No data for this period",
            "description": "Text shown when there are no items."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line",
        "--ds-radius-pill",
        "--ds-rank-list-bar-h",
        "--ds-rank-list-padding-block",
        "--ds-rank-list-padding-inline",
        "--ds-rank-list-position-w",
        "--ds-rank-list-share",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-6",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/RankList",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-rank-list--default",
            "description": "Ranked data, largest first. Rows render in the order given: the list never re-sorts or truncates on its own.",
            "hasPlay": true
          },
          {
            "name": "GivenOrder",
            "id": "components-rank-list--given-order",
            "description": "Without `sort`, unsorted data keeps its given order (here: insertion order).",
            "hasPlay": true
          },
          {
            "name": "SortedAndLimited",
            "id": "components-rank-list--sorted-and-limited",
            "description": "`sort` and `limit` are opt-in: a \"top 3\" by value.",
            "hasPlay": true
          },
          {
            "name": "WithoutPositions",
            "id": "components-rank-list--without-positions",
            "hasPlay": false
          },
          {
            "name": "WithoutSwatchOrMeta",
            "id": "components-rank-list--without-swatch-or-meta",
            "hasPlay": false
          },
          {
            "name": "Actionable",
            "id": "components-rank-list--actionable",
            "description": "Rows with `onAction` are buttons: Tab reaches them, Enter or Space press.",
            "hasPlay": true
          },
          {
            "name": "Empty",
            "id": "components-rank-list--empty",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-rank-list--long-labels",
            "hasPlay": false
          },
          {
            "name": "BarsAlign",
            "id": "components-rank-list--bars-align",
            "description": "Bars of different rows share one column grid, so their lengths can be compared by eye.",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-rank-list--portuguese-brazil",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/RankList/RankList.md",
        "summary": "An ordered list of items with a value, a bar relative to the largest value and an optional detail. Rows can be plain or buttons. Meant to sit inside a `Card` without body padding.",
        "whenToUse": [
          "\"Top products\", \"best customers\", \"colors by sales\": a short ranking with comparable values."
        ],
        "whenNotToUse": [
          "Several columns, sorting by the user, selection: use `DataTable`.",
          "Parts of a whole: use `ProportionBar`.",
          "A trend over time: use `Chart`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- \"Top products\", \"best customers\", \"colors by sales\": a short ranking with comparable values."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Several columns, sorting by the user, selection: use `DataTable`.\n- Parts of a whole: use `ProportionBar`.\n- A trend over time: use `Chart`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  ol (role=list)\n    li > div | button     grid, shared columns for all rows (subgrid)\n      position            decorative number (aria-hidden); the ol already numbers the items\n      label               optional swatch (aria-hidden) + text, truncated\n      value               mono, formatted\n      bar                 4px track + fill, relative to the largest value (aria-hidden)\n      meta                mono detail, optional\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop           | Type                   | Default | Notes                                                                       |\n| -------------- | ---------------------- | ------- | --------------------------------------------------------------------------- |\n| `items`        | `RankItem[]`           | -       | `id`, `label`, `value`, `meta`, `swatch`, `onAction`. Rendered **as given** |\n| `sort`         | `asc \\| desc`          |         | Opt-in sort by value (stable ties)                                          |\n| `limit`        | `number`               |         | Opt-in truncation, applied after `sort`                                     |\n| `format`       | `(value) => ReactNode` | `Intl`  | Use `formatCurrency` for money                                              |\n| `showPosition` | `boolean`              | `true`  | Numbers 1, 2, 3 of the rows shown                                           |\n| `emptyState`   | `ReactNode`            |         | Default: \"No data for this period\" (messages)                               |\n| `aria-label`   | `string`               |         | Optional name of the list                                                   |\n| `messages`     | `Partial<messages>`    |         | `empty`                                                                     |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A real ordered list (`ol`, explicit `role=\"list\"` so Safari keeps the semantics with `list-style: none`).\n- Rows with `onAction` are `<button>`s (React Aria `Button`): Tab, Enter, Space. Their name is the label followed by the value; position, bar and swatch are hidden from assistive technology.\n- The bar and swatch are decorative: the value (and `meta`) carry the information as text.\n- Bars of all rows share one grid, so their lengths are comparable.\n- Text colors: label `text`, meta `text-2` (stays above 4.5:1 on hover); focus is an inset accent outline (system color in forced colors)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are names, not sentences. Meta is short and mono: `12 units · 18%`.\n- Use this for things (products, colors, customers). Give customers an `onAction` that opens the profile."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Card padding=\"none\" title=\"Top products\">\n  <RankList\n    sort=\"desc\"\n    limit={6}\n    format={(v) => formatCurrency(v, 'USD', locale)}\n    items={products.map((f) => ({\n      id: f.id,\n      label: f.name,\n      value: f.totalMinor,\n      swatch: f.hex,\n      meta: `${f.units} units · ${f.pct}`,\n    }))}\n  />\n</Card>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- **No silent sorting or truncation.** The handoff always sorted descending and kept six rows; here the items render as given, and `sort` / `limit` are opt-in props. Migrating: add `sort=\"desc\" limit={6}`.\n- An ordered list instead of `div`s; actionable rows are buttons instead of `div role=\"button\"`; `it.onClick` is `onAction`.\n- Empty text comes from messages (the Portuguese default is gone); `showPos` is `showPosition`.\n- The swatch is the core `Swatch` (`decorative`, size `sm`); it takes a hex color.\n- Bars align across rows (subgrid), values and positions use `Intl`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Several columns, sorting by the user, selection: use `DataTable`."
          },
          {
            "name": "ProportionBar",
            "reason": "Parts of a whole: use `ProportionBar`."
          },
          {
            "name": "Chart",
            "reason": "A trend over time: use `Chart`."
          }
        ],
        "uses": ["Swatch"],
        "usedBy": []
      }
    },
    {
      "name": "Rating",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Rating } from 'loui-ds';",
      "folder": "src/components/Rating",
      "sourceFile": "src/components/Rating/Rating.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A read-only star rating. It is a single image (`role=\"img\"`) with a text alternative such\nas \"3.5 out of 5 stars\"; the stars themselves are decorative. Half stars are supported.\n\nIt does not accept input. An interactive rating needs radio-group semantics and is not part\nof the design system yet.",
      "props": [
        {
          "name": "value",
          "type": "number",
          "required": true,
          "description": "The rating to show, from 0 to `max`. Values are clamped and rounded to the nearest half\nstar (\"3.7\" shows three and a half stars, and says \"3.5 out of 5 stars\")."
        },
        {
          "name": "max",
          "type": "number",
          "required": false,
          "default": "5",
          "description": "Number of stars. A whole number from 1. Default `5`."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Star size token: `sm` 12px, `md` 14px (default), `lg` 18px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "showValue",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Also shows the value as text (\"3.5/5\") after the stars. Hidden from assistive technology: the name already says it."
        },
        {
          "name": "messages",
          "type": "Partial<{ label: (value: string, max: string, maxCount: number) => string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 50
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Rating",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "label",
            "kind": "function",
            "default": "{value} out of {max} stars",
            "signature": "(value: string, max: string, maxCount: number)",
            "description": "Accessible name of the rating. Arguments are already formatted for the locale."
          }
        ]
      },
      "cssVariables": [
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-line-control",
        "--ds-rating-fill",
        "--ds-rating-gap",
        "--ds-rating-star-gap",
        "--ds-text-2",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Rating",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-rating--default",
            "hasPlay": true
          },
          {
            "name": "HalfStar",
            "id": "components-rating--half-star",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-rating--empty",
            "hasPlay": false
          },
          {
            "name": "Full",
            "id": "components-rating--full",
            "hasPlay": false
          },
          {
            "name": "WithValue",
            "id": "components-rating--with-value",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-rating--sizes",
            "hasPlay": false
          },
          {
            "name": "CustomScale",
            "id": "components-rating--custom-scale",
            "hasPlay": false
          },
          {
            "name": "OutOfRange",
            "id": "components-rating--out-of-range",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-rating--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Rating/Rating.md",
        "summary": "A read-only star rating, such as a review score. One image with a text alternative (\"3.5 out of 5 stars\"); half stars are supported.\n\n- Handoff source: `components/display/Rating` (TODO `CMP-DSP-06`).",
        "whenToUse": [
          "Show a score someone else gave (a review, a quality grade).",
          "Next to the review text in lists and cards, optionally with the numeric value."
        ],
        "whenNotToUse": [
          "To collect a rating: it takes no input. An interactive rating needs radio-group semantics (one tab stop, arrows, `Space`) and is out of scope for now; use `SegmentedControl` or `RadioGroup` with labelled options meanwhile.",
          "For an average with a distribution or a trend: use `Chart` or `ProportionBar`."
        ],
        "keyboard": [],
        "examples": [
          "<Rating value={4} />\n<Rating value={3.5} showValue size=\"lg\" />\n<Rating value={7} max={10} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Show a score someone else gave (a review, a quality grade).\n- Next to the review text in lists and cards, optionally with the numeric value."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To collect a rating: it takes no input. An interactive rating needs radio-group semantics (one tab stop, arrows, `Space`) and is out of scope for now; use `SegmentedControl` or `RadioGroup` with labelled options meanwhile.\n- For an average with a distribution or a trend: use `Chart` or `ProportionBar`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan[role=img][aria-label=\"3.5 out of 5 stars\"]\n  stars (aria-hidden)\n    star x max      empty glyph + clipped filled glyph (0, 50 or 100 %)\n  value             optional \"3.5/5\" (aria-hidden)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop        | Type                   | Default  | Notes                                                         |\n| ----------- | ---------------------- | -------- | ------------------------------------------------------------- |\n| `value`     | `number`               | required | Clamped to `0..max`, rounded to the nearest half star.        |\n| `max`       | `number`               | `5`      | Number of stars (whole number, at least 1).                   |\n| `size`      | `'sm' \\| 'md' \\| 'lg'` | `'md'`   | Star size: 12, 14 and 18 px (icon tokens).                    |\n| `showValue` | `boolean`              | `false`  | Adds \"3.5/5\" in mono text after the stars.                    |\n| `messages`  | `{ label }`            |          | Override the accessible name (`label(value, max, maxCount)`). |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `role=\"img\"` with an `aria-label` from messages; the glyphs and the visible value are `aria-hidden`, so the rating is announced once.\n- The label is pluralized by the scale (`1 out of 1 star`, `3.5 out of 5 stars`) and numbers follow the locale (`3,5 de 5 estrelas` in pt-BR).\n- The value is not carried by color alone: the label and the optional value text state the number. The empty star uses `--ds-line-control` (3:1 against the surfaces); filled uses `--ds-warning`.\n- Forced colors: filled stars use `CanvasText`, empty ones `GrayText`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Show the number next to the stars when the exact value matters; stars alone are fine in dense lists.\n- Pair a rating with the review it belongs to; do not leave it alone in a row of badges."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Rating value={4} />\n<Rating value={3.5} showValue size=\"lg\" />\n<Rating value={7} max={10} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Half stars are displayed (the handoff rounded to whole stars).\n- The label comes from messages, is pluralized and localized (it was hard-coded pt-BR).\n- Stars are the bundled Lucide `Star` icon drawn with a CSS fill, not the text glyph; sizes are icon tokens.\n- Documented decision: read-only. Interactive rating is a separate future component.\n- Filled icons are normally avoided (AGENTS.md section 7); the star is the one exception because filled versus empty is the meaning."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "To collect a rating: it takes no input. An interactive rating needs radio-group semantics (one tab stop, arrows, `Space`) and is out of scope for now; use `SegmentedControl` or `RadioGroup` with labelled options meanwhile."
          },
          {
            "name": "RadioGroup",
            "reason": "To collect a rating: it takes no input. An interactive rating needs radio-group semantics (one tab stop, arrows, `Space`) and is out of scope for now; use `SegmentedControl` or `RadioGroup` with labelled options meanwhile."
          },
          {
            "name": "Chart",
            "reason": "For an average with a distribution or a trend: use `Chart` or `ProportionBar`."
          },
          {
            "name": "ProportionBar",
            "reason": "For an average with a distribution or a trend: use `Chart` or `ProportionBar`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": ["ApprovalCard"]
      }
    },
    {
      "name": "RequiredMark",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { RequiredMark } from 'loui-ds';",
      "folder": "src/components/Field",
      "sourceFile": "src/components/Field/Field.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "The visible marker of a required field, text from messages. Hidden from assistive technology:\nthe control announces \"required\" itself. Use it when a field cannot use `Label`\n(for example inside a `Checkbox` label).",
      "props": [
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "Field",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "requiredMark",
            "kind": "string",
            "default": "*",
            "description": "Visible marker after the label of a required field. Hidden from assistive technology."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-field-gap",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Field",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Anatomy",
            "id": "components-field--anatomy",
            "description": "The building blocks composed by hand inside a React Aria field. Prefer the ready-made fields.",
            "hasPlay": true
          },
          {
            "name": "ErrorsAfterSaveAttempt",
            "id": "components-field--errors-after-save-attempt",
            "description": "Validation timing: native validation (the default) keeps every error hidden until the first\nsave attempt, then focuses the first invalid field.",
            "hasPlay": true
          },
          {
            "name": "RealtimeValidation",
            "id": "components-field--realtime-validation",
            "description": "The alternative: `validationBehavior=\"aria\"` reports problems while the user types. Use sparingly.",
            "hasPlay": true
          },
          {
            "name": "StandaloneErrorText",
            "id": "components-field--standalone-error-text",
            "description": "Error text for a hand-built group with no field around it, linked with `aria-describedby`.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Field/Field.md",
        "summary": "Shared building blocks that make every form control (`Input`, `Textarea`, `Select`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`) look and behave the same: the same label, description and error styles, the same spacing, the same required marker, the same disabled treatment and the same validation timing. Tickets: ARCH-008, ARCH-009, part of GAP-003.\n\nMost code never imports from `Field` directly: use the ready-made controls. Use the pieces when you compose a field that has no ready-made component yet (for example a custom picker built on a React Aria field).",
        "whenToUse": [],
        "whenNotToUse": [],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "exports",
            "heading": "Exports",
            "content": "| Export                                 | What it is                                                                                                                                                                     |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `Label`                                | React Aria `Label` with the field label style (13/18, medium). `isRequired` appends the required marker.                                                                       |\n| `Description`                          | React Aria `Text slot=\"description\"`, 13/18 in `--ds-text-3`. Linked to the control with `aria-describedby`. `tone` tints it (see Toned description).                          |\n| `FieldError`                           | React Aria `FieldError`: renders only while the field is invalid and the error is displayed. The message is in a `role=\"alert\"` element. 13/18 in `--ds-danger`.               |\n| `ErrorText`                            | Standalone error text for a group with no field around it. Same look as `FieldError`; `id` (generated by default), `announce` adds `role=\"alert\"`. Renders nothing when empty. |\n| `FieldMessages`                        | `Description` (with `descriptionTone`) followed by `FieldError`. Every control renders it under its box.                                                                       |\n| `RequiredMark`                         | The marker alone, for fields whose label is not a `Label`.                                                                                                                     |\n| `FieldMessageTone`                     | Type: `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'`, the tone of a description.                                                                                |\n| `FieldLabelingProps`, `WithLabel`      | Types. `FieldLabelingProps` makes \"label, `aria-label` or `aria-labelledby`\" mandatory. `WithLabel<P>` helps type stories.                                                     |\n| `FieldErrorMessage`, `FieldValidation` | Types for the `errorMessage` prop (a node or a function of the validation result).                                                                                             |"
          },
          {
            "id": "anatomy-and-spacing",
            "heading": "Anatomy and spacing",
            "content": "```\nLabel                       13/18 medium, --ds-text\n  gap: --ds-field-gap (6px)\nControl box                 border --ds-line-control, radius md, height 32/40/48\n  gap: --ds-field-gap\nDescription                 13/18, --ds-text-3\nError                       13/18, --ds-danger\n```\n\nThe description stays visible when an error shows (the handoff replaced the hint with the error). Constraints such as \"8 characters or more\" remain useful while the user fixes the value, and both texts are linked to the control."
          },
          {
            "id": "toned-description-gap-039",
            "heading": "Toned description (GAP-039)",
            "content": "A live check under a field (\"Matches\", \"Over by $5\", \"Short by $5\") is a description with a tone, not an error: the field stays valid and no `role=\"alert\"` is added.\n\n```tsx\n<CurrencyInput\n  label=\"Amount received\"\n  currency=\"USD\"\n  description=\"Short by $5.00\"\n  descriptionTone=\"danger\"\n/>\n```\n\n- `descriptionTone` (`neutral` default, `accent`, `success`, `warning`, `danger`) is supported by `Input`, `Textarea`, `Select`, `Combobox`, `NumberField`, `CurrencyInput` and `SegmentedControl`; `Description` and `FieldMessages` take `tone` / `descriptionTone`.\n- The text carries the meaning; the tone only reinforces it. Write the words yourself (\"Over by $5\", not a bare \"$5\") and never rely on colour alone. The tone sets `data-tone` on the description element and nothing else changes: the description is announced exactly as before through `aria-describedby`.\n- Colours are the signal tokens `--ds-accent`, `--ds-success`, `--ds-warning`, `--ds-danger`, which pass 4.5:1 on `--ds-bg`, `--ds-surface` and `--ds-surface-raised` (see `contrast-pairs.json`).\n- Use `errorMessage` when the value must be fixed before submitting; use a toned description for information that can be left as it is.\n- Other fields built on `FieldMessages` (`Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `ChoiceTiles`, `DatePicker`, `TagInput`) do not expose `descriptionTone` yet."
          },
          {
            "id": "one-state-model-arch-009",
            "heading": "One state model (ARCH-009)",
            "content": "| State     | Look                                                                                                                       | Semantics                                          |\n| --------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Default   | `--ds-line-control` border, `--ds-bg` fill                                                                                 |                                                    |\n| Hover     | Border brightens to `--ds-text-3`. The fill never changes, so the border stays at 3:1 or more.                             |                                                    |\n| Focus     | Accent border and the 3px focus ring (`--ds-focus-ring`); forced-colors gets a system outline                              |                                                    |\n| Invalid   | Danger border and danger focus ring                                                                                        | `aria-invalid`, error linked by `aria-describedby` |\n| Read only | `--ds-surface` fill, same border                                                                                           | `readonly`; focusable, submitted with the form     |\n| Disabled  | Label and control at `--ds-opacity-disabled`, dashed border, `--ds-surface` fill; description and error stay full strength | `disabled`; not focusable, not submitted           |\n| Required  | Marker after the label                                                                                                     | `required` / `aria-required` on the control        |\n\nForm-control borders use `--ds-line-control` and stay at 3:1 or better against the page in every state (WCAG 1.4.11). The suites assert this per state."
          },
          {
            "id": "required-marker",
            "heading": "Required marker",
            "content": "`Label isRequired` appends the text of the `requiredMark` message (`*` by default) inside the label. The marker is `aria-hidden`: the control itself exposes \"required\", so announcing the marker too would repeat it. Override the text per instance (`messages={{ requiredMark: '(required)' }}`) or app-wide:\n\n```tsx\n<DSProvider locale=\"pt-BR\" messages={{ Field: { requiredMark: '(obrigatório)' } }}>\n```"
          },
          {
            "id": "validation-behavior-native-default-and-why",
            "heading": "Validation behavior: native (default), and why",
            "content": "React Aria fields validate in one of two modes:\n\n- `validationBehavior=\"native\"` (React Aria's default, kept here): the browser's constraint validation. Constraints such as `required`, `type`, `pattern` and `minLength` block submission. The error is **not displayed until the user tries to submit**; then focus moves to the first invalid field and the error appears. Custom `validate` rules report when the user commits the field (leaves it) and again on submit.\n- `validationBehavior=\"aria\"`: only ARIA attributes are set; nothing blocks submission, and the error follows every keystroke.\n\n**Decision: `native` is the default for all fields.** It implements the product rule _errors appear only after a save attempt_ without extra code, it works without JavaScript hydration problems (the browser blocks the submit), it puts `required` on the real control and it moves focus to the first problem. `aria` suits fields that need live feedback (a username availability check, a counter): opt in per field with `validationBehavior=\"aria\"`. A consumer that renders its own `Form` can set the mode for every field below it.\n\nRules for authors:\n\n- Provide an `errorMessage` that says how to fix the problem (\"Enter a complete email address.\"). Without one, React Aria shows the browser's own message, in the browser's language. A static `errorMessage` replaces only that browser text: a message the app supplied (a `validate` rule or `validationErrors` on a `Form`) is shown instead (GAP-009).\n- To control `isInvalid` yourself (for example after a server response), pass `isInvalid` and `errorMessage`; the error shows at once.\n- The error text is announced when it appears (`role=\"alert\"`) and read again when focus lands on the field.\n- Putting `<form>` in the page is enough; no wrapper component is required."
          },
          {
            "id": "errors-outside-a-field-errortext",
            "heading": "Errors outside a field: `ErrorText`",
            "content": "| Use                   | When                                                                                                                  |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `FieldError`          | Inside a React Aria field (`Input`, `Select`, `CheckboxGroup`, ...): validation is wired for you.                     |\n| `ErrorText`           | A group of controls you built by hand (colour options, rows) with no field around it. Light, inline, under the group. |\n| `Alert tone=\"danger\"` | An error about the whole page, form or request (\"Could not save the order\"). Heavy, boxed.                            |\n\n`ErrorText` renders whenever it has content, so you decide when it shows. It is not a live region by default: link it with `aria-describedby`, and move focus to the first invalid control on submit so the message is read. Set `announce` when the error appears with no focus move (it adds `role=\"alert\"`). `aria-invalid` is not allowed on `role=\"group\"`; if the group has a role that supports it (`radiogroup`, `listbox`), set it together with `aria-describedby`.\n\n```tsx\nconst errorId = useId();\n<div\n  role=\"group\"\n  aria-labelledby={labelId}\n  aria-describedby={error ? errorId : undefined}\n>\n  {options}\n</div>\n<ErrorText id={errorId}>{error}</ErrorText>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every control needs an accessible name: `label`, `aria-label` or `aria-labelledby`. The types enforce it; a development warning covers untyped callers.\n- `Description` and `FieldError` are linked with `aria-describedby`; the order is description, then error.\n- Disabled fields are exempt from contrast requirements; everything else meets 4.5:1 for text and 3:1 for borders."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Ids come from React Aria (`useId`), not from the label text.\n- Error and hint are both shown (see above), and both are programmatically linked.\n- One disabled opacity for every control (`--ds-opacity-disabled`); the handoff used per-control values.\n- No hover background on fields: hover brightens the border."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": [],
        "usedBy": ["Checkbox", "Switch"]
      }
    },
    {
      "name": "ResponseFeedback",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ResponseFeedback } from 'loui-ds/patterns';",
      "folder": "src/patterns/ResponseFeedback",
      "sourceFile": "src/patterns/ResponseFeedback/ResponseFeedback.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Feedback and follow-up actions under an assistant response: two toggle buttons (good and bad\nresponse, `aria-pressed`), then optional Copy and Regenerate.\n\nChoosing a rating is reversible, so nothing asks to confirm: pressing the active rating again\nremoves it. A polite live region says \"Feedback sent\" or \"Feedback removed\" after the change.\nThe first render never announces anything. The buttons show text only for now: thumbs, copy\nand refresh icons are not part of the curated icon set yet.",
      "props": [
        {
          "name": "value",
          "type": "ResponseRating | null",
          "required": false,
          "description": "Controlled rating. `null` means no rating."
        },
        {
          "name": "defaultValue",
          "type": "ResponseRating | null",
          "required": false,
          "default": "null",
          "description": "Initial rating when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "(value: ResponseRating | null) => void",
          "required": false,
          "description": "Called with the new rating, or `null` when the person takes their rating back."
        },
        {
          "name": "onCopy",
          "type": "() => void",
          "required": false,
          "description": "Called when Copy is pressed. Without it there is no Copy button. Confirm with a toast (\"Copied\")."
        },
        {
          "name": "onRegenerate",
          "type": "() => void",
          "required": false,
          "description": "Called when Regenerate is pressed. Without it there is no Regenerate button."
        },
        {
          "name": "isRegenerating",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Disables Regenerate while a new response is being generated."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Disables every button."
        },
        {
          "name": "messages",
          "type": "Partial<{ groupLabel: string; positive: string; negative: string; copy: string; regenerate: string; sent: string; removed: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 167
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ResponseFeedback",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "groupLabel",
            "kind": "string",
            "default": "Rate this response",
            "description": "Accessible name of the group of rating buttons."
          },
          {
            "key": "positive",
            "kind": "string",
            "default": "Good response",
            "description": "Toggle buttons; pressed means the rating is set."
          },
          {
            "key": "negative",
            "kind": "string",
            "default": "Bad response"
          },
          {
            "key": "copy",
            "kind": "string",
            "default": "Copy response",
            "description": "Action buttons (verb plus object)."
          },
          {
            "key": "regenerate",
            "kind": "string",
            "default": "Regenerate response"
          },
          {
            "key": "sent",
            "kind": "string",
            "default": "Feedback sent",
            "description": "Polite announcements after a rating changes."
          },
          {
            "key": "removed",
            "kind": "string",
            "default": "Feedback removed"
          }
        ]
      },
      "cssVariables": [
        "--ds-font-sans",
        "--ds-line-subtle",
        "--ds-response-feedback-divider-gap",
        "--ds-response-feedback-gap"
      ],
      "stories": {
        "title": "Patterns/ResponseFeedback",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-response-feedback--default",
            "hasPlay": true
          },
          {
            "name": "RatedBad",
            "id": "patterns-response-feedback--rated-bad",
            "hasPlay": false
          },
          {
            "name": "RatingOnly",
            "id": "patterns-response-feedback--rating-only",
            "hasPlay": false
          },
          {
            "name": "Regenerating",
            "id": "patterns-response-feedback--regenerating",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "patterns-response-feedback--disabled",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-response-feedback--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ResponseFeedback/ResponseFeedback.md",
        "summary": "Feedback and follow-up actions under an assistant response: good and bad response toggles, then optional Copy and Regenerate. Import it from `loui-ds/patterns`.",
        "whenToUse": [
          "Under each assistant response in a `ChatPanel`, to learn which answers help."
        ],
        "whenNotToUse": [
          "Collecting a written review or a star rating: use `Rating` in a form.",
          "Anything that sends the rating somewhere the owner cannot see: say where it goes in your product."
        ],
        "keyboard": [
          "The rating buttons are toggle buttons (`aria-pressed`) in a labelled group. Each is a tab stop; `Space` and `Enter` press."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Under each assistant response in a `ChatPanel`, to learn which answers help."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Collecting a written review or a star rating: use `Rating` in a form.\n- Anything that sends the rating somewhere the owner cannot see: say where it goes in your product."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv\n  group \"Rate this response\"   toggle \"Good response\" · toggle \"Bad response\"   (aria-pressed)\n  button \"Copy response\"       only with onCopy\n  button \"Regenerate response\" only with onRegenerate\n  status                       visually hidden live region\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                     | Type                               | Default | Notes                                                                          |\n| ------------------------ | ---------------------------------- | ------- | ------------------------------------------------------------------------------ |\n| `value` / `defaultValue` | `'positive' \\| 'negative' \\| null` | `null`  | Controlled or uncontrolled rating.                                             |\n| `onChange`               | `(value \\| null) => void`          |         | `null` when the person takes the rating back.                                  |\n| `onCopy`                 | `() => void`                       |         | Shows Copy. Copy the text yourself, then confirm with a toast.                 |\n| `onRegenerate`           | `() => void`                       |         | Shows Regenerate.                                                              |\n| `isRegenerating`         | `boolean`                          | `false` | Disables Regenerate while the new response is generated.                       |\n| `isDisabled`             | `boolean`                          | `false` | Disables every button.                                                         |\n| `messages`               | partial catalog                    |         | `groupLabel`, `positive`, `negative`, `copy`, `regenerate`, `sent`, `removed`. |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The rating buttons are toggle buttons (`aria-pressed`) in a labelled group. Each is a tab stop; `Space` and `Enter` press.\n- Rating is reversible, so nothing asks to confirm: pressing the active button again removes the rating.\n- A polite live region says \"Feedback sent\" or \"Feedback removed\" after a change. It is silent on first render.\n- Each control shows a decorative icon (`ThumbsUpIcon`, `ThumbsDownIcon`, `CopyIcon`, `RefreshIcon`) next to its visible text label; the text is the accessible name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Buttons are verb plus object (\"Copy response\", \"Regenerate response\").\n- Confirm Copy with a short past-tense toast (\"Copied\"), outside this pattern.\n- Regenerate replaces content: offer Undo, as for any reversible action."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<ResponseFeedback\n  onChange={(rating) => saveRating(message.id, rating)}\n  onCopy={() => copyToClipboard(message.content)}\n  onRegenerate={() => regenerate(message.id)}\n  isRegenerating={busy}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "No handoff source: new pattern (TODO `AI-013`)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Rating",
            "reason": "Collecting a written review or a star rating: use `Rating` in a form."
          }
        ],
        "uses": ["Button", "Icon", "Toggle", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "ScrollArea",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ScrollArea } from 'loui-ds';",
      "folder": "src/components/ScrollArea",
      "sourceFile": "src/components/ScrollArea/ScrollArea.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A scrolling container with thin, themed scrollbars. It scrolls inside the space its parent\ngives it: give it a height (or put it in a flex column and let it shrink).\n\nWhile its content overflows it becomes a keyboard stop (`tabindex=\"0\"`) and a labelled\n`region`, so keyboard users can scroll it with the arrow keys (WCAG 2.1.1). When nothing\noverflows it is a plain container, with no tab stop and no landmark. A name is therefore\nalways required.",
      "props": [
        {
          "name": "orientation",
          "type": "'both' | 'horizontal' | 'vertical'",
          "required": false,
          "default": "'vertical'",
          "description": "Which axes scroll. Default `vertical`.",
          "values": ["both", "horizontal", "vertical"]
        },
        {
          "name": "maxBlockSize",
          "type": "string | number",
          "required": false,
          "description": "Maximum height of the region: a CSS length (`\"20rem\"`, `\"40vh\"`) or a number of pixels. The\nregion grows with its content up to this height and then scrolls, which suits a list that\nshould show a fixed number of rows (a searchable picker). Set as the\n`--ds-scroll-area-max-h` custom property; without it the region is limited by its parent\n(`100%`). While it scrolls it stays a focusable, labelled region."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the region (\"Order history\")."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the region."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-line-control",
        "--ds-scroll-area-max-h",
        "--ds-space-1",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Layout/ScrollArea",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-scroll-area--default",
            "description": "Overflowing content: the area is a labelled region and a keyboard stop. Press Tab, then PageDown (the browser test covers the scrolling itself).",
            "hasPlay": true
          },
          {
            "name": "MaxHeight",
            "id": "layout-scroll-area--max-height",
            "description": "`maxBlockSize` lets the region grow with its content up to a cap, then scroll (a visible list of N rows).",
            "hasPlay": false
          },
          {
            "name": "NotScrollable",
            "id": "layout-scroll-area--not-scrollable",
            "hasPlay": true
          },
          {
            "name": "Horizontal",
            "id": "layout-scroll-area--horizontal",
            "hasPlay": false
          },
          {
            "name": "Both",
            "id": "layout-scroll-area--both",
            "hasPlay": false
          },
          {
            "name": "LabelledBy",
            "id": "layout-scroll-area--labelled-by",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-scroll-area--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/ScrollArea/ScrollArea.md",
        "summary": "A scrolling container with thin, themed scrollbars that stays keyboard accessible.",
        "whenToUse": [
          "A bounded list, log or panel inside a page: a notification list, a long form step, a code output.",
          "A region that must scroll on its own while the page around it does not."
        ],
        "whenNotToUse": [
          "For the page itself: let the document scroll, or use `AppShell`'s `main`.",
          "For a table: `DataTable` has its own horizontal scroll and sticky parts.",
          "Inside a Dialog or Sheet body: those already scroll their body."
        ],
        "keyboard": ["Arrow keys, `PageUp`/`PageDown`, `Home`/`End` scroll it natively."],
        "examples": [
          "<div style={{ blockSize: 240 }}>\n  <ScrollArea aria-label=\"Activity\">{events.map(renderEvent)}</ScrollArea>\n</div>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A bounded list, log or panel inside a page: a notification list, a long form step, a code output.\n- A region that must scroll on its own while the page around it does not."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For the page itself: let the document scroll, or use `AppShell`'s `main`.\n- For a table: `DataTable` has its own horizontal scroll and sticky parts.\n- Inside a Dialog or Sheet body: those already scroll their body."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (overflow auto, scrollbar-width: thin)\n  div (content wrapper, observed for size changes)\n    children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                                   | Default      | Notes                                                                                                                      |\n| -------------------------------- | -------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------- |\n| `aria-label` / `aria-labelledby` | `string`                               | **required** | One of them. It names the region while it scrolls                                                                          |\n| `orientation`                    | `'vertical' \\| 'horizontal' \\| 'both'` | `'vertical'` | Which axes scroll                                                                                                          |\n| `maxBlockSize`                   | `string \\| number`                     | none         | Height cap (CSS length, or pixels as a number). The region grows with its content up to it, then scrolls (stays focusable) |\n\nOther `div` attributes (not `role`, `tabIndex`) and `ref` go to the root. Give the area a height, or put it in a flex column and let it shrink; it scrolls inside the space its parent gives it."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A scroll container must be reachable by keyboard (WCAG 2.1.1). While the content overflows, the area gets `tabindex=\"0\"` and `role=\"region\"` with your label; with nothing to scroll it is a plain container (no tab stop, no landmark). A `ResizeObserver` keeps this in sync as content or size changes.\n- Because focusability is dynamic, the type always requires a name, so a scrollable area is never unlabelled.\n- Arrow keys, `PageUp`/`PageDown`, `Home`/`End` scroll it natively.\n- The focus ring is drawn inside the box, so a clipping parent cannot hide it.\n- Scrollbars are the browser's thin scrollbars colored with `--ds-line-control` (3:1 on the background); on hover they brighten to `--ds-text-3`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Name it for what it holds: \"Order history\", \"Activity\", not \"Scrollable area\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<div style={{ blockSize: 240 }}>\n  <ScrollArea aria-label=\"Activity\">{events.map(renderEvent)}</ScrollArea>\n</div>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff scrolled with unlabelled `overflow: auto` divs that keyboard users could not reach. Scrollbar colors come from tokens."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "AppShell",
            "reason": "For the page itself: let the document scroll, or use `AppShell`'s `main`."
          },
          {
            "name": "DataTable",
            "reason": "For a table: `DataTable` has its own horizontal scroll and sticky parts."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "SearchInput",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SearchInput } from 'loui-ds';",
      "folder": "src/components/SearchInput",
      "sourceFile": "src/components/SearchInput/SearchInput.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A search box: leading search icon, clear button (and `Escape` clears), optional decorative\nshortcut hint, optional suggestions with accent-insensitive match highlighting. Built on React\nAria `SearchField` (plain) or `ComboBox` (with `suggestions`).\n\n`onSubmit(text)` fires on `Enter` when no suggestion is highlighted.",
      "props": [
        {
          "name": "value",
          "type": "string",
          "required": false,
          "description": "The text. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "required": false,
          "description": "The initial text when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((value: string) => void)",
          "required": false,
          "description": "Called with the new text on every keystroke and when it is cleared."
        },
        {
          "name": "suggestions",
          "type": "readonly (string | SearchSuggestion)[]",
          "required": false,
          "description": "Suggestions shown under the box while typing, matched accent- and case-insensitively against\n`label` with the matching part highlighted. Strings are shorthand for `{ label }`. Without\nthis prop the component is a plain search field (`role=\"searchbox\"`); with it the input is a\ncombobox (`role=\"combobox\"`). Keep the array order: it is the order shown."
        },
        {
          "name": "onSelect",
          "type": "((suggestion: SearchSuggestion) => void)",
          "required": false,
          "description": "Called when a suggestion is chosen (click or `Enter` on the highlighted one)."
        },
        {
          "name": "maxResults",
          "type": "number",
          "required": false,
          "description": "Most suggestions shown. Default `6`."
        },
        {
          "name": "shortcut",
          "type": "readonly string[]",
          "required": false,
          "description": "A decorative hint of the global shortcut that focuses the search (`['Mod', 'K']`, `['/']`),\ndrawn with `Kbd` while the box is empty. It does not register the shortcut: use\n`useHotkey` and focus `inputRef` yourself. Hidden from assistive technology, since the hint\nis only useful when the shortcut exists; announce it with `aria-keyshortcuts` if it does."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the box is empty. Default from messages: \"Search\"."
        },
        {
          "name": "landmark",
          "type": "boolean",
          "required": false,
          "description": "Wraps the field in a `role=\"search\"` landmark named by the label. Use it once per page for\nthe main search of a page or site; not for filters inside a table."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the suggestions."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the `<input>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ clear: string; placeholder: string; noResults: string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `clear`, `placeholder`, `noResults`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaSearchFieldProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "enterKeyHint",
              "type": "'search' | 'enter' | 'done' | 'go' | 'next' | 'previous' | 'send'",
              "required": false
            },
            {
              "name": "type",
              "type": "\"text\" | \"search\" | \"url\" | \"tel\" | \"email\" | \"password\" | (string & {})",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTextFieldProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-activedescendant",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-autocomplete",
              "type": "'none' | 'inline' | 'list' | 'both'",
              "required": false
            },
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"false\" | \"true\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "SearchFieldProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onClear",
              "type": "() => void",
              "required": false
            },
            {
              "name": "onSubmit",
              "type": "(value: string) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 8,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMProps",
          "domGeneric": false,
          "count": 7,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            },
            {
              "name": "autoCorrect",
              "type": "string",
              "required": false
            },
            {
              "name": "inputMode",
              "type": "'text' | 'search' | 'url' | 'tel' | 'email' | 'none' | 'numeric' | 'decimal'",
              "required": false
            },
            {
              "name": "maxLength",
              "type": "number",
              "required": false
            },
            {
              "name": "minLength",
              "type": "number",
              "required": false
            },
            {
              "name": "pattern",
              "type": "string",
              "required": false
            },
            {
              "name": "spellCheck",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "SearchInput",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "clear",
            "kind": "string",
            "default": "Clear search",
            "description": "Accessible name of the clear button."
          },
          {
            "key": "placeholder",
            "kind": "string",
            "default": "Search",
            "description": "Placeholder when none is given."
          },
          {
            "key": "noResults",
            "kind": "string",
            "default": "No results",
            "description": "Shown in the suggestions list when nothing matches the text."
          }
        ]
      },
      "cssVariables": [
        "--ds-combobox-popover-max-height",
        "--ds-field-control-padding-inline",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-6",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium",
        "--ds-z-dropdown"
      ],
      "stories": {
        "title": "Components/SearchInput",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-search-input--default",
            "hasPlay": true
          },
          {
            "name": "ClearButton",
            "id": "components-search-input--clear-button",
            "hasPlay": true
          },
          {
            "name": "WithShortcutHint",
            "id": "components-search-input--with-shortcut-hint",
            "hasPlay": false
          },
          {
            "name": "Landmark",
            "id": "components-search-input--landmark",
            "hasPlay": true
          },
          {
            "name": "WithSuggestions",
            "id": "components-search-input--with-suggestions",
            "hasPlay": true
          },
          {
            "name": "AccentInsensitiveHighlight",
            "id": "components-search-input--accent-insensitive-highlight",
            "hasPlay": true
          },
          {
            "name": "NoSuggestions",
            "id": "components-search-input--no-suggestions",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-search-input--disabled",
            "hasPlay": true
          },
          {
            "name": "VisibleLabel",
            "id": "components-search-input--visible-label",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-search-input--sizes",
            "hasPlay": false
          },
          {
            "name": "FullWidthInRow",
            "id": "components-search-input--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/SearchInput/SearchInput.md",
        "summary": "A search box: leading search icon, clear button, optional decorative shortcut hint and optional suggestions with accent-insensitive match highlighting. Built on React Aria `SearchField` (plain) or `ComboBox` (with `suggestions`). Task: CMP-INP-06.",
        "whenToUse": ["Searching a list, a table or the app, with or without suggestions."],
        "whenNotToUse": [
          "Choosing a value from a list: `Combobox` (it keeps a selected value; SearchInput keeps free text).",
          "Filters with several fields: compose `Input`, `Select`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Searching a list, a table or the app, with or without suggestions."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value from a list: `Combobox` (it keeps a selected value; SearchInput keeps free text).\n- Filters with several fields: compose `Input`, `Select`."
          },
          {
            "id": "modes",
            "heading": "Modes",
            "content": "| Mode               | Element                                  | Notes                                                                                                                                                      |\n| ------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Plain (default)    | `input[type=search]`, `role=\"searchbox\"` | Clear button; `Escape` clears; `onSubmit(text)` on `Enter`.                                                                                                |\n| With `suggestions` | `role=\"combobox\"`                        | List under the box while there is text. `Escape` closes the list, then clears. `Enter` on a highlighted suggestion calls `onSelect`; otherwise `onSubmit`. |"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                                                  | Notes                                                                                                                                                                          |\n| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `aria-label` / `label` / `aria-labelledby`                                                            | One is required. Most searches use `aria-label` (\"Search products\") and no visible label.                                                                                      |\n| `value` / `defaultValue` / `onChange(text)`                                                           | Controlled / uncontrolled text.                                                                                                                                                |\n| `onSubmit(text)`, `onClear()`                                                                         | `Enter` and the clear action.                                                                                                                                                  |\n| `suggestions`, `onSelect`, `maxResults`                                                               | Strings or `{ label, meta, data }`. Matched on `label` with `matchesSearch`, highlighted with `highlightMatch`, at most `maxResults` (default 6), in the order given.          |\n| `shortcut`                                                                                            | Decorative hint such as `['Mod', 'K']`, drawn with `Kbd` while the box is empty. It registers nothing: use `useHotkey` and focus `inputRef`. Hidden from assistive technology. |\n| `landmark`                                                                                            | Wraps the field in `role=\"search\"` named by the label. Once per page.                                                                                                          |\n| `size`, `placeholder`, `name`, `isDisabled`, `isReadOnly`, `autoFocus`, `inputRef`, `ref`, `messages` | `messages`: `clear`, `placeholder`, `noResults`.                                                                                                                               |\n| `fullWidth`                                                                                           | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.                                                                             |"
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                     | Behavior                                                                    |\n| ----------------------- | --------------------------------------------------------------------------- |\n| Typing                  | Plain: updates the text. Suggestions: opens the list when there are matches |\n| `ArrowDown` / `ArrowUp` | (Suggestions) move through the list; focus stays in the input               |\n| `Enter`                 | Select the highlighted suggestion, else `onSubmit(text)`                    |\n| `Escape`                | Plain: clear. Suggestions: close the list, then clear                       |\n\nThe clear button is \"Clear search\" (from messages), out of the tab order in suggestions mode, hidden while the box is empty. Highlighted parts are `<mark>` without a background: only weight and brightness change, so contrast holds."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "`aria-label` names the scope (\"Search products\"), not the control. Placeholder \"Search\" is a hint, never the name."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<SearchInput\n  aria-label=\"Search products\"\n  value={query}\n  onChange={setQuery}\n  suggestions={products}\n  onSelect={(product) => open(product.data)}\n  shortcut={['/']}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Suggestions use the combobox pattern with ARIA wiring (the handoff list had no roles), accent-insensitive highlighting (it matched by lowercase index) and an empty state from messages.\n- Clear button, `Escape`, `role=\"search\"` option, `onSubmit` and `onClear` are new. The shortcut is a decorative `Kbd` hint; registering it is the consumer's job (`useHotkey`).\n- Uses RAC `ComboBox` rather than `Autocomplete` for suggestions: the floating list needs a popover and the ComboBox contract is already tested in `Combobox`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Combobox",
            "reason": "Choosing a value from a list: `Combobox` (it keeps a selected value; SearchInput keeps free text)."
          },
          {
            "name": "Input",
            "reason": "Filters with several fields: compose `Input`, `Select`."
          },
          {
            "name": "Select",
            "reason": "Filters with several fields: compose `Input`, `Select`."
          }
        ],
        "uses": ["Icon", "Kbd"],
        "usedBy": []
      }
    },
    {
      "name": "SegmentedControl",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SegmentedControl } from 'loui-ds';",
      "folder": "src/components/SegmentedControl",
      "sourceFile": "src/components/SegmentedControl/SegmentedControl.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A segmented control picks one value from a few options, like a radio group drawn as a\nbar. It is a value control: it does not own panels (use `Tabs` for that). Exactly one\noption is always selected.\n\nBuilt on React Aria `ToggleButtonGroup` with single selection and no empty selection:\nit is exposed as a radio group, with one tab stop and arrow-key navigation.\n\nWith `label` and/or `description` the bar is wrapped in a field-like column: the label names\nthe group, the description is linked with `aria-describedby`. Long labels never truncate:\nwhen the options are wider than the container the bar scrolls sideways.",
      "props": [
        {
          "name": "options",
          "type": "readonly SegmentedControlOption[]",
          "required": true,
          "description": "The options, in display order. Two to five fit comfortably."
        },
        {
          "name": "value",
          "type": "string",
          "required": false,
          "description": "Selected option (controlled)."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "required": false,
          "description": "Initially selected option (uncontrolled). Defaults to the first enabled option."
        },
        {
          "name": "onChange",
          "type": "(value: string) => void",
          "required": false,
          "description": "Called with the new value when the user selects another option."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the bar, linked to the group with `aria-describedby`. It can hold a\n`Kbd` to show the shortcuts of the options (see SegmentedControl.md)."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description, same as on `Input`: the text carries the meaning.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md'",
          "required": false,
          "default": "'md'",
          "description": "Height of the segments: `sm` 24px, `md` 32px (default).",
          "values": ["sm", "md"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Stretch to the container; the segments share the width equally. Without it the bar is as\nwide as its options, also inside a `Stack` (which stretches its children by default)."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the bar (the group element), also when a `label` or `description` wraps it."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the group element."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", ToggleButtonGroupRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleGroupProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-radius-sm",
        "--ds-segmented-control-height-md",
        "--ds-segmented-control-height-sm",
        "--ds-segmented-control-padding",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/SegmentedControl",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-segmented-control--default",
            "hasPlay": true
          },
          {
            "name": "Small",
            "id": "components-segmented-control--small",
            "hasPlay": false
          },
          {
            "name": "FullWidth",
            "id": "components-segmented-control--full-width",
            "hasPlay": false
          },
          {
            "name": "WithCounts",
            "id": "components-segmented-control--with-counts",
            "hasPlay": true
          },
          {
            "name": "DisabledOption",
            "id": "components-segmented-control--disabled-option",
            "hasPlay": true
          },
          {
            "name": "DisabledGroup",
            "id": "components-segmented-control--disabled-group",
            "hasPlay": true
          },
          {
            "name": "KeyboardSelection",
            "id": "components-segmented-control--keyboard-selection",
            "description": "One tab stop. Arrows move focus, Space selects, the selection can never be empty.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-segmented-control--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-segmented-control--long-labels",
            "hasPlay": false
          },
          {
            "name": "WithLabel",
            "id": "components-segmented-control--with-label",
            "description": "A visible label names the group, styled like the label of `Input`. No `aria-label` needed.",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-segmented-control--with-description",
            "hasPlay": true
          },
          {
            "name": "ToneDescription",
            "id": "components-segmented-control--tone-description",
            "hasPlay": false
          },
          {
            "name": "FourOptions",
            "id": "components-segmented-control--four-options",
            "description": "Four options in a 440 px column: every label is shown in full.",
            "hasPlay": true
          },
          {
            "name": "InsideStack",
            "id": "components-segmented-control--inside-stack",
            "description": "A `Stack` stretches its children, but the bar keeps the width of its options.",
            "hasPlay": true
          },
          {
            "name": "ShortcutHint",
            "id": "components-segmented-control--shortcut-hint",
            "description": "Shortcuts go in the description, where `Kbd` is read once and the option names stay clean.",
            "hasPlay": true
          },
          {
            "name": "PortugueseCounts",
            "id": "components-segmented-control--portuguese-counts",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-segmented-control--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-segmented-control--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/SegmentedControl/SegmentedControl.md",
        "summary": "A bar of two to five options where exactly one is selected, like a radio group drawn as buttons. It is a value control: it changes how something is shown or filtered, it does not own panels. Built on React Aria `ToggleButtonGroup` with single selection and no empty selection.\n\n- Handoff source: `components/forms/SegmentedControl.jsx` (TODO `CMP-FRM-09`).",
        "whenToUse": [
          "A view mode (List, Board, Calendar), a period (Day, Week, Month) or a small filter (All, Open, Closed).",
          "When the options are always visible and the change applies immediately."
        ],
        "whenNotToUse": [
          "To switch between panels of content: use `Tabs`.",
          "For more than five options, or labels that are whole sentences: use `Select` or `RadioGroup`. Long labels do not truncate (the bar scrolls sideways), but a scrolling bar hides options.",
          "For on/off settings: use `Switch`."
        ],
        "keyboard": [
          "Keyboard: one tab stop; `←` / `→` move focus between options, `Space` selects the focused option. React Aria does not wrap at the ends. Disabled options are skipped. The selection can never be empty."
        ],
        "examples": [
          "<SegmentedControl\n  label=\"Transaction type\"\n  description=\"Changes the fields below.\"\n  options={types}\n  value={type}\n  onChange={setType}\n/>\n\n<SegmentedControl\n  aria-label=\"View\"\n  options={[\n    { value: 'list', label: 'List' },\n    { value: 'board', label: 'Board' },\n  ]}\n  value={view}\n  onChange={setView}\n/>\n\n<SegmentedControl\n  aria-label=\"Order status\"\n  size=\"sm\"\n  options={[\n    { value: 'all', label: 'All', count: 1248 },\n    { value: 'open', label: 'Open', count: 12, countLabel: '12 open' },\n  ]}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A view mode (List, Board, Calendar), a period (Day, Week, Month) or a small filter (All, Open, Closed).\n- When the options are always visible and the change applies immediately."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To switch between panels of content: use `Tabs`.\n- For more than five options, or labels that are whole sentences: use `Select` or `RadioGroup`. Long labels do not truncate (the bar scrolls sideways), but a scrolling bar hides options.\n- For on/off settings: use `Switch`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.field                              (only with label or description)\n  span  label                          (names the group, aria-labelledby)\n  div[role=radiogroup]                 (bordered track)\n    button[role=radio] aria-checked    (one per option)\n      label\n      count                            (optional, mono)\n  span  description                    (aria-describedby, optional tone)\n```\n\nWithout `label` and `description` the track is rendered alone, named by `aria-label` or `aria-labelledby`."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                       | Default      | Notes                                                                                                                                 |\n| -------------------------------- | -------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |\n| `options`                        | `SegmentedControlOption[]` | required     | `{ value, label, count?, countLabel?, isDisabled? }`.                                                                                 |\n| `label`                          | `ReactNode`                |              | Visible label above the bar, styled like the label of `Input`. It names the group and wins over `aria-label`.                         |\n| `aria-label` / `aria-labelledby` | `string`                   | one required | Names the group (\"View\") when there is no visible `label`. One of the three is required (the types enforce it).                       |\n| `description`                    | `ReactNode`                |              | Helper text under the bar, linked with `aria-describedby`. It may contain `Kbd` (see Shortcut hints).                                 |\n| `descriptionTone`                | `FieldMessageTone`         | `'neutral'`  | Tints the description (`success`, `warning`, `danger`, `accent`). The words carry the meaning. See Field.md.                          |\n| `value` / `defaultValue`         | `string`                   | first option | Controlled / uncontrolled selection. Defaults to the first enabled option.                                                            |\n| `onChange`                       | `(value: string) => void`  | none         | Called only when the selection changes.                                                                                               |\n| `size`                           | `'sm' \\| 'md'`             | `'md'`       | Segment height: 24px (`sm`, the minimum target) or 32px (`md`).                                                                       |\n| `fullWidth`                      | `boolean`                  | `false`      | Stretch to the container; segments share the width equally. Off by default: the bar is as wide as its options, also inside a `Stack`. |\n| `isDisabled`                     | `boolean`                  | `false`      | Disables every option.                                                                                                                |\n\nCounts are formatted for the active locale. `countLabel` is the text a screen reader hears after the label (\"Open 12 open\").\n\n`className` and `ref` always point at the bar (the `radiogroup`), also when a `label` or `description` wraps it in a plain `div`. Size the field by sizing the container."
          },
          {
            "id": "width-and-overflow",
            "heading": "Width and overflow",
            "content": "- The bar is as wide as its options. It does not stretch in a `Stack` (which stretches its children) unless `fullWidth` is set.\n- Labels never truncate or wrap. When the options are wider than the container, the bar is capped at the container width and scrolls sideways; a focused option scrolls into view, and the keyboard contract does not change. With `fullWidth` the segments share the width equally but never get narrower than their label.\n- Keep labels short anyway: a scrolling bar hides options. Four short options fit in 440 px."
          },
          {
            "id": "shortcut-hints",
            "heading": "Shortcut hints",
            "content": "`count` is text or a number and cannot show a shortcut, and an option has no slot for a `Kbd`: a hint inside an option would become part of its accessible name (\"List Control 1\"). Show the shortcuts once, in the `description`, where they are read as a description of the group, and register them with `useHotkey`:\n\n```tsx\n<SegmentedControl\n  label=\"View\"\n  options={views}\n  value={view}\n  onChange={setView}\n  description={\n    <>\n      Switch with <Kbd keys={['Alt', '1']} /> <Kbd keys={['Alt', '2']} />\n    </>\n  }\n/>\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Semantics: `radiogroup` with `radio` buttons and `aria-checked`. It is deliberately not a `tablist`.\n- Keyboard: one tab stop; `←` / `→` move focus between options, `Space` selects the focused option. React Aria does not wrap at the ends. Disabled options are skipped. The selection can never be empty.\n- The visible `label` is a `span` referenced by `aria-labelledby` (a `label` element would not name a group); the description is referenced by `aria-describedby`. A consumer `aria-describedby` is kept, in front of the description.\n- A disabled control dims its label like the other fields.\n- Selected state is shown by a raised background, a line and ink text, not by color alone. The track border uses `--ds-line-control` (3:1).\n- The count uses `--ds-text-2`: `--ds-text-3` would fail 4.5:1 on the selected surface.\n- Focus: accent border and ring. Forced colors: the selected segment border uses `Highlight`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are one word or a short phrase in sentence case (\"Open\", \"This week\").\n- Same-length labels look best; avoid mixing a count on some options only.\n- With a `label`, name the setting, not the control (\"Transaction type\", not \"Segmented control\"); do not repeat it in the options."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<SegmentedControl\n  label=\"Transaction type\"\n  description=\"Changes the fields below.\"\n  options={types}\n  value={type}\n  onChange={setType}\n/>\n\n<SegmentedControl\n  aria-label=\"View\"\n  options={[\n    { value: 'list', label: 'List' },\n    { value: 'board', label: 'Board' },\n  ]}\n  value={view}\n  onChange={setView}\n/>\n\n<SegmentedControl\n  aria-label=\"Order status\"\n  size=\"sm\"\n  options={[\n    { value: 'all', label: 'All', count: 1248 },\n    { value: 'open', label: 'Open', count: 12, countLabel: '12 open' },\n  ]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `role=\"tablist\"` / `tab` is replaced by `radiogroup` / `radio`: the control sets a value, not a panel.\n- Roving focus and keyboard behavior come from React Aria; the handoff had none.\n- `onChange` receives the value, is not called when the value does not change, and the group needs an accessible name.\n- Counts are localized and can have a text alternative. Small segments are 24px (handoff 26px), medium 32px; both are tokens (`--ds-segmented-control-*`).\n- The track border is `--ds-line-control` instead of `--ds-line` to meet 3:1 for UI boundaries."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Tabs",
            "reason": "To switch between panels of content: use `Tabs`."
          },
          {
            "name": "Select",
            "reason": "For more than five options, or labels that are whole sentences: use `Select` or `RadioGroup`. Long labels do not truncate (the bar scrolls sideways), but a scrolling bar hides options."
          },
          {
            "name": "RadioGroup",
            "reason": "For more than five options, or labels that are whole sentences: use `Select` or `RadioGroup`. Long labels do not truncate (the bar scrolls sideways), but a scrolling bar hides options."
          },
          {
            "name": "Switch",
            "reason": "For on/off settings: use `Switch`."
          }
        ],
        "uses": ["VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Select",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Select } from 'loui-ds';",
      "folder": "src/components/Select",
      "sourceFile": "src/components/Select/Select.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A single-choice dropdown built on React Aria `Select`: a button that opens a listbox in a\npopover. Keyboard: Enter, Space or the arrow keys open it; typing selects by prefix (typeahead);\nEscape closes it and returns focus to the trigger. A hidden native `<select>` carries `name` and\nthe value into form submission and browser autofill.\n\nUse it for 4 or more options; for 2 to 4 prefer `RadioGroup`.",
      "props": [
        {
          "name": "options",
          "type": "readonly (SelectOption | SelectGroup)[]",
          "required": true,
          "description": "The options, or groups of options. Keep the order you want users to see (see Select.md)."
        },
        {
          "name": "value",
          "type": "string | null",
          "required": false,
          "description": "The selected `value`; `null` means nothing is selected. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "string | null",
          "required": false,
          "description": "The initially selected `value` when uncontrolled."
        },
        {
          "name": "onChange",
          "type": "((value: string | null) => void)",
          "required": false,
          "description": "Called with the new `value`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Trigger text while nothing is selected. Default from messages: \"Select an option\"."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "popoverClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the popover that holds the options."
        },
        {
          "name": "messages",
          "type": "Partial<{ placeholder: string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `placeholder`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaSelectProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            },
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", SelectRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "SelectProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "allowsEmptyCollection",
              "type": "boolean",
              "required": false
            },
            {
              "name": "defaultOpen",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isOpen",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onOpenChange",
              "type": "(isOpen: boolean) => void",
              "required": false
            },
            {
              "name": "shouldCloseOnSelect",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: Key) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Select",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "placeholder",
            "kind": "string",
            "default": "Select an option",
            "description": "Shown in the trigger while nothing is selected."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-field-control-padding-inline",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-z-dropdown"
      ],
      "stories": {
        "title": "Components/Select",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-select--default",
            "hasPlay": true
          },
          {
            "name": "KeyboardAndTypeahead",
            "id": "components-select--keyboard-and-typeahead",
            "hasPlay": true
          },
          {
            "name": "EscapeClosesAndRestoresFocus",
            "id": "components-select--escape-closes-and-restores-focus",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-select--open",
            "description": "The listbox open under the trigger, for the visual review (TEST-020).",
            "hasPlay": true
          },
          {
            "name": "WithSelection",
            "id": "components-select--with-selection",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-select--with-description",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-select--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-select--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-select--disabled",
            "hasPlay": false
          },
          {
            "name": "Sizes",
            "id": "components-select--sizes",
            "hasPlay": false
          },
          {
            "name": "DisabledOptions",
            "id": "components-select--disabled-options",
            "hasPlay": true
          },
          {
            "name": "Sections",
            "id": "components-select--sections",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-select--long-labels",
            "hasPlay": false
          },
          {
            "name": "FullWidthInRow",
            "id": "components-select--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Select/Select.md",
        "summary": "Single-choice dropdown: a button that opens a listbox in a popover. Built on React Aria `Select` (`Button`, `SelectValue`, `Popover`, `ListBox`). A hidden native `<select>` carries `name` and the value into form submission and autofill. Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": ["A choice from 4 or more options."],
        "whenNotToUse": [
          "2 to 4 options that users compare: `RadioGroup` (or a segmented control).",
          "Typing to filter a long list: `ComboBox` (planned).",
          "Several selections: `CheckboxGroup`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A choice from 4 or more options."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- 2 to 4 options that users compare: `RadioGroup` (or a segmented control).\n- Typing to filter a long list: `ComboBox` (planned).\n- Several selections: `CheckboxGroup`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                      | Notes                                                                                               |\n| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `label`, `description`, `descriptionTone`, `errorMessage` | As in `Input`.                                                                                      |\n| `options`                                                 | `SelectOption[]` (`value`, `label`, `isDisabled`) or groups `{ id, label, options }`, mixed freely. |\n| `value` / `defaultValue` / `onChange(value)`              | The selected option `value`; `null` means nothing selected. `onChange` receives `string \\| null`.   |\n| `placeholder`                                             | Trigger text while empty. Default from messages: \"Select an option\" (pt-BR \"Selecione uma opção\").  |\n| `size`                                                    | `sm` 32px, `md` 40px (default), `lg` 48px.                                                          |\n| `fullWidth`                                               | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.  |\n| `name`, `isRequired`, `isInvalid`, `isDisabled`           | Standard. `name` is submitted through the hidden `<select>`.                                        |\n| `popoverClassName`, `className`, `ref`                    | Styling hooks; `ref` is the field root.                                                             |\n| `messages`                                                | `placeholder`, `requiredMark`.                                                                      |\n\nOption order is yours: the component never reorders. The product rule that lists are alphabetical by default applies to what you pass in: use `sortByText(options, (o) => o.label, locale)` from `loui-ds` (locale- and accent-aware) unless the order carries meaning (sizes, steps)."
          },
          {
            "id": "anatomy-and-styling",
            "heading": "Anatomy and styling",
            "content": "- Trigger: the same box as `Input` (`--ds-line-control` border, focus ring, no hover fill), with a chevron.\n- Popover: raised surface, `--ds-shadow-overlay`, radius `lg`, layer `--ds-z-dropdown`, at least as wide as the trigger, scrolls above `--ds-select-popover-max-height`.\n- Option: `--ds-item-h` (36px) tall, hover and keyboard focus use `--ds-surface-hover`, the selected option shows a check (`Icon`), disabled options use the shared opacity.\n- Section captions are mono, uppercase by CSS, in `--ds-text-2` (`--ds-text-3` fails contrast on the raised surface)."
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                                      | Behavior                                                       |\n| ---------------------------------------- | -------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown`, `ArrowUp` | Open the list and focus the selected (or first/last) option    |\n| `ArrowDown` / `ArrowUp`                  | Next / previous enabled option                                 |\n| `Home` / `End`                           | First / last option                                            |\n| Letters                                  | Typeahead: focus the option starting with what you type        |\n| `Enter` / `Space`                        | Select the focused option, close, focus returns to the trigger |\n| `Escape`                                 | Close without changing; focus returns to the trigger           |\n| Typing on the closed trigger             | Selects by prefix without opening                              |\n\nThe trigger is a `button` with `aria-haspopup=\"listbox\"`, named by the label and described by the description and error. Options are `role=\"option\"` with `aria-selected`; sections are `role=\"group\"` named by their caption; disabled options have `aria-disabled`."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Select\n  label=\"Region\"\n  name=\"region\"\n  options={[\n    { value: 'eu', label: 'EU: Frankfurt' },\n    { value: 'us', label: 'US: Virginia' },\n  ]}\n  isRequired\n  errorMessage=\"Choose a region.\"\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A custom accessible listbox replaces the native `<select>` (ADR-0001), with a hidden native select kept for forms and autofill. This resolves the \"native or custom\" question of CMP-FRM-05.\n- Label association, description, error, `aria-invalid`, required, placeholder and groups are new; handoff `onChange(value, event)` is `onChange(value)`.\n- No inline chevron data URI; the chevron is an `Icon` using the icon set.\n- Focus styling is CSS and survives consumer `onFocus`/`onBlur`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "RadioGroup",
            "reason": "2 to 4 options that users compare: `RadioGroup` (or a segmented control)."
          },
          {
            "name": "CheckboxGroup",
            "reason": "Several selections: `CheckboxGroup`."
          }
        ],
        "uses": ["FieldMessages", "Icon", "Label"],
        "usedBy": []
      }
    },
    {
      "name": "Sheet",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Sheet } from 'loui-ds';",
      "folder": "src/components/Sheet",
      "sourceFile": "src/components/Sheet/Sheet.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A side panel that slides in from the inline-end edge, for viewing or editing one record\nwithout leaving the list. Built on React Aria `Modal` and `Dialog`: focus is trapped and\nrestored, the page is inert and does not scroll, and `Escape` closes only the top-most layer\n(a `Dialog` opened from a sheet closes first).\n\nThe header (title, mono meta, extra content, close button) is `--ds-topbar-h` tall; the body\nscrolls; the footer stays at the bottom.",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": true,
          "description": "Heading of the sheet and its accessible name. One line; it is truncated with an ellipsis."
        },
        {
          "name": "meta",
          "type": "ReactNode",
          "required": false,
          "description": "Short mono text after the title, such as a record code (\"ORD-1048\")."
        },
        {
          "name": "headerExtra",
          "type": "ReactNode",
          "required": false,
          "description": "Extra header content after the meta, usually a `Badge` with the status."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Optional description linked to the sheet with `aria-describedby`."
        },
        {
          "name": "children",
          "type": "ReactNode | ((options: SheetRenderOptions) => ReactNode)",
          "required": false,
          "description": "Body of the sheet; it scrolls when it is taller than the viewport. A function receives `close`."
        },
        {
          "name": "footer",
          "type": "ReactNode | ((options: SheetRenderOptions) => ReactNode)",
          "required": false,
          "description": "Footer actions, aligned to the end: Cancel (secondary) then the primary action. A function\nreceives `close`. The footer stays at the bottom while the body scrolls."
        },
        {
          "name": "footerStart",
          "type": "ReactNode | ((options: SheetRenderOptions) => ReactNode)",
          "required": false,
          "description": "Footer content aligned to the start, for the destructive action (\"Delete order\")."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'sm'",
          "description": "Width: `sm` 440px (default), `md` 460px, `lg` 480px. Full width on small screens.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "padded",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Pads the body (24px, 18px between children). Turn off for a table or a full-bleed list. Default `true`."
        },
        {
          "name": "trigger",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": false,
          "description": "The element that opens the sheet. Optional: omit it and control the sheet with `isOpen`."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the sheet opens or closes."
        },
        {
          "name": "isDismissable",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Whether pressing the scrim closes the sheet. Default `true`."
        },
        {
          "name": "isKeyboardDismissDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables closing with `Escape`. Use it only while a request is running."
        },
        {
          "name": "messages",
          "type": "Partial<{ close: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the sheet panel."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the sheet panel."
        }
      ],
      "inheritedProps": [],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Sheet",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "close",
            "kind": "string",
            "default": "Close panel",
            "description": "Name of the close button in the Sheet header."
          }
        ]
      },
      "cssVariables": [
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-line-subtle",
        "--ds-scrim",
        "--ds-shadow-dialog",
        "--ds-sheet-from",
        "--ds-sheet-padding",
        "--ds-sheet-width",
        "--ds-sheet-width-lg",
        "--ds-sheet-width-md",
        "--ds-sheet-width-sm",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-topbar-h",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Sheet",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-sheet--default",
            "description": "Open, focus trapped inside, Escape closes, focus returns to the trigger.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-sheet--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Medium",
            "id": "components-sheet--medium",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-sheet--large",
            "hasPlay": false
          },
          {
            "name": "CloseButton",
            "id": "components-sheet--close-button",
            "hasPlay": true
          },
          {
            "name": "LongContent",
            "id": "components-sheet--long-content",
            "description": "Long content scrolls in the body; the header and the footer stay in view.",
            "hasPlay": false
          },
          {
            "name": "NotPadded",
            "id": "components-sheet--not-padded",
            "description": "A list or table that bleeds to the edges.",
            "hasPlay": false
          },
          {
            "name": "LongTitle",
            "id": "components-sheet--long-title",
            "hasPlay": false
          },
          {
            "name": "WithDescription",
            "id": "components-sheet--with-description",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-sheet--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Sheet/Sheet.md",
        "summary": "A side panel that slides in from the inline-end edge to view or edit one record without leaving the list. Built on React Aria `Modal` and `Dialog`. Generic: it lives in the core entry point.\n\n- Handoff source: `components/erp/Sheet` (CMP-PAT-01).",
        "whenToUse": [
          "A record's details or edit form next to the list it came from.",
          "Filters or settings with a footer of actions."
        ],
        "whenNotToUse": [
          "A short confirmation or a small form: use `Dialog`.",
          "A hint or a few controls on a trigger: use `Popover`.",
          "Page-level navigation: use `Sidebar`."
        ],
        "keyboard": [
          "Same overlay contract as `Dialog`: focus trapped and restored, `inert` page, scroll lock, `Escape` closes only the top layer (a `Dialog` opened inside the sheet closes first; tested in `Dialog.test.tsx`)."
        ],
        "examples": [
          "<Sheet\n  trigger={<Button>View order</Button>}\n  title=\"Order\"\n  meta=\"ORD-1048\"\n  headerExtra={<Badge tone=\"success\">Shipped</Badge>}\n  footerStart={<Button variant=\"danger\">Delete order</Button>}\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>\n        Save order\n      </Button>\n    </>\n  )}\n>\n  <Input label=\"Customer\" />\n</Sheet>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A record's details or edit form next to the list it came from.\n- Filters or settings with a footer of actions."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A short confirmation or a small form: use `Dialog`.\n- A hint or a few controls on a trigger: use `Popover`.\n- Page-level navigation: use `Sidebar`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nDialogTrigger\n  trigger\n  ModalOverlay       --ds-scrim, z sheet\n    Modal            surface, strong line at the start edge, dialog shadow, width sm | md | lg\n      Dialog         role=dialog, aria-labelledby = title\n        header       --ds-topbar-h tall: title (ellipsis) · meta (mono) · headerExtra · close IconButton\n        body         scrolls; padded (24px) or not\n        footer       sticky: footerStart (destructive) ... footer (Cancel + primary)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                      | Type                                    | Default | Notes                                                                     |\n| ----------------------------------------- | --------------------------------------- | ------- | ------------------------------------------------------------------------- |\n| `title`                                   | `ReactNode` (required)                  | none    | Heading and accessible name (not limited to strings, unlike the handoff). |\n| `meta`                                    | `ReactNode`                             |         | Mono text after the title (\"ORD-1048\").                                   |\n| `headerExtra`                             | `ReactNode`                             |         | Usually a `Badge`.                                                        |\n| `description`                             | `ReactNode`                             |         | Linked with `aria-describedby`.                                           |\n| `children`                                | `ReactNode \\| ({ close }) => ReactNode` |         | Body.                                                                     |\n| `footer` / `footerStart`                  | `ReactNode \\| ({ close }) => ReactNode` |         | End / start aligned actions.                                              |\n| `size`                                    | `'sm' \\| 'md' \\| 'lg'`                  | `sm`    | 440 / 460 / 480 px (`--ds-sheet-width-*`); full width on small screens.   |\n| `padded`                                  | `boolean`                               | `true`  | `false` for a table or list that bleeds to the edges.                     |\n| `trigger`                                 | `ReactElement`                          |         | Optional when controlled.                                                 |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                         |         | Controlled / uncontrolled open state.                                     |\n| `isDismissable`                           | `boolean`                               | `true`  | Press on the scrim closes it.                                             |\n| `isKeyboardDismissDisabled`               | `boolean`                               | `false` |                                                                           |\n| `messages`                                | `{ close }`                             |         | \"Close panel\" (pt-BR \"Fechar painel\").                                    |"
          },
          {
            "id": "initial-focus",
            "heading": "Initial focus",
            "content": "React Aria moves focus into the sheet on open: to the first element with `autoFocus` if there is one, otherwise to the dialog itself. This holds for every way of opening it (keyboard or pointer on the trigger, `element.click()` from code, and a controlled sheet without a trigger whose `isOpen` is set from an effect or a timer); regression tests cover each one (GAP-031). If `autoFocus` seems lost in your own tests, the cause is the test environment, not `Sheet`: wait for the dialog to be visible before asserting focus (the field is focused after the overlay mounts)."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Same overlay contract as `Dialog`: focus trapped and restored, `inert` page, scroll lock, `Escape` closes only the top layer (a `Dialog` opened inside the sheet closes first; tested in `Dialog.test.tsx`).\n- Named by the title through `aria-labelledby`, so a non-string title works (the handoff only labeled string titles).\n- The close button is an `IconButton` named from messages. The handoff set `title=\"Close (Esc)\"`.\n- Slide-in is a 150 ms transform and the scrim fades; both are removed under `prefers-reduced-motion`. The slide direction follows the writing direction (`:dir(rtl)`).\n- The panel is `Modal` content inside the overlay, so it stacks above the scrim without a second z-index.\n- See the Dialog doc (\"Layers\") for popovers opened from inside the sheet.\n- **Exit animation.** The sheet stays mounted for the 150 ms slide-out. It keeps rendering the props of its last open render during that time, so you can clear the state that feeds it (`setRecord(null)`) in the same update that closes it and the panel will not animate out empty (no blank fields or title). Reopening shows the new content at once. Under `prefers-reduced-motion` there is no animation, so it leaves the DOM immediately (tested). If the tab is hidden the browser does not run animations and React Aria unmounts the sheet only once the tab is visible again.\n\nControlled use without a `trigger` renders the overlay directly, without a `DialogTrigger`, so opening the sheet logs nothing to the console (GAP-022). Focus still returns to the element focused before it opened."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: the record type or name, sentence case. Meta: its code, as data.\n- Footer: destructive action at the start (\"Delete order\"); Cancel then the primary action at the end (\"Save order\"). Deleting still goes through a `ConfirmDialog`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Sheet\n  trigger={<Button>View order</Button>}\n  title=\"Order\"\n  meta=\"ORD-1048\"\n  headerExtra={<Badge tone=\"success\">Shipped</Badge>}\n  footerStart={<Button variant=\"danger\">Delete order</Button>}\n  footer={({ close }) => (\n    <>\n      <Button onPress={close}>Cancel</Button>\n      <Button variant=\"primary\" onPress={save}>\n        Save order\n      </Button>\n    </>\n  )}\n>\n  <Input label=\"Customer\" />\n</Sheet>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria `Modal`: focus trap/restore, `inert`, scroll lock, stack-aware `Escape` (the handoff added a window listener per sheet).\n- `width` (440 / 460 / 480 ERP-specific values) became `size` (`sm` / `md` / `lg`) with component tokens; header height is `--ds-topbar-h`; layers come from `--ds-z-sheet`.\n- `contained` mode is not ported. `onClose` is replaced by `onOpenChange`.\n- The scrim is `--ds-scrim`, no blur; the panel is `--ds-surface` with the dialog shadow.\n- New: `description`, `footerStart`, controlled state, close label from messages, RTL slide."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Dialog",
            "reason": "A short confirmation or a small form: use `Dialog`."
          },
          {
            "name": "Popover",
            "reason": "A hint or a few controls on a trigger: use `Popover`."
          },
          {
            "name": "Sidebar",
            "reason": "Page-level navigation: use `Sidebar`."
          }
        ],
        "uses": ["IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "ShortcutsHelp",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ShortcutsHelp } from 'loui-ds';",
      "folder": "src/components/ShortcutsHelp",
      "sourceFile": "src/components/ShortcutsHelp/ShortcutsHelp.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A dialog that lists the registered keyboard shortcuts (`useHotkeys` with a `description`),\ngrouped by their `group`, one table per group with the keys drawn by `Kbd`. Opens with `?` by\ndefault. Place it once, anywhere under the app, and it stays in sync with what is mounted.",
      "props": [
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "((isOpen: boolean) => void)",
          "required": false,
          "description": "Called when the dialog opens (from the hotkey) or closes."
        },
        {
          "name": "hotkey",
          "type": "string | false",
          "required": false,
          "default": "?",
          "description": "The shortcut that opens the dialog, registered in the hotkey registry. Default `'?'`.\nIt does not fire while an input has focus, so typing a question mark still works.\nPass `false` to open the dialog only through `isOpen`."
        },
        {
          "name": "includeInactive",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Also list shortcuts whose scope is not active right now. Default `false`."
        },
        {
          "name": "messages",
          "type": "Partial<{ title: string; description: string; ungrouped: string; empty: string; close: string; action: string; keys: string; openDescription: string; openGroup: string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the dialog panel."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "ShortcutsHelp",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "title",
            "kind": "string",
            "default": "Keyboard shortcuts",
            "description": "Title of the dialog."
          },
          {
            "key": "description",
            "kind": "string",
            "default": "Shortcuts that work on this page.",
            "description": "Description under the title."
          },
          {
            "key": "ungrouped",
            "kind": "string",
            "default": "Other",
            "description": "Heading for shortcuts that have no `group`."
          },
          {
            "key": "empty",
            "kind": "string",
            "default": "No keyboard shortcuts on this page.",
            "description": "Shown when no shortcut is registered."
          },
          {
            "key": "close",
            "kind": "string",
            "default": "Close",
            "description": "Label of the button that closes the dialog."
          },
          {
            "key": "action",
            "kind": "string",
            "default": "Action",
            "description": "Column header: what the shortcut does."
          },
          {
            "key": "keys",
            "kind": "string",
            "default": "Shortcut",
            "description": "Column header: the keys."
          },
          {
            "key": "openDescription",
            "kind": "string",
            "default": "Show keyboard shortcuts",
            "description": "Description of the shortcut that opens the dialog."
          },
          {
            "key": "openGroup",
            "kind": "string",
            "default": "General",
            "description": "Group of the shortcut that opens the dialog."
          }
        ]
      },
      "cssVariables": [
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-lh-caption",
        "--ds-line-subtle",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/ShortcutsHelp",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-shortcuts-help--default",
            "description": "`?` opens the dialog from anywhere outside an input; the page shortcuts stay quiet while it is open.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-shortcuts-help--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-shortcuts-help--empty",
            "description": "No shortcut is registered.",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-shortcuts-help--portuguese-brazil",
            "description": "Brazilian Portuguese: title, group fallback, column headers (hidden) and the spoken key names.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ShortcutsHelp/ShortcutsHelp.md",
        "summary": "A dialog that lists the keyboard shortcuts registered on the page, one table per group, with the keys drawn by `Kbd`. It opens with `?` by default.",
        "whenToUse": [
          "Once per app (or per module), next to the `CommandPalette`, so users can learn the shortcuts without leaving the page."
        ],
        "whenNotToUse": [
          "To register shortcuts: use `useHotkeys`. This only lists what is registered.",
          "As the only way to find a command: the `CommandPalette` also searches by name."
        ],
        "keyboard": ["`Escape` and the Close button close it."],
        "examples": ["<HotkeyProvider>\n  <App />\n  <ShortcutsHelp />\n</HotkeyProvider>"],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Once per app (or per module), next to the `CommandPalette`, so users can learn the shortcuts without leaving the page."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To register shortcuts: use `useHotkeys`. This only lists what is registered.\n- As the only way to find a command: the `CommandPalette` also searches by name."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nDialog \"Keyboard shortcuts\"  (size md, Close button)\n  table  caption = group\n    tr   th[scope=row] description · td Kbd keys\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop              | Type                        | Default | Notes                                                                |\n| ----------------- | --------------------------- | ------- | -------------------------------------------------------------------- |\n| `isOpen`          | `boolean`                   | -       | Controlled open state. Pair with `onOpenChange`                      |\n| `defaultOpen`     | `boolean`                   | `false` | Initial state when uncontrolled                                      |\n| `onOpenChange`    | `(isOpen: boolean) => void` | -       | Called on open (from the hotkey) and close                           |\n| `hotkey`          | `string \\| false`           | `'?'`   | Registered in the hotkey registry. Not fired while typing in a field |\n| `includeInactive` | `boolean`                   | `false` | Also lists shortcuts whose scope is not active now                   |\n| `messages`        | `Partial<messages>`         | -       | Per-instance overrides                                               |"
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- Lists shortcuts that have a `description` (`useHotkeys`). Groups are sorted with the locale; shortcuts without a `group` go under \"Other\", last. Rows are sorted by description.\n- Updates while it is open: shortcuts that mount or unmount appear or disappear.\n- It lists itself (\"Show keyboard shortcuts\") and the `CommandPalette` shortcut.\n- While it is open, page shortcuts do not fire."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A modal `dialog` named \"Keyboard shortcuts\" with a description; focus is trapped and returns on close.\n- Each group is a real `<table>` named by its `<caption>`; the description is the row header, the keys are the cell. The column headers exist for screen readers and are visually hidden.\n- Keys use `Kbd`, with spoken names for symbols.\n- `Escape` and the Close button close it."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Descriptions are verb plus object in sentence case (\"Create order\"), as in the command palette.\n- Messages: `title`, `description`, `ungrouped`, `empty`, `close`, `action`, `keys`, `openDescription`, `openGroup`. English and pt-BR ship."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<HotkeyProvider>\n  <App />\n  <ShortcutsHelp />\n</HotkeyProvider>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff drew a static keyboard card. This list is generated from the registry, so it cannot drift from the real shortcuts."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "CommandPalette",
            "reason": "As the only way to find a command: the `CommandPalette` also searches by name."
          }
        ],
        "uses": ["Button", "Dialog", "Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Sidebar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Sidebar } from 'loui-ds';",
      "folder": "src/components/Sidebar",
      "sourceFile": "src/components/Sidebar/Sidebar.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The app sidebar: a workspace block, navigation sections, an optional status card, a footer\nslot and the signed-in user. It is an `aside` landmark; each section is a separate `nav`\nlandmark named by its caption. It is `--ds-sidebar-w` wide and scrolls on its own when it is\ntaller than its container, so give the parent a fixed height.\n\nReading order and focus order follow the visual order: sections top to bottom, then the\nfooter. Collapsing or an off-canvas mobile variant is not part of this component.",
      "props": [
        {
          "name": "workspace",
          "type": "SidebarWorkspace",
          "required": true,
          "description": "The workspace block at the top."
        },
        {
          "name": "sections",
          "type": "readonly SidebarSection[]",
          "required": true,
          "description": "Navigation groups. Each one is its own labelled `nav` landmark."
        },
        {
          "name": "value",
          "type": "string",
          "required": false,
          "description": "`value` of the active item (gets `aria-current=\"page\"`)."
        },
        {
          "name": "onChange",
          "type": "(value: string) => void",
          "required": false,
          "description": "Called with the `value` of the item the user pressed."
        },
        {
          "name": "status",
          "type": "SidebarStatus",
          "required": false,
          "description": "Optional status card above the footer."
        },
        {
          "name": "footer",
          "type": "ReactNode",
          "required": false,
          "description": "Free slot above the user block, for example a help link or an upgrade note."
        },
        {
          "name": "user",
          "type": "SidebarUser",
          "required": false,
          "description": "The signed-in user, at the bottom."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Overrides the accessible name of the landmark (default \"Sidebar\" from the catalog)."
        },
        {
          "name": "messages",
          "type": "Partial<{ label: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Sidebar",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "label",
            "kind": "string",
            "default": "Sidebar",
            "description": "Accessible name of the sidebar landmark."
          }
        ]
      },
      "cssVariables": [
        "--ds-bg",
        "--ds-danger",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line",
        "--ds-line-subtle",
        "--ds-nav-item-padding-inline",
        "--ds-primary-bg",
        "--ds-primary-fg",
        "--ds-radius-lg",
        "--ds-radius-md",
        "--ds-radius-pill",
        "--ds-sidebar-gap",
        "--ds-sidebar-padding-block",
        "--ds-sidebar-padding-inline",
        "--ds-sidebar-status-dot-size",
        "--ds-sidebar-w",
        "--ds-sidebar-workspace-mark-size",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-warning",
        "--ds-weight-medium",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Sidebar",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-sidebar--default",
            "hasPlay": true
          },
          {
            "name": "Complete",
            "id": "components-sidebar--complete",
            "hasPlay": false
          },
          {
            "name": "StatusTones",
            "id": "components-sidebar--status-tones",
            "hasPlay": false
          },
          {
            "name": "StatusNeutral",
            "id": "components-sidebar--status-neutral",
            "hasPlay": false
          },
          {
            "name": "WithoutCaptions",
            "id": "components-sidebar--without-captions",
            "hasPlay": true
          },
          {
            "name": "FocusOrder",
            "id": "components-sidebar--focus-order",
            "description": "Focus order follows the visual order: sections top to bottom, then the footer.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-sidebar--controlled",
            "hasPlay": true
          },
          {
            "name": "ManyItems",
            "id": "components-sidebar--many-items",
            "description": "Taller content scrolls inside the sidebar; the page does not.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-sidebar--long-text",
            "hasPlay": false
          },
          {
            "name": "Portuguese",
            "id": "components-sidebar--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-sidebar--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Sidebar/Sidebar.md",
        "summary": "The app sidebar: workspace block, navigation sections, optional status card, footer slot and the signed-in user. It is an `aside` landmark and each section is its own `nav`.\n\n- Handoff source: `components/layout/Sidebar.jsx` (TODO `CMP-LAY-01`).",
        "whenToUse": [
          "The persistent primary navigation of an app shell, next to the content and below a `SkipLink`."
        ],
        "whenNotToUse": [
          "For in-page navigation or a table of contents.",
          "For mobile off-canvas navigation: collapsing and responsive behavior are not part of this component yet."
        ],
        "keyboard": [],
        "examples": [
          "<Sidebar\n  workspace={{ name: 'Northwind', subtitle: 'Production' }}\n  value=\"orders\"\n  onChange={setView}\n  sections={[\n    {\n      id: 'work',\n      title: 'Work',\n      items: [\n        {\n          value: 'orders',\n          label: 'Orders',\n          href: '/orders',\n          icon: StarIcon,\n          count: 3,\n          countLabel: '3 pending',\n          countTone: 'warning',\n        },\n        { value: 'customers', label: 'Customers', href: '/customers' },\n      ],\n    },\n  ]}\n  status={{ title: 'All systems operational', meta: 'Last sync', value: '2 min ago' }}\n  user={{ name: 'Ana Souza', role: 'Owner' }}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The persistent primary navigation of an app shell, next to the content and below a `SkipLink`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For in-page navigation or a table of contents.\n- For mobile off-canvas navigation: collapsing and responsive behavior are not part of this component yet."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\naside[aria-label=\"Sidebar\"]         (--ds-sidebar-w wide, scrolls on its own)\n  workspace                         (initial tile, name, subtitle)\n  nav[aria-labelledby=caption]      (one per section)\n    caption                         (mono, uppercase by CSS)\n    ul > li > NavItem\n  bottom\n    status                          (role=status: dot + title + meta/value)\n    footer                          (slot)\n    user                            (Avatar + name + role)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                              | Notes                                                                                       |\n| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------- |\n| `workspace`  | `{ name, subtitle?, initial? }`   | `initial` defaults to the first letter of the name (locale upper-casing).                   |\n| `sections`   | `SidebarSection[]`                | `{ id, title, items }` or `{ id, label, items }` (one of `title` / `label` is required).    |\n| `value`      | `string`                          | `value` of the active item.                                                                 |\n| `onChange`   | `(value: string) => void`         | Called with the item the user pressed (links and buttons).                                  |\n| `status`     | `{ tone?, title, meta?, value? }` | `tone`: `neutral` (default), `success`, `warning`, `danger`. The title carries the meaning. |\n| `footer`     | `ReactNode`                       | Free slot above the user block.                                                             |\n| `user`       | `{ name, role?, avatarSrc? }`     | Rendered with a decorative `Avatar` (the name is next to it).                               |\n| `aria-label` | `string`                          | Overrides the landmark name (default \"Sidebar\" from the catalog).                           |\n| `messages`   | `{ label?: string }`              | Per-instance message override (ADR-0005).                                                   |\n\nA `SidebarItem` is `{ value, label, href?, icon?, count?, countLabel?, countTone?, isDisabled? }`; it renders a `NavItem`. Always set `countLabel` with `count`."
          },
          {
            "id": "layout",
            "heading": "Layout",
            "content": "- Width is `--ds-sidebar-w` (240px). The handoff's `width` prop and 248px default are dropped; there is one width in the system.\n- The sidebar uses `max-block-size: 100%` and `overflow-y: auto`: give its parent a definite height (for example a full-height flex row) and it scrolls on its own while the content scrolls separately. The status, footer and user stay at the bottom when there is room."
          },
          {
            "id": "behavior-change",
            "heading": "Behavior change",
            "content": "The status dot is now grey (`neutral`) when `tone` is omitted; it used to default to green. Pass `tone: 'success'` to keep the green dot."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Landmarks: `aside` (complementary) named \"Sidebar\", and one `nav` per section named by its visible caption (or by `label` when there is no caption). Names are unique, so landmark lists are meaningful.\n- Items are in a `ul`, so screen readers announce \"list, 5 items\".\n- Focus order follows the visual order: sections top to bottom, then the footer, then nothing in the status or user blocks (they are text).\n- The status block is `role=\"status\"` (polite live region): a change from \"Synced\" to \"Sync failed\" is announced. The dot is `aria-hidden`; the title says the status in words.\n- The workspace tile and the user avatar are decorative because the names are shown next to them.\n- The active item has `aria-current=\"page\"` (see `NavItem`)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Section captions are written in sentence case (\"Work\"); the CSS makes them uppercase mono.\n- Status titles say the state: \"All systems operational\", not \"OK\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Sidebar\n  workspace={{ name: 'Northwind', subtitle: 'Production' }}\n  value=\"orders\"\n  onChange={setView}\n  sections={[\n    {\n      id: 'work',\n      title: 'Work',\n      items: [\n        {\n          value: 'orders',\n          label: 'Orders',\n          href: '/orders',\n          icon: StarIcon,\n          count: 3,\n          countLabel: '3 pending',\n          countTone: 'warning',\n        },\n        { value: 'customers', label: 'Customers', href: '/customers' },\n      ],\n    },\n  ]}\n  status={{ title: 'All systems operational', meta: 'Last sync', value: '2 min ago' }}\n  user={{ name: 'Ana Souza', role: 'Owner' }}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Width is the token (240px) instead of a 248px default prop.\n- `aside` + one labelled `nav` per section + lists, instead of anonymous `nav`s with `div` captions.\n- Items are `NavItem`s (real links); the user avatar is the `Avatar` component; the status card is a `role=\"status\"` region with a decorative dot.\n- Not included yet: collapsed/icon-only mode and a responsive off-canvas variant (needs its own decision)."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["Avatar", "NavItem"],
        "usedBy": []
      }
    },
    {
      "name": "Skeleton",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Skeleton } from 'loui-ds';",
      "folder": "src/components/Skeleton",
      "sourceFile": "src/components/Skeleton/Skeleton.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A placeholder block in the shape of content that is still loading. It is always hidden\nfrom assistive technology: mark the region that is loading with `aria-busy=\"true\"` and\nremove the attribute (and the skeletons) when the content arrives.\n\nThe `width` and `height` props are the only dynamic values in the library that reach the\nDOM as custom properties on `style`, because arbitrary sizes cannot be tokens.",
      "props": [
        {
          "name": "width",
          "type": "string | number",
          "required": false,
          "description": "Width: a number in px or any CSS length. Default `100%`. Ignored when `circle` is set (the circle is as wide as it is tall)."
        },
        {
          "name": "height",
          "type": "string | number",
          "required": false,
          "description": "Height of the block, or of each line: a number in px or any CSS length. Default `12px` (`--ds-skeleton-height`)."
        },
        {
          "name": "circle",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Renders a circle, for an avatar. The diameter is `height`."
        },
        {
          "name": "lines",
          "type": "number",
          "required": false,
          "description": "Renders this many text lines; the last one is shorter. Takes precedence over `circle`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-skeleton-height",
        "--ds-skeleton-pulse-duration",
        "--ds-skeleton-pulse-opacity",
        "--ds-space-2",
        "--ds-surface-hover"
      ],
      "stories": {
        "title": "Components/Skeleton",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-skeleton--default",
            "hasPlay": false
          },
          {
            "name": "Block",
            "id": "components-skeleton--block",
            "hasPlay": false
          },
          {
            "name": "Circle",
            "id": "components-skeleton--circle",
            "hasPlay": false
          },
          {
            "name": "Lines",
            "id": "components-skeleton--lines",
            "hasPlay": false
          },
          {
            "name": "SingleLine",
            "id": "components-skeleton--single-line",
            "hasPlay": false
          },
          {
            "name": "Pill",
            "id": "components-skeleton--pill",
            "hasPlay": false
          },
          {
            "name": "BusyRegion",
            "id": "components-skeleton--busy-region",
            "description": "The loading region carries `aria-busy`; the skeletons themselves are hidden from assistive technology.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-skeleton--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Skeleton/Skeleton.md",
        "summary": "A placeholder in the shape of content that is still loading: a block, a circle, or a few text lines.",
        "whenToUse": [
          "The layout of the content is known (a list of rows, a card, an avatar next to text) and the wait is short.",
          "Keep the page from jumping when the data arrives."
        ],
        "whenNotToUse": [
          "For an action in progress (saving, sending): use `Spinner` or a loading `Button`.",
          "For a measurable task: use `Progress`.",
          "When there is nothing to show after loading: use `EmptyState`."
        ],
        "keyboard": [],
        "examples": [
          "<section aria-busy={isLoading} aria-label=\"Recent orders\">\n  {isLoading ? (\n    <>\n      <Skeleton circle height={28} />\n      <Skeleton lines={3} />\n    </>\n  ) : (\n    <OrderList orders={orders} />\n  )}\n</section>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The layout of the content is known (a list of rows, a card, an avatar next to text) and the wait is short.\n- Keep the page from jumping when the data arrives."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For an action in progress (saving, sending): use `Spinner` or a loading `Button`.\n- For a measurable task: use `Progress`.\n- When there is nothing to show after loading: use `EmptyState`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan[aria-hidden=\"true\"]  block | circle\nspan[aria-hidden=\"true\"]  lines → n × span (last one is 60% wide)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop     | Type               | Default | Notes                                                             |\n| -------- | ------------------ | ------- | ----------------------------------------------------------------- |\n| `width`  | `number \\| string` | `100%`  | Number is px. Ignored with `circle`                               |\n| `height` | `number \\| string` | `12px`  | Height of the block or of each line (`--ds-skeleton-height`)      |\n| `circle` | `boolean`          | `false` | Diameter is `height`                                              |\n| `lines`  | `number`           |         | Text lines; the last is shorter (not when there is only one line) |\n\nOther `span` attributes and `ref` go to the root. `width` and `height` reach the DOM as custom properties on `style` (the only dynamic styles in the component); everything else is CSS."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Every skeleton is `aria-hidden=\"true\"`: it is decoration and has no text.\n- **Mark the region that is loading with `aria-busy=\"true\"`** and remove it, together with the skeletons, when the content arrives. This tells assistive technology the region is being updated. A skeleton on its own does not do this.\n- If the wait is long, also expose a polite status (\"Loading orders\") near the region; skeletons never announce it.\n- The pulse (opacity only) is switched off under `prefers-reduced-motion`. In forced-colors mode the placeholder gets a border because background colors are dropped."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Match the final layout: the same number of rows and similar widths, so nothing moves when the data arrives.\n- Show at most one screenful of placeholders."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<section aria-busy={isLoading} aria-label=\"Recent orders\">\n  {isLoading ? (\n    <>\n      <Skeleton circle height={28} />\n      <Skeleton lines={3} />\n    </>\n  ) : (\n    <OrderList orders={orders} />\n  )}\n</section>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Always hidden from assistive technology (the handoff hid only the single block, not the `lines` variant).\n- Reduced motion switches the pulse off; the keyframes are local to the component.\n- Pulse duration and default height are component tokens.\n- No `radius` or `style` props. A `SkeletonGroup` wrapper was not added: `aria-busy` on your own region is one attribute, and a wrapper would hide which element is busy.\n- Presets (avatar, row, card) are compositions of `circle`, `lines` and `width`/`height`; see the busy-region story."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Spinner",
            "reason": "For an action in progress (saving, sending): use `Spinner` or a loading `Button`."
          },
          {
            "name": "Button",
            "reason": "For an action in progress (saving, sending): use `Spinner` or a loading `Button`."
          },
          {
            "name": "Progress",
            "reason": "For a measurable task: use `Progress`."
          },
          {
            "name": "EmptyState",
            "reason": "When there is nothing to show after loading: use `EmptyState`."
          }
        ],
        "uses": [],
        "usedBy": ["DataTable"]
      }
    },
    {
      "name": "SkipLink",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SkipLink } from 'loui-ds';",
      "folder": "src/components/SkipLink",
      "sourceFile": "src/components/SkipLink/SkipLink.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A link that is invisible until a keyboard user focuses it, then jumps past the repeated\nnavigation to the main content (WCAG 2.4.1 Bypass Blocks). Put it as the very first\nfocusable element of the page, before the sidebar and the top bar.\n\nIt moves focus itself instead of relying on the hash navigation, so it also works with hash\nbased routers and with a target that is not focusable by default.",
      "props": [
        {
          "name": "targetId",
          "type": "string",
          "required": false,
          "default": "main",
          "description": "Id of the element to jump to, without the `#`. Default `main`. The element should be the\npage's `<main>` (or its first heading); it does not need a `tabindex`."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Link text. Defaults to \"Skip to main content\" from the message catalog."
        },
        {
          "name": "messages",
          "type": "Partial<{ label: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AnchorHTMLAttributes",
          "domGeneric": true,
          "count": 7
        },
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "SkipLink",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "label",
            "kind": "string",
            "default": "Skip to main content",
            "description": "Default text of the skip link."
          }
        ]
      },
      "cssVariables": [
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-primary-bg",
        "--ds-primary-fg",
        "--ds-radius-md",
        "--ds-skip-link-z-index",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/SkipLink",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-skip-link--default",
            "description": "Hidden until it receives keyboard focus: press Tab.",
            "hasPlay": true
          },
          {
            "name": "JumpsToMain",
            "id": "components-skip-link--jumps-to-main",
            "description": "Enter jumps past the navigation: focus lands on the main content.",
            "hasPlay": true
          },
          {
            "name": "CustomTarget",
            "id": "components-skip-link--custom-target",
            "hasPlay": true
          },
          {
            "name": "Portuguese",
            "id": "components-skip-link--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-skip-link--right-to-left",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-skip-link--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/SkipLink/SkipLink.md",
        "summary": "A link that stays invisible until a keyboard user focuses it, then jumps past the repeated navigation to the main content (WCAG 2.4.1 Bypass Blocks).\n\n- Source: TODO `BASE-002` (there was no handoff component).",
        "whenToUse": [
          "Once per page, as the first focusable element, before the sidebar and the top bar."
        ],
        "whenNotToUse": ["As a general \"go to section\" link: use `Link`."],
        "keyboard": ["It is a real link, so it appears in link lists and works with `Enter`."],
        "examples": ["<>\n  <SkipLink />\n  <Sidebar … />\n  <main id=\"main\">…</main>\n</>"],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Once per page, as the first focusable element, before the sidebar and the top bar."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- As a general \"go to section\" link: use `Link`."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop       | Type                 | Default                | Notes                                                                    |\n| ---------- | -------------------- | ---------------------- | ------------------------------------------------------------------------ |\n| `targetId` | `string`             | `'main'`               | Id of the element to jump to, without `#`. Becomes `href=\"#<targetId>\"`. |\n| `children` | `ReactNode`          | \"Skip to main content\" | Custom text. Default comes from the catalog (English and pt-BR).         |\n| `messages` | `{ label?: string }` | catalog                | Per-instance override (ADR-0005).                                        |\n| `onClick`  | click handler        | none                   | Runs first; `preventDefault()` cancels the jump.                         |\n\n`ref` points to the `<a>`; `className` and other anchor attributes pass through."
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "- Hidden with the same technique as `VisuallyHidden` (1px, clipped), so it is still focusable. On focus it becomes a primary-colored button at the top start corner, above everything (`--ds-skip-link-z-index`).\n- On activation it focuses the target element itself and cancels the hash navigation. This works with hash-based routers and with targets that are not focusable (a temporary `tabindex=\"-1\"` is added and removed on blur; an existing `tabindex` is kept). If the target does not exist, the browser's default behavior runs.\n- Give the target `id=\"main\"`; put it on `<main>`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- It is a real link, so it appears in link lists and works with `Enter`.\n- Visible focus with the accent ring; forced colors use `Highlight`.\n- The text says where it goes. Do not shorten it to \"Skip\"."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- \"Skip to main content\" (default). For a second link use the destination: \"Skip to the results\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<>\n  <SkipLink />\n  <Sidebar … />\n  <main id=\"main\">…</main>\n</>\n```"
          },
          {
            "id": "ds-sr-only-base-002",
            "heading": "`.ds-sr-only` (BASE-002)",
            "content": "The TODO asks for a `.ds-sr-only` utility class too. It belongs in `src/styles/base.css` (`@layer utilities`), a shared file this component does not own. The rule to add is the same as `.root` of `VisuallyHidden` (absolute, 1px, `clip-path: inset(50%)`, no overflow)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Link",
            "reason": "As a general \"go to section\" link: use `Link`."
          }
        ],
        "uses": [],
        "usedBy": ["AppShell", "AppShellNavToggle"]
      }
    },
    {
      "name": "Slider",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Slider } from 'loui-ds';",
      "folder": "src/components/Slider",
      "sourceFile": "src/components/Slider/Slider.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "A single-value slider built on React Aria `Slider`: a labelled track with one thumb that is a\nreal `<input type=\"range\">`, so keyboard, pointer, touch and form submission (`name`) work\nnatively. Keys: arrows step by `step`, `PageUp`/`PageDown` move by a larger step (10% of the\nrange, at least `step`), `Home`/`End` jump to the limits.\n\nRange selection with two thumbs is not part of this component yet.",
      "props": [
        {
          "name": "value",
          "type": "number",
          "required": false,
          "description": "The value when controlled. Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "number",
          "required": false,
          "description": "The initial value when uncontrolled. Defaults to `minValue`."
        },
        {
          "name": "onChange",
          "type": "((value: number) => void)",
          "required": false,
          "description": "Called while the value changes (each key press, each pointer move)."
        },
        {
          "name": "onChangeEnd",
          "type": "((value: number) => void)",
          "required": false,
          "description": "Called once when a drag ends or a key press is committed. Use it to save."
        },
        {
          "name": "formatOptions",
          "type": "NumberFormatOptions",
          "required": false,
          "description": "Formats the value shown next to the label and spoken to screen readers\n(`aria-valuetext`): `{ style: 'percent' }`, `{ style: 'unit', unit: 'kilometer' }`, ...\nUses the locale of `DSProvider`."
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "Name for form submission; the form receives the number."
        },
        {
          "name": "showValue",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Shows the formatted value (mono) on the right of the label. Default `true`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the track, linked with `aria-describedby`."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", SliderRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "SliderProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "maxValue",
              "type": "number",
              "required": false
            },
            {
              "name": "minValue",
              "type": "number",
              "required": false
            },
            {
              "name": "step",
              "type": "number",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-slider-thumb-dot-size",
        "--ds-slider-thumb-size",
        "--ds-slider-track-height",
        "--ds-space-3",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/Slider",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-slider--default",
            "hasPlay": true
          },
          {
            "name": "KeyboardContract",
            "id": "components-slider--keyboard-contract",
            "hasPlay": true
          },
          {
            "name": "Percent",
            "id": "components-slider--percent",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-slider--with-description",
            "hasPlay": true
          },
          {
            "name": "SteppedByTen",
            "id": "components-slider--stepped-by-ten",
            "hasPlay": false
          },
          {
            "name": "WithoutValue",
            "id": "components-slider--without-value",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-slider--disabled",
            "hasPlay": true
          },
          {
            "name": "AriaLabelOnly",
            "id": "components-slider--aria-label-only",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-slider--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-slider--long-label",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Slider/Slider.md",
        "summary": "Single-value slider: a labelled track with one thumb and a formatted value. Built on React Aria `Slider` (`SliderTrack`, `SliderFill`, `SliderThumb`, `SliderOutput`). The thumb is a real `<input type=\"range\">`, so keyboard, pointer, touch and form submission work natively. Task: CMP-INP-04.",
        "whenToUse": [
          "Picking a number from a bounded range where the approximate value matters more than the exact one (discount, opacity, threshold)."
        ],
        "whenNotToUse": [
          "An exact quantity: `NumberField`. Money: `CurrencyInput`.",
          "A range with two ends: not supported yet (two-thumb range is P2).",
          "Fewer than about five discrete choices: `RadioGroup` or a segmented control."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Picking a number from a bounded range where the approximate value matters more than the exact one (discount, opacity, threshold)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- An exact quantity: `NumberField`. Money: `CurrencyInput`.\n- A range with two ends: not supported yet (two-thumb range is P2).\n- Fewer than about five discrete choices: `RadioGroup` or a segmented control."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "- Header: `Label` (left) and the formatted value in mono (right, `SliderOutput`).\n- Track: rail (`--ds-line-control`, 3:1 against the page), fill from the start to the thumb (`--ds-accent`), thumb.\n- Thumb: a 24px hit area (`--ds-slider-thumb-size`, WCAG 2.5.8) with a 16px visible knob; the focus ring follows the hit area.\n- Optional description under the track."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                         | Notes                                                                                            |\n| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |\n| `label` / `aria-label` / `aria-labelledby`   | One is required (types and a development warning).                                               |\n| `value` / `defaultValue` / `onChange(value)` | Controlled / uncontrolled, RAC naming. `onChange` fires on every step; `onChangeEnd` on commit.  |\n| `minValue`, `maxValue`, `step`               | Defaults `0`, `100`, `1`.                                                                        |\n| `formatOptions`                              | `Intl.NumberFormat` options for the shown value and `aria-valuetext`, locale from `DSProvider`.  |\n| `showValue`                                  | Default `true`.                                                                                  |\n| `description`                                | Helper text, linked with `aria-describedby` on the thumb.                                        |\n| `name`                                       | Submitted with the form as the number.                                                           |\n| `isDisabled`                                 | Dimmed track; the text drops to `--ds-text-3` instead of using opacity so it keeps its contrast. |"
          },
          {
            "id": "accessibility-and-keyboard",
            "heading": "Accessibility and keyboard",
            "content": "| Key                       | Behavior                                                                 |\n| ------------------------- | ------------------------------------------------------------------------ |\n| `ArrowRight` / `ArrowUp`  | Increase by `step`                                                       |\n| `ArrowLeft` / `ArrowDown` | Decrease by `step`                                                       |\n| `PageUp` / `PageDown`     | Increase / decrease by a larger step (10% of the range, at least `step`) |\n| `Home` / `End`            | Minimum / maximum                                                        |\n\nThe thumb is `input[type=range]`, named by the label, with `aria-valuetext` from `formatOptions`. The value text is not a live region (RAC announces changes through the input)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "Label with the thing being set (\"Discount\"), not an instruction. Put the unit in `formatOptions` (`{ style: 'percent' }`, `{ style: 'unit', unit: 'kilometer' }`), never in the label."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Slider\n  label=\"Discount\"\n  name=\"discount\"\n  defaultValue={10}\n  maxValue={50}\n  formatOptions={{ style: 'unit', unit: 'percent' }}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The thumb is a real range input (the handoff `div role=\"slider\"` had no name, no Home/End/PageUp/PageDown and no `aria-valuetext`).\n- `format(value)` became `formatOptions` (locale-aware `Intl`); `min`/`max` became `minValue`/`maxValue` (RAC names); the knob has a 24px hit area; the label is required.\n- The decorative shadow on the knob was dropped (shadows only on overlays)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "NumberField",
            "reason": "An exact quantity: `NumberField`. Money: `CurrencyInput`."
          },
          {
            "name": "CurrencyInput",
            "reason": "An exact quantity: `NumberField`. Money: `CurrencyInput`."
          },
          {
            "name": "RadioGroup",
            "reason": "Fewer than about five discrete choices: `RadioGroup` or a segmented control."
          }
        ],
        "uses": ["Label"],
        "usedBy": []
      }
    },
    {
      "name": "SourceList",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { SourceList } from 'loui-ds/patterns';",
      "folder": "src/patterns/SourceList",
      "sourceFile": "src/patterns/SourceList/SourceList.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The numbered list of sources an answer is based on. Each source is a list item that\n`Citation` markers jump to; the title is a link when the source has a `url`.\n\nSource numbers are the position in `sources`: keep the order stable while the answer streams,\nand append new sources at the end.",
      "props": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "Id of the list. Required: citations use it to find their source."
        },
        {
          "name": "sources",
          "type": "readonly Source[]",
          "required": true,
          "description": "The sources, in citation order. The first is source 1."
        },
        {
          "name": "title",
          "type": "string",
          "required": false,
          "description": "Heading text. Defaults to \"Sources\"."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "3",
          "description": "Heading level of the title (`h1` to `h6`). Default `3`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "messages",
          "type": "Partial<{ title: string; citation: (number: number, title: string) => string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "SourceList",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "title",
            "kind": "string",
            "default": "Sources",
            "description": "Default heading of the source list."
          },
          {
            "key": "citation",
            "kind": "function",
            "default": "Source 2: {title}",
            "signature": "(number: number, title: string)",
            "description": "Accessible name of a citation marker: \"Source 2: Title\"."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-citation-size",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line-control",
        "--ds-radius-sm",
        "--ds-source-list-gap",
        "--ds-source-list-indent",
        "--ds-source-list-padding",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Patterns/SourceList",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-source-list--default",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "patterns-source-list--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-source-list--right-to-left",
            "hasPlay": false
          },
          {
            "name": "WithCitations",
            "id": "patterns-source-list--with-citations",
            "description": "Markers in a sentence jump to the matching source; focus lands on it.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/SourceList/SourceList.md",
        "summary": "The numbered list of sources an answer is based on (`SourceList`) and the inline markers that point to it (`Citation`). Import both from `loui-ds/patterns`.",
        "whenToUse": [
          "An assistant answer that rests on documents, pages or records, and the owner should be able to check them."
        ],
        "whenNotToUse": [
          "Footnotes in long-form documents: use plain links.",
          "A list of results the user searched for: use a table or a list of cards."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- An assistant answer that rests on documents, pages or records, and the owner should be able to check them."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Footnotes in long-form documents: use plain links.\n- A list of results the user searched for: use a table or a list of cards."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\np            ... a claim [2] ...        Citation: <a href=\"#s-source-b\" aria-label=\"Source 2: Title\">2</a>\nsection      named by its heading (\"Sources\")\n  ol\n    li       tabIndex=-1 (focus target)    title (link when it has a url) · domain (mono) · snippet\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`SourceList`\n\n| Prop           | Type            | Default   | Notes                                                                        |\n| -------------- | --------------- | --------- | ---------------------------------------------------------------------------- |\n| `id`           | `string`        | -         | Required. Citations find their source through it.                            |\n| `sources`      | `Source[]`      | -         | `{ id, title, url?, domain?, snippet? }`. The position is the source number. |\n| `title`        | `string`        | \"Sources\" | Heading text.                                                                |\n| `headingLevel` | `1`-`6`         | `3`       |                                                                              |\n| `messages`     | partial catalog |           | `title`, `citation(number, title)`.                                          |\n\n`Citation`: `number` (1-based, the visible text), `title`, `listId`, `sourceId`, plus anchor props. `getSourceElementId(listId, sourceId)` returns the DOM id when you need it."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A citation is a real link (`href=\"#...\"`) named \"Source 2: Title\"; the visible text is the number, which the name contains.\n- Activating it moves focus to the matching list item and scrolls it into view. It does not change the URL hash, so it does not fight your router. Without JavaScript the hash link still works.\n- Source titles with a `url` are links that open in a new tab (with the external-link icon and a screen-reader note).\n- The marker is 24 px square (WCAG 2.5.8)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Keep source order stable while the answer streams; append new sources at the end, so numbers already shown never change.\n- Titles say what the page is, not \"Click here\". Snippets are short, plain text."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<p>\n  Orders move through four statuses\n  <Citation number={1} title=\"Order lifecycle\" listId=\"answer-1\" sourceId=\"lifecycle\" />.\n</p>\n<SourceList\n  id=\"answer-1\"\n  sources={[{ id: 'lifecycle', title: 'Order lifecycle', url: 'https://example.com/orders' }]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "No handoff source: new pattern (TODO `AI-013`)."
          }
        ]
      },
      "related": {
        "alternatives": [],
        "uses": ["Link"],
        "usedBy": []
      }
    },
    {
      "name": "Sparkline",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Sparkline } from 'loui-ds';",
      "folder": "src/components/Sparkline",
      "sourceFile": "src/components/Sparkline/Sparkline.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A tiny trend line (or area) without axes, drawn in SVG, for tables, stat cards and lists. It\nshows the shape of a series, not its values: put the current number next to it.\n\nIt is an image with a required text summary (`aria-label`), or `decorative` when the same\ninformation is written next to it. It is not interactive and not focusable. The vertical\nrange is the data's own minimum to maximum, so a small change on a large number still\nshows; use `Chart` when readers need axes, values per point or zero as a reference.",
      "props": [
        {
          "name": "data",
          "type": "readonly (number | null | undefined)[]",
          "required": true,
          "description": "The values in display order, usually time. `null`, `undefined` and non-finite numbers are\ngaps: the line breaks there. An empty array draws nothing."
        },
        {
          "name": "variant",
          "type": "'area' | 'line'",
          "required": false,
          "default": "'line'",
          "description": "`line` (default) or `area`, which also fills softly under the line.",
          "values": ["area", "line"]
        },
        {
          "name": "tone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Color of the line: `neutral` (default), `accent`, `success`, `warning` or `danger`. Color is\nnever the only signal: say what the trend means in the accessible name or next to it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "width",
          "type": "number",
          "required": false,
          "default": "96",
          "description": "Width in px (user units). Shrinks to fit a narrower container. Default 96."
        },
        {
          "name": "height",
          "type": "number",
          "required": false,
          "default": "24",
          "description": "Height in px (user units). Default 24."
        },
        {
          "name": "showLast",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Draws a solid dot on the last value, the current one."
        },
        {
          "name": "showMinMax",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Draws hollow markers on the lowest and the highest value (omitted when all values are equal)."
        },
        {
          "name": "includeZero",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Keeps 0 in the vertical range. Default: the range is the data's own minimum to maximum."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Summary of the trend, required unless `decorative` or `aria-labelledby`: \"Revenue, last 7 days: up 12%\"."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that summarizes the trend."
        },
        {
          "name": "decorative",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Hides the sparkline from assistive technology. Use it only when the same information\nis in text next to it (a number and \"+12%\"): the line alone is never the only source."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 50
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "SVGAttributes",
          "domGeneric": true,
          "count": 259
        }
      ],
      "variants": {
        "variant": ["area", "line"],
        "tone": ["neutral", "accent", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-danger",
        "--ds-sparkline-area-opacity",
        "--ds-sparkline-color",
        "--ds-sparkline-dot-r",
        "--ds-sparkline-marker-r",
        "--ds-sparkline-stroke-w",
        "--ds-success",
        "--ds-text-2",
        "--ds-warning"
      ],
      "stories": {
        "title": "Components/Sparkline",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-sparkline--default",
            "hasPlay": true
          },
          {
            "name": "Area",
            "id": "components-sparkline--area",
            "hasPlay": false
          },
          {
            "name": "Accent",
            "id": "components-sparkline--accent",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "components-sparkline--success",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "components-sparkline--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "components-sparkline--danger",
            "hasPlay": false
          },
          {
            "name": "LastValue",
            "id": "components-sparkline--last-value",
            "hasPlay": false
          },
          {
            "name": "MinMax",
            "id": "components-sparkline--min-max",
            "hasPlay": false
          },
          {
            "name": "Larger",
            "id": "components-sparkline--larger",
            "hasPlay": false
          },
          {
            "name": "IncludeZero",
            "id": "components-sparkline--include-zero",
            "hasPlay": false
          },
          {
            "name": "FlatSeries",
            "id": "components-sparkline--flat-series",
            "hasPlay": false
          },
          {
            "name": "WithGaps",
            "id": "components-sparkline--with-gaps",
            "hasPlay": false
          },
          {
            "name": "SinglePoint",
            "id": "components-sparkline--single-point",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-sparkline--empty",
            "hasPlay": false
          },
          {
            "name": "Decorative",
            "id": "components-sparkline--decorative",
            "hasPlay": true
          },
          {
            "name": "NarrowContainer",
            "id": "components-sparkline--narrow-container",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-sparkline--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Sparkline/Sparkline.md",
        "summary": "A tiny trend line (or area) without axes, drawn in SVG, for tables, stat cards and lists. It shows the shape of a series, not its values: put the current number next to it.\n\n- TODO `GAP-007`.",
        "whenToUse": [
          "Next to a number in a `StatCard`, a table row or a list item, to show the recent trend.",
          "When the shape matters more than exact values."
        ],
        "whenNotToUse": [
          "When readers need axes, values per point or keyboard exploration: use `Chart`.",
          "To show a quantity within a range: use `Meter` or `DonutMeter`.",
          "As the only place a number appears: the sparkline is a summary, not a source."
        ],
        "keyboard": [],
        "examples": [
          "<Sparkline data={daily} aria-label=\"Orders, last 7 days: up 12%\" tone=\"success\" showLast />\n\n<p>\n  Orders 1,248 <Sparkline data={daily} decorative /> +12%\n</p>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Next to a number in a `StatCard`, a table row or a list item, to show the recent trend.\n- When the shape matters more than exact values."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- When readers need axes, values per point or keyboard exploration: use `Chart`.\n- To show a quantity within a range: use `Meter` or `DonutMeter`.\n- As the only place a number appears: the sparkline is a summary, not a source."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nsvg[role=img] aria-label            (or aria-hidden when decorative)\n  path.area                         (variant=\"area\" only)\n  path.line                         (one per run of values, a gap starts a new one)\n  circle.marker[min|max]            (showMinMax: hollow rings)\n  circle.last                       (showLast: solid dot)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                                                          | Default      | Notes                                                                                |\n| -------------------------------- | ------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------ |\n| `data`                           | `(number \\| null \\| undefined)[]`                             | required     | Values in display order. `null`, `undefined`, `NaN` and `Infinity` are gaps.         |\n| `aria-label` / `aria-labelledby` | `string`                                                      | one of three | A text summary of the trend. Required unless `decorative`. Enforced by the types.    |\n| `decorative`                     | `true`                                                        | none         | Hides it from assistive technology. Only when the same facts are written next to it. |\n| `variant`                        | `'line' \\| 'area'`                                            | `'line'`     | `area` fills softly under the line.                                                  |\n| `tone`                           | `'neutral' \\| 'accent' \\| 'success' \\| 'warning' \\| 'danger'` | `'neutral'`  | Color of the line. Never the only signal.                                            |\n| `width` / `height`               | `number`                                                      | 96 / 24      | Size in px. Shrinks to a narrower container, keeping its aspect ratio.               |\n| `showLast`                       | `boolean`                                                     | `false`      | Solid dot on the last value.                                                         |\n| `showMinMax`                     | `boolean`                                                     | `false`      | Hollow rings on the lowest and the highest value (not drawn when all are equal).     |\n| `includeZero`                    | `boolean`                                                     | `false`      | Keeps 0 in the vertical range. Default: the data's own min to max.                   |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- It is an `img` with a required text summary: say what a sighted reader sees, with the numbers that matter (\"Orders, last 7 days: up 12%\"). Use `decorative` only when that text is already next to the sparkline.\n- It is not focusable and not interactive; there are no tooltips. For exploration use `Chart`.\n- Color is never the only signal: tone changes the line color, the trend must be in the name or next to it. Min and max are hollow rings and the last value is a solid dot, so they differ by shape.\n- Line, markers and area use `--ds-text-2` or a signal color (3:1 or better on the dark surfaces). Forced colors: everything is drawn in `CanvasText`.\n- Under 200% zoom and in narrow containers it shrinks with the container."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Summaries are one short sentence in sentence case with the period and the direction: \"Revenue, last 30 days: up 8%\"."
          },
          {
            "id": "differences-from-chart",
            "heading": "Differences from `Chart`",
            "content": "The vertical range is the data's own minimum to maximum, not a \"nice\" axis that includes zero (`niceScale`), because a trend line on `98, 100, 102` must not be flattened. For that reason `niceScale` and the `Chart` layout helpers are not reused; the geometry lives in `sparklineLayout.ts` and is unit tested."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Sparkline data={daily} aria-label=\"Orders, last 7 days: up 12%\" tone=\"success\" showLast />\n\n<p>\n  Orders 1,248 <Sparkline data={daily} decorative /> +12%\n</p>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Chart",
            "reason": "When readers need axes, values per point or keyboard exploration: use `Chart`."
          },
          {
            "name": "Meter",
            "reason": "To show a quantity within a range: use `Meter` or `DonutMeter`."
          },
          {
            "name": "DonutMeter",
            "reason": "To show a quantity within a range: use `Meter` or `DonutMeter`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Spinner",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Spinner } from 'loui-ds';",
      "folder": "src/components/Spinner",
      "sourceFile": "src/components/Spinner/Spinner.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An indeterminate loading ring that inherits the text color.\n\nIt is a `progressbar` without a value, not a live region, so many spinners on a screen do\nnot each interrupt a screen reader. For large areas prefer `Skeleton`. Under\n`prefers-reduced-motion` the ring is static.",
      "props": [
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Size token: `sm` 14px, `md` 18px (default), `lg` 20px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "Accessible name. Defaults to the \"Loading\" message. Say what is loading when it helps\n(\"Loading orders\")."
        },
        {
          "name": "decorative",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Hide the spinner from assistive technology. Use it only when the surrounding UI already\nannounces the busy state (visible status text, `aria-busy` region). Not needed inside a\npending `Button`, which wires the spinner as its busy label."
        },
        {
          "name": "messages",
          "type": "Partial<{ loading: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 50
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "Spinner",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "loading",
            "kind": "string",
            "default": "Loading"
          }
        ]
      },
      "cssVariables": [
        "--ds-icon-lg",
        "--ds-icon-md",
        "--ds-icon-sm",
        "--ds-radius-pill",
        "--ds-spinner-duration",
        "--ds-spinner-stroke-width"
      ],
      "stories": {
        "title": "Components/Spinner",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-spinner--default",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-spinner--sizes",
            "hasPlay": false
          },
          {
            "name": "WithLabel",
            "id": "components-spinner--with-label",
            "description": "Say what is loading when the page has more than one busy area.",
            "hasPlay": true
          },
          {
            "name": "DecorativeWithText",
            "id": "components-spinner--decorative-with-text",
            "description": "Visible status text next to the spinner; the spinner itself is hidden from assistive technology.",
            "hasPlay": true
          },
          {
            "name": "InheritsColor",
            "id": "components-spinner--inherits-color",
            "description": "Inherits the text color of its container.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Spinner/Spinner.md",
        "summary": "An indeterminate loading ring that inherits the text color. Small, quiet, and not a live region.\n\n- Handoff source: `components/feedback/Spinner` (CMP-FBK-06).\n- Built on: a plain element with `role=\"progressbar\"`. A pending `Button` wires it as its busy label through React Aria.",
        "whenToUse": [
          "Inside a pending `Button` (`isPending`), which renders it for you.",
          "Next to a short status text for a small, bounded wait (\"Saving changes\").",
          "A single small area that is loading and has no better placeholder."
        ],
        "whenNotToUse": [
          "For large areas or page loads: use `Skeleton` so the layout does not jump.",
          "When you know the progress: use `Progress` with a value.",
          "As a decoration. It means \"busy\", so remove it when the work ends."
        ],
        "keyboard": [],
        "examples": [
          "<Spinner />\n<Spinner label=\"Loading orders\" size=\"lg\" />\n\n<p role=\"status\">\n  <Spinner decorative size=\"sm\" /> Saving changes\n</p>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Inside a pending `Button` (`isPending`), which renders it for you.\n- Next to a short status text for a small, bounded wait (\"Saving changes\").\n- A single small area that is loading and has no better placeholder."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For large areas or page loads: use `Skeleton` so the layout does not jump.\n- When you know the progress: use `Progress` with a value.\n- As a decoration. It means \"busy\", so remove it when the work ends."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "One element: a ring with an open top edge. Size comes from `--ds-icon-sm|md|lg`, thickness from `--ds-spinner-stroke-width`, rotation time from `--ds-spinner-duration`."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                   | Default   | Notes                                                                                     |\n| ------------ | ---------------------- | --------- | ----------------------------------------------------------------------------------------- |\n| `size`       | `'sm' \\| 'md' \\| 'lg'` | `'md'`    | 14, 18, 20 px. Use `sm` inside `sm` and `md` buttons.                                     |\n| `label`      | `string`               | \"Loading\" | Accessible name. Say what is loading when it helps (\"Loading orders\").                    |\n| `decorative` | `boolean`              | `false`   | Hides the spinner from assistive technology. Only when other UI announces the busy state. |\n| `messages`   | `{ loading?: string }` | none      | Per-instance message override (ADR-0005).                                                 |\n| `className`  | `string`               | none      | Merged with the component class.                                                          |\n\nOther attributes and `ref` pass through to the element."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `role=\"progressbar\"` with no value (indeterminate) and an accessible name. It is **not** `role=\"status\"` or `aria-live`: a screen reader does not announce every spinner when it mounts. One announcement strategy per screen: the button (`Button` announces pending and done) or a single status region you control.\n- Decorative mode: `aria-hidden=\"true\"`. Pair it with visible status text in a `role=\"status\"` region, as in the `DecorativeWithText` story.\n- Reduced motion: under `prefers-reduced-motion: reduce` the ring does not rotate. It stays as a static open ring; the accessible name and the busy control state carry the meaning.\n- Forced colors: the ring is drawn with `currentColor` borders, which follow the system text color.\n- Color: inherits the text color, so it needs no extra contrast treatment beyond its container's."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- The name is a short noun phrase in sentence case: \"Loading\", \"Loading orders\". No ellipsis, no exclamation mark.\n- Default text comes from the `Spinner` message catalog (`en`: \"Loading\", `pt-BR`: \"Carregando\"). Override it per instance or through `DSProvider messages={{ Spinner: { loading: '...' } }}`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Spinner />\n<Spinner label=\"Loading orders\" size=\"lg\" />\n\n<p role=\"status\">\n  <Spinner decorative size=\"sm\" /> Saving changes\n</p>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The pt-BR default label is gone; English is the default and pt-BR is in the catalog.\n- `size` is `sm | md | lg` mapped to icon tokens instead of a pixel number; `color` and `style` props were removed (the ring follows `currentColor`).\n- `role=\"status\"` was replaced by `progressbar`, and a visible `label` is no longer part of the component: place your own text next to a decorative spinner.\n- Reduced-motion alternative added."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Skeleton",
            "reason": "For large areas or page loads: use `Skeleton` so the layout does not jump."
          },
          {
            "name": "Progress",
            "reason": "When you know the progress: use `Progress` with a value."
          }
        ],
        "uses": [],
        "usedBy": ["Button", "ChatPanel", "Combobox", "MatrixGrid", "ToolCallStep"]
      }
    },
    {
      "name": "SplitLayout",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SplitLayout } from 'loui-ds';",
      "folder": "src/components/SplitLayout",
      "sourceFile": "src/components/SplitLayout/SplitLayout.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A main column with a fixed-width side panel beside it: an editor with a chat, a list with a\nsummary. The side panel can stay in view while the main content scrolls, and stacks under the\nmain content when the layout is narrow. Use `Grid` for equal columns, and the `AppShell`\n`detail` prop for the detail pane of a whole screen.\n\nThe main content comes first in the DOM (reading and Tab order); `asidePosition=\"start\"` only\nchanges where it is drawn.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "The main content. It takes the free width and can shrink, so long words wrap instead of widening the page."
        },
        {
          "name": "aside",
          "type": "ReactNode",
          "required": false,
          "description": "The side panel: a chat, a filter, a summary. Without it, only the main content renders."
        },
        {
          "name": "asideLabel",
          "type": "string",
          "required": false,
          "description": "Accessible name of the side panel. With it the panel is an `aside` landmark; without it, a\nplain `div`, because a layout primitive must not add unnamed landmarks."
        },
        {
          "name": "asideWidth",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Width of the side panel: `sm` 280px, `md` 360px (default, same as the `AppShell` detail pane), `lg` 480px.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "asidePosition",
          "type": "'start' | 'end'",
          "required": false,
          "default": "'end'",
          "description": "Side of the side panel in the reading direction: `end` (default) or `start`. It is logical,\nso it flips in right-to-left text. The DOM order stays main first.",
          "values": ["start", "end"]
        },
        {
          "name": "gap",
          "type": "1 | 2 | 3 | 4 | 5 | 6 | 8 | 12 | 16 | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '12' | '16'",
          "required": false,
          "default": "6",
          "description": "Space between the main content and the aside, as a key of the space scale. Default `6` (24px).",
          "values": ["1", "2", "3", "4", "5", "6", "8", "12", "16"]
        },
        {
          "name": "sticky",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Keeps the aside in view while the main content scrolls (`position: sticky`). The aside is\nlimited to the height of the viewport and scrolls on its own, and becomes a keyboard stop\nwhile it scrolls. Sticky only works beside the main content: it is off while collapsed."
        },
        {
          "name": "collapseBelow",
          "type": "'sm' | 'md' | 'lg' | 'never'",
          "required": false,
          "default": "'md'",
          "description": "Width of the layout (its own width, measured with a container query) under which the aside\nstacks under the main content: `sm` 480px, `md` 768px (default), `lg` 1024px, or `never`.\nThe widths are the `--ds-breakpoint-*` tokens.",
          "values": ["sm", "md", "lg", "never"]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-line-control",
        "--ds-space-1",
        "--ds-split-layout-aside-w-lg",
        "--ds-split-layout-aside-w-md",
        "--ds-split-layout-aside-w-sm",
        "--ds-split-layout-gap",
        "--ds-split-layout-sticky-offset",
        "--ds-toast-clearance"
      ],
      "stories": {
        "title": "Layout/SplitLayout",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "End",
            "id": "layout-split-layout--end",
            "description": "The aside sits at the inline end, 360px wide. The main content comes first in the DOM.",
            "hasPlay": true
          },
          {
            "name": "Start",
            "id": "layout-split-layout--start",
            "hasPlay": false
          },
          {
            "name": "SmallAside",
            "id": "layout-split-layout--small-aside",
            "hasPlay": false
          },
          {
            "name": "LargeAside",
            "id": "layout-split-layout--large-aside",
            "hasPlay": false
          },
          {
            "name": "WithoutLandmark",
            "id": "layout-split-layout--without-landmark",
            "description": "Without `asideLabel` the aside is a plain `div`: no landmark is added.",
            "hasPlay": false
          },
          {
            "name": "Sticky",
            "id": "layout-split-layout--sticky",
            "description": "The aside stays in view while the long main content scrolls. Scroll the frame to see it.",
            "hasPlay": false
          },
          {
            "name": "CollapsedNarrowContainer",
            "id": "layout-split-layout--collapsed-narrow-container",
            "description": "Narrower than 768px the aside stacks under the main content and is no longer sticky.",
            "hasPlay": false
          },
          {
            "name": "CollapseNever",
            "id": "layout-split-layout--collapse-never",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-split-layout--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/SplitLayout/SplitLayout.md",
        "summary": "A main column with a fixed-width side panel beside it. The side panel can stay in view while the main content scrolls, and it stacks under the main content when the layout is narrow.",
        "whenToUse": [
          "An editor or a long form with a chat, a summary or a help panel that should stay visible: `sticky`.",
          "A list with a filter or preview panel next to it, inside a page."
        ],
        "whenNotToUse": [
          "Equal columns: use `Grid`.",
          "The detail pane of a whole screen (beside the page content, with its own scroll and landmark): use the `detail` prop of `AppShell`.",
          "A panel that slides over the page: use `Sheet`."
        ],
        "keyboard": [],
        "examples": [
          "<SplitLayout aside={<ChatPanel … />} asideLabel=\"Assistant\" sticky>\n  <Stack gap={4}>\n    <EditorSection … />\n    <EditorSection … />\n  </Stack>\n</SplitLayout>\n\n<SplitLayout asidePosition=\"start\" asideWidth=\"sm\" aside={<FilterList … />}>\n  <OrdersTable … />\n</SplitLayout>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- An editor or a long form with a chat, a summary or a help panel that should stay visible: `sticky`.\n- A list with a filter or preview panel next to it, inside a page."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Equal columns: use `Grid`.\n- The detail pane of a whole screen (beside the page content, with its own scroll and landmark): use the `detail` prop of `AppShell`.\n- A panel that slides over the page: use `Sheet`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (container)\n  div (grid)\n    div            main content (children)\n    aside | div    side panel (aside), after the main content in the DOM\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop            | Type                              | Default | Notes                                                                                                              |\n| --------------- | --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |\n| `children`      | `ReactNode`                       |         | The main content; it can shrink                                                                                    |\n| `aside`         | `ReactNode`                       |         | The side panel. Without it only the main content renders                                                           |\n| `asideLabel`    | `string`                          |         | Names the panel as an `aside` landmark. Without it the panel is a plain `div`                                      |\n| `asideWidth`    | `'sm' \\| 'md' \\| 'lg'`            | `'md'`  | 280 / 360 / 480px (`--ds-split-layout-aside-w-*`); `md` is the `AppShell` detail width (`--ds-app-shell-detail-w`) |\n| `asidePosition` | `'start' \\| 'end'`                | `'end'` | Logical: flips in right-to-left text                                                                               |\n| `gap`           | space key (`1` to `16`)           | `6`     | Space between main and aside                                                                                       |\n| `sticky`        | `boolean`                         | `false` | The aside stays in view (16px from the top) and scrolls on its own if taller than the viewport                     |\n| `collapseBelow` | `'sm' \\| 'md' \\| 'lg' \\| 'never'` | `'md'`  | Layout width under which the aside stacks under the main content: 480 / 768 / 1024px                               |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "responsive-behavior",
            "heading": "Responsive behavior",
            "content": "- The layout measures its own width (a container query), so it behaves the same inside a frame, a preview or a test as on the page. The pixel values in the CSS repeat the `--ds-breakpoint-*` tokens; the test suite asserts that they agree.\n- Collapsed: one column, main content first, the aside under it. Sticky is off while collapsed.\n- `sticky` limits the aside to the viewport height (`100dvh`). Inside a scrolling region shorter than the viewport, such as the `main` of an `AppShell`, give the aside content its own bounded height, or accept that the aside scrolls to its end when the region ends."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The DOM order is main first, then aside: reading order and Tab order follow it, also with `asidePosition=\"start\"` (only the drawing order changes). Put content people need first in `children`.\n- `asideLabel` makes the panel a named `aside` (complementary) landmark. A layout primitive adds no unnamed landmark, so leave it off when the panel is not worth a landmark. Do not use it inside `AppShell` for the same content as `detail`.\n- A sticky aside that is taller than the viewport scrolls on its own and becomes a keyboard stop (`tabindex=\"0\"`) so the arrow keys scroll it; with nothing to scroll there is no extra tab stop. The scroll padding keeps focused controls clear of the toast region (WCAG 2.4.11).\n- Reflow: nothing needs a minimum width; below the collapse width the layout is a single column (WCAG 1.4.10)."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<SplitLayout aside={<ChatPanel … />} asideLabel=\"Assistant\" sticky>\n  <Stack gap={4}>\n    <EditorSection … />\n    <EditorSection … />\n  </Stack>\n</SplitLayout>\n\n<SplitLayout asidePosition=\"start\" asideWidth=\"sm\" aside={<FilterList … />}>\n  <OrdersTable … />\n</SplitLayout>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Grid",
            "reason": "Equal columns: use `Grid`."
          },
          {
            "name": "AppShell",
            "reason": "The detail pane of a whole screen (beside the page content, with its own scroll and landmark): use the `detail` prop of `AppShell`."
          },
          {
            "name": "Sheet",
            "reason": "A panel that slides over the page: use `Sheet`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Stack",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Stack } from 'loui-ds';",
      "folder": "src/components/Stack",
      "sourceFile": "src/components/Stack/Stack.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Lays its children out in a vertical column with a consistent gap. The gap is a space token\nkey, never a pixel value. Children can shrink below their content width (`min-inline-size: 0`).",
      "props": [
        {
          "name": "gap",
          "type": "1 | 2 | 3 | 4 | 5 | 6 | 8 | 12 | 16 | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '12' | '16'",
          "required": false,
          "default": "4",
          "description": "Space between children, as a key of the space scale, a number or its string form (`4` = `--ds-space-4` = 16px). Default `4`.",
          "values": ["1", "2", "3", "4", "5", "6", "8", "12", "16"]
        },
        {
          "name": "align",
          "type": "'center' | 'start' | 'end' | 'stretch'",
          "required": false,
          "default": "'stretch'",
          "description": "Cross-axis (horizontal) alignment of the children. Default `stretch`.",
          "values": ["center", "start", "end", "stretch"]
        },
        {
          "name": "as",
          "type": "'div' | 'article' | 'aside' | 'footer' | 'form' | 'header' | 'li' | 'main' | 'nav' | 'ol' | 'section' | 'ul'",
          "required": false,
          "default": "'div'",
          "description": "Element to render, `div` by default. Use `ul` or `ol` for a list of `li` children: the\nlist reset is applied and `role=\"list\"` keeps the list semantics in Safari.",
          "values": [
            "div",
            "article",
            "aside",
            "footer",
            "form",
            "header",
            "li",
            "main",
            "nav",
            "ol",
            "section",
            "ul"
          ]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": ["--ds-space-4", "--ds-stack-gap"],
      "stories": {
        "title": "Layout/Stack",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "layout-stack--default",
            "hasPlay": false
          },
          {
            "name": "SmallGap",
            "id": "layout-stack--small-gap",
            "hasPlay": false
          },
          {
            "name": "LargeGap",
            "id": "layout-stack--large-gap",
            "hasPlay": false
          },
          {
            "name": "AlignStart",
            "id": "layout-stack--align-start",
            "hasPlay": false
          },
          {
            "name": "AlignCenter",
            "id": "layout-stack--align-center",
            "hasPlay": false
          },
          {
            "name": "AlignEnd",
            "id": "layout-stack--align-end",
            "hasPlay": false
          },
          {
            "name": "AsList",
            "id": "layout-stack--as-list",
            "description": "As a list: the list reset is applied and the list role is kept.",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "layout-stack--long-text",
            "description": "Children shrink instead of overflowing a narrow container.",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "layout-stack--right-to-left",
            "description": "Mirrors nothing (it is vertical), but start and end alignment follow the writing direction.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Stack/Stack.md",
        "summary": "A vertical column of children with one consistent gap. The gap is a key of the space scale, never a pixel value.",
        "whenToUse": [
          "Stack blocks of content, fields or cards with the same rhythm.",
          "A list of items with a uniform gap (`as=\"ul\"`)."
        ],
        "whenNotToUse": [
          "For a row of items: use `Inline`.",
          "For a grid of equal cells: use `Grid`.",
          "To space one pair of elements apart once: a margin from the parent is simpler. Use the layout components for repeated structure."
        ],
        "keyboard": [],
        "examples": [
          "<Stack gap={6}>\n  <Heading level={2}>Account</Heading>\n  <Stack gap={3}>\n    <Input label=\"Name\" />\n    <Input label=\"Email\" />\n  </Stack>\n</Stack>\n\n<Stack as=\"ul\" gap={2}>\n  <li>Orders</li>\n  <li>Customers</li>\n</Stack>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Stack blocks of content, fields or cards with the same rhythm.\n- A list of items with a uniform gap (`as=\"ul\"`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a row of items: use `Inline`.\n- For a grid of equal cells: use `Grid`.\n- To space one pair of elements apart once: a margin from the parent is simpler. Use the layout components for repeated structure."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (flex column, gap)\n  children\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop    | Type                                                                          | Default     | Notes                                                       |\n| ------- | ----------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------- |\n| `gap`   | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6 \\| 8 \\| 12 \\| 16` or the same as a string (`\"4\"`) | `4`         | Space token key: 4, 8, 12, 16, 20, 24, 32, 48, 64 px        |\n| `align` | `'start' \\| 'center' \\| 'end' \\| 'stretch'`                                   | `'stretch'` | Horizontal alignment of the children                        |\n| `as`    | `'div' \\| 'section' \\| 'article' \\| 'ul' \\| 'ol' …`                           | `'div'`     | `ul` and `ol` get the list reset and `role=\"list\"` (Safari) |\n\nOther `div` attributes and `ref` go to the root. The gap travels as the custom property `--ds-stack-gap` (ADR-0002 amendment); a consumer `style` is kept."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A `div` Stack adds no semantics. Choose `as` for the meaning: `ul`/`ol` for lists, `section` with a label for a region.\n- `list-style: none` removes list semantics in Safari/VoiceOver, so lists get an explicit `role=\"list\"`.\n- Children can shrink below their content width (`min-inline-size: 0`), so long text wraps instead of overflowing at 320px.\n- Direction-neutral: `start` and `end` follow the writing direction."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- None. Stack holds whatever its children are."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Stack gap={6}>\n  <Heading level={2}>Account</Heading>\n  <Stack gap={3}>\n    <Input label=\"Name\" />\n    <Input label=\"Email\" />\n  </Stack>\n</Stack>\n\n<Stack as=\"ul\" gap={2}>\n  <li>Orders</li>\n  <li>Customers</li>\n</Stack>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff had no layout primitives; screens used inline `display: flex` styles with raw pixel gaps. Gaps are now token keys, typed, with no raw pixels."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Inline",
            "reason": "For a row of items: use `Inline`."
          },
          {
            "name": "Grid",
            "reason": "For a grid of equal cells: use `Grid`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "StackedBar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { StackedBar } from 'loui-ds';",
      "folder": "src/components/StackedBar",
      "sourceFile": "src/components/StackedBar/StackedBar.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Stacked bars: how each category divides into the same set of parts, horizontal (a row per\ncategory) or vertical (a column per category). Drawn in HTML and CSS, no axis: the total of\neach bar is printed with it and the bars share one scale.\n\nSeries are told apart without color: a gap separates neighbouring parts, the neutral ramp\ndiffers in lightness, and beyond four series a hatch pattern repeats the ramp. The legend\nnames every series (and can hide them), and a visually hidden table carries all the numbers.\n\nEach bar is an image with a text summary (\"Q1: Online 40 and Store 30. Total 70.\"). The bars\nare one tab stop; the arrow keys move between them, and the active bar's summary is also\nprinted in a readout line under the chart (hover works the same). A single readout per bar,\nnot per segment, keeps the keyboard path short and the focus target large.",
      "props": [
        {
          "name": "data",
          "type": "readonly StackedBarDatum[]",
          "required": true,
          "description": "One bar per datum, in display order. The chart does not sort."
        },
        {
          "name": "series",
          "type": "readonly StackedBarSeries[]",
          "required": true,
          "description": "The parts of each bar, bottom to top (vertical) or start to end (horizontal). Without\n`color` they follow a neutral ramp of four steps; from the fifth series on the ramp\nrepeats with a hatch (up to eight; beyond that pass `color` or group the tail into \"Other\")."
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'",
          "description": "`horizontal` (default): one row per datum. `vertical`: one column per datum.",
          "values": ["horizontal", "vertical"]
        },
        {
          "name": "normalize",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Draws every bar at full length so each shows the share of its parts (100% stacked). By\ndefault bars are scaled to the largest total, so lengths also compare the totals."
        },
        {
          "name": "format",
          "type": "(value: number) => string",
          "required": false,
          "description": "Formats values in the totals, the legend, the readout and the data table. Default: `Intl` for the active locale. Use `formatCurrency` for money."
        },
        {
          "name": "legend",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Shows the legend under the bars. On by default: the legend is what names the series."
        },
        {
          "name": "interactiveLegend",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Makes the legend items toggle buttons that hide and show series. Hidden series leave the bars, totals, summaries and the data table."
        },
        {
          "name": "legendShare",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Adds the share of each series (its total over the total of the visible series) to the\nlegend, next to the formatted total. Hidden series leave the denominator. Shares are rounded\none by one and may not add up to exactly 100%."
        },
        {
          "name": "hiddenSeries",
          "type": "readonly string[]",
          "required": false,
          "description": "Controlled list of hidden series ids."
        },
        {
          "name": "defaultHiddenSeries",
          "type": "readonly string[]",
          "required": false,
          "default": "[]",
          "description": "Initial hidden series ids when uncontrolled."
        },
        {
          "name": "onHiddenSeriesChange",
          "type": "(hiddenSeries: string[]) => void",
          "required": false,
          "description": "Called with the new list of hidden series ids when the user toggles a legend item."
        },
        {
          "name": "height",
          "type": "string | number",
          "required": false,
          "description": "Height of the plot area of a vertical chart (a number is px, or any CSS length). Default `--ds-stacked-bar-height` (200px). Ignored when horizontal."
        },
        {
          "name": "description",
          "type": "string",
          "required": false,
          "description": "Longer description of what the chart shows. Read after the name, together with the keyboard instructions."
        },
        {
          "name": "categoryLabel",
          "type": "string",
          "required": false,
          "description": "Header of the category column in the hidden data table. Default: the \"Category\" message."
        },
        {
          "name": "messages",
          "type": "Partial<{ instructions: (count: number, formatted: string) => string; series: (label: string, value: string) => string; bar: (label: string, items: string, total: string) => string; tableCaption: (name?: string) => string; category: string; total: string; empty: string; allHidden: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the chart: what it shows (\"Orders by channel, per quarter\"). Required unless `aria-labelledby` is set."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the chart (a visible heading)."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "StackedBar",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "instructions",
            "kind": "function",
            "default": "{formatted} bars. Use the arrow keys to move between bars, Home and End for the first and last.",
            "signature": "(count: number, formatted: string)",
            "description": "Instructions linked to the chart with `aria-describedby`. `count` is the number of bars;\n`formatted` is the same number formatted for the locale."
          },
          {
            "key": "series",
            "kind": "function",
            "default": "{label} {value}",
            "signature": "(label: string, value: string)",
            "description": "One series inside the summary of a bar: \"Online 40\". Both arguments are already formatted."
          },
          {
            "key": "bar",
            "kind": "function",
            "default": "{label}: {items}. Total {total}.",
            "signature": "(label: string, items: string, total: string)",
            "description": "Accessible name of a bar, also shown in the readout. `items` is the list of series joined\nfor the locale (\"Online 40 and Store 30\"); `total` is the formatted sum of the visible series."
          },
          {
            "key": "tableCaption",
            "kind": "function",
            "default": "Data for {name?}",
            "signature": "(name?: string)",
            "description": "Caption of the visually hidden data table. `name` is the accessible name of the chart, if it has a text one."
          },
          {
            "key": "category",
            "kind": "string",
            "default": "Category",
            "description": "Header of the category column of the data table, unless `categoryLabel` is given."
          },
          {
            "key": "total",
            "kind": "string",
            "default": "Total",
            "description": "Header of the total column of the data table."
          },
          {
            "key": "empty",
            "kind": "string",
            "default": "No data to show",
            "description": "Text shown in place of the bars when there is no data."
          },
          {
            "key": "allHidden",
            "kind": "string",
            "default": "All series are hidden. Show one in the legend.",
            "description": "Text shown in place of the bars when every series is hidden through the legend."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-chart-series-1",
        "--ds-chart-series-2",
        "--ds-chart-series-3",
        "--ds-chart-series-4",
        "--ds-chart-series-hatch-size",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-stacked-bar-bar-thickness",
        "--ds-stacked-bar-category-w",
        "--ds-stacked-bar-color",
        "--ds-stacked-bar-column-max-w",
        "--ds-stacked-bar-extent",
        "--ds-stacked-bar-height",
        "--ds-stacked-bar-readout-min-h",
        "--ds-stacked-bar-segment-gap",
        "--ds-stacked-bar-share",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/StackedBar",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-stacked-bar--default",
            "description": "Each bar is an image named by a text summary; the legend names the series and a hidden table holds the numbers.",
            "hasPlay": true
          },
          {
            "name": "Vertical",
            "id": "components-stacked-bar--vertical",
            "hasPlay": false
          },
          {
            "name": "VerticalTall",
            "id": "components-stacked-bar--vertical-tall",
            "hasPlay": false
          },
          {
            "name": "Normalized",
            "id": "components-stacked-bar--normalized",
            "description": "Every bar at full length: the share of each part, not the size of the total.",
            "hasPlay": false
          },
          {
            "name": "NormalizedVertical",
            "id": "components-stacked-bar--normalized-vertical",
            "hasPlay": false
          },
          {
            "name": "WithCurrency",
            "id": "components-stacked-bar--with-currency",
            "hasPlay": false
          },
          {
            "name": "EightSeries",
            "id": "components-stacked-bar--eight-series",
            "description": "Beyond four series the ramp repeats with a hatch pattern, up to eight series.",
            "hasPlay": false
          },
          {
            "name": "ExplicitColors",
            "id": "components-stacked-bar--explicit-colors",
            "description": "Explicit colors are for when the neutral ramp is not enough; the legend still names every series.",
            "hasPlay": false
          },
          {
            "name": "InteractiveLegend",
            "id": "components-stacked-bar--interactive-legend",
            "description": "The legend toggles series. Hidden series leave the bars, the totals and the data table.",
            "hasPlay": true
          },
          {
            "name": "InteractiveLegendControlled",
            "id": "components-stacked-bar--interactive-legend-controlled",
            "hasPlay": false
          },
          {
            "name": "AllSeriesHidden",
            "id": "components-stacked-bar--all-series-hidden",
            "hasPlay": false
          },
          {
            "name": "KeyboardReadout",
            "id": "components-stacked-bar--keyboard-readout",
            "description": "Arrow keys move between bars; the focused (or hovered) bar is also printed under the chart.",
            "hasPlay": true
          },
          {
            "name": "WithoutLegend",
            "id": "components-stacked-bar--without-legend",
            "hasPlay": false
          },
          {
            "name": "ZeroBar",
            "id": "components-stacked-bar--zero-bar",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-stacked-bar--empty",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-stacked-bar--long-labels",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-stacked-bar--portuguese-brazil",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-stacked-bar--right-to-left",
            "hasPlay": false
          },
          {
            "name": "LegendShare",
            "id": "components-stacked-bar--legend-share",
            "description": "`legendShare` adds each series' share of the visible total to the legend.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/StackedBar/StackedBar.md",
        "summary": "Stacked bars for parts of a whole per category, horizontal (a row per category) or vertical (a column per category). Each bar has a text summary, a total, and the chart comes with a legend and a hidden data table.",
        "whenToUse": [
          "Showing how each of a few categories (2 to about 12) divides into the same 2 to 8 parts: orders per channel per quarter, tasks per status per team.",
          "Comparing totals and composition together (default), or composition only (`normalize`)."
        ],
        "whenNotToUse": [
          "One whole divided into parts: `ProportionBar`.",
          "One series over time or categories: `Chart`. Ranked values: `RankList`.",
          "Exact figures the reader must look up: give them a table; the hidden table is for assistive technology.",
          "More than eight series: group the tail into \"Other\"."
        ],
        "keyboard": [
          "Keyboard: one tab stop for all bars (roving). Arrow keys (and `Home`/`End`) move between bars, `Escape` dismisses the readout. The focused bar has a visible focus ring (system colors in forced colors)."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Showing how each of a few categories (2 to about 12) divides into the same 2 to 8 parts: orders per channel per quarter, tasks per status per team.\n- Comparing totals and composition together (default), or composition only (`normalize`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- One whole divided into parts: `ProportionBar`.\n- One series over time or categories: `Chart`. Ranked values: `RankList`.\n- Exact figures the reader must look up: give them a table; the hidden table is for assistive technology.\n- More than eight series: group the tail into \"Other\"."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv.root\n  div[role=group][aria-label][aria-describedby]\n    ul\n      li  category (aria-hidden) · div.slot > div[role=img][tabindex][aria-label=\"Q1: Online 40…\"] · total (aria-hidden)\n  p[aria-hidden]  readout of the focused or hovered bar\n  Legend           names the series (optionally toggles them)\n  VisuallyHidden   instructions (aria-describedby) and a <table> with all values\n```\n\nDrawn with HTML and CSS: bars use one scale (the largest total is full length), parts are `flex` shares of their bar. There is no value axis; the total is printed with each bar and the legend shows the total of each series."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                   | Type                         | Default        | Notes                                                                                               |\n| ---------------------- | ---------------------------- | -------------- | --------------------------------------------------------------------------------------------------- |\n| `data`                 | `{ label, values }[]`        | -              | `values` is keyed by series `id`; missing or negative counts as 0                                   |\n| `series`               | `{ id, label, color? }[]`    | -              | Part order: start to end (horizontal), bottom to top (vertical)                                     |\n| `orientation`          | `'horizontal' \\| 'vertical'` | `'horizontal'` |                                                                                                     |\n| `normalize`            | `boolean`                    | `false`        | Every bar at full length (100% stacked)                                                             |\n| `format`               | `(value) => string`          | `Intl`         | Totals, legend, summaries, table. Use `formatCurrency` for money                                    |\n| `legend`               | `boolean`                    | `true`         | The legend names the series; turn it off only if you name them nearby                               |\n| `interactiveLegend`    | `boolean`                    | `false`        | Legend items hide and show series                                                                   |\n| `legendShare`          | `boolean`                    | `false`        | Legend also shows each series' share of the visible total (rounded one by one, may not sum to 100%) |\n| `hiddenSeries`         | `readonly string[]`          |                | Controlled hidden series                                                                            |\n| `defaultHiddenSeries`  | `readonly string[]`          | `[]`           | Initial hidden series when uncontrolled                                                             |\n| `onHiddenSeriesChange` | `(ids: string[]) => void`    |                |                                                                                                     |\n| `height`               | `number \\| string`           | 200px          | Vertical only (`--ds-stacked-bar-height`)                                                           |\n| `description`          | `string`                     |                | Read after the name                                                                                 |\n| `categoryLabel`        | `string`                     | \"Category\"     | Header of the first table column                                                                    |\n| `messages`             | `Partial<messages>`          |                | Instructions, summaries, table headers, empty states                                                |\n\n`aria-label` or `aria-labelledby` is required."
          },
          {
            "id": "telling-series-apart-without-color",
            "heading": "Telling series apart without color",
            "content": "- A 2px gap (the track shows through) separates neighbouring parts.\n- Series follow a neutral ramp of four steps that differ in lightness (`--ds-chart-series-1` to `-4`), so they also differ for readers with color vision deficiencies. From the fifth series the ramp repeats with a hatch pattern (up to eight). The series `color` prop is for when that is not enough; it is never hatched.\n- The legend names every series and its swatch uses the same fill. A series keeps its fill when another is hidden.\n- Each bar's summary and the data table list every series by name with its value.\n\nThe single-accent rule holds: the accent appears only in the focus ring. Signal colors appear only if you pass them as explicit series colors."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A group named by `aria-label`/`aria-labelledby`, described by the instructions and your `description`. Each bar is an image whose name is its summary: \"Q1: Online 40, Store 30, and Phone 10. Total 80.\"\n- Keyboard: one tab stop for all bars (roving). Arrow keys (and `Home`/`End`) move between bars, `Escape` dismisses the readout. The focused bar has a visible focus ring (system colors in forced colors).\n- Why per-bar and not per-segment: a bar is a large target, the path through a chart stays short (one stop), and its single summary gives screen reader users the whole composition at once. The same text is printed in the readout line under the chart for keyboard and pointer users; the readout is `aria-hidden` because the focused bar is already announced.\n- Hover shows the same readout. The line has reserved height, so nothing shifts.\n- A visually hidden `<table>` (caption, category and one column per visible series, total) carries all values.\n- Hidden series leave the bars, totals, summaries and table. If all are hidden, a message says so. An empty `data` shows \"No data to show\".\n- Contrast: every ramp step keeps 3:1 against the track (`--ds-bg`); labels use `text-2`.\n- Forced colors: segments use system colors; the track gets a border.\n- Category labels are truncated in horizontal bars (96px column, `--ds-stacked-bar-category-w`); the summary and the table carry the full text. Keep labels short."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Bar labels: short (\"Q1\", \"North\"). Series labels: short nouns in sentence case.\n- Name the chart for what it shows (\"Orders per quarter, by channel\").\n- Formatted values come from `Intl`; pass `format` for currency."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<StackedBar\n  aria-label=\"Orders per quarter, by channel\"\n  interactiveLegend\n  series={[\n    { id: 'online', label: 'Online' },\n    { id: 'store', label: 'Store' },\n    { id: 'phone', label: 'Phone' },\n  ]}\n  data={[\n    { label: 'Q1', values: { online: 40, store: 30, phone: 10 } },\n    { label: 'Q2', values: { online: 50, store: 20, phone: 30 } },\n  ]}\n/>\n```"
          },
          {
            "id": "not-included",
            "heading": "Not included",
            "content": "- Value axis and grid lines, negative values, grouped (side by side) bars, per-segment focus, zoom. Use `Chart` for a single series with an axis.\n- Series order in vertical bars is fixed (first series at the bottom); the chart never sorts."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "ProportionBar",
            "reason": "One whole divided into parts: `ProportionBar`."
          },
          {
            "name": "Chart",
            "reason": "One series over time or categories: `Chart`. Ranked values: `RankList`."
          },
          {
            "name": "RankList",
            "reason": "One series over time or categories: `Chart`. Ranked values: `RankList`."
          }
        ],
        "uses": ["Legend", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "StatCard",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { StatCard } from 'loui-ds';",
      "folder": "src/components/StatCard",
      "sourceFile": "src/components/StatCard/StatCard.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An indicator card: a mono caption, a large mono number and a short note, optionally with a\ntrend and a small visual (such as a `ProportionBar`) in between.\n\nIt is a description list: the label is the term, the value and the note are its\ndescriptions, so a screen reader announces \"Sold, $37,655.30, Up 12% vs previous 30 days\".\nA colored value is never the only signal: `tone` other than `neutral` requires a `note` or a\n`trend` (a computed `StatCardTone` is accepted when either is passed), and the trend is written as text (\"Up 12%\").",
      "props": [
        {
          "name": "label",
          "type": "ReactNode",
          "required": true,
          "description": "What the number is, in a few words: \"Sold\", \"Overdue\". Shown as a mono caption (uppercase by CSS) and used as the term of the card."
        },
        {
          "name": "value",
          "type": "ReactNode",
          "required": true,
          "description": "The number or amount. Format it with `formatCurrency` or `formatNumber` for the active locale."
        },
        {
          "name": "size",
          "type": "'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "`md` (default, 24px) or `lg` (30px).",
          "values": ["md", "lg"]
        },
        {
          "name": "aside",
          "type": "ReactNode",
          "required": false,
          "description": "Short mono text at the end of the label row: \"42%\". Read after the value."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Content between the value and the note, for example a 4px `ProportionBar`."
        },
        {
          "name": "noteTone",
          "type": "'neutral' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Color of the note text. Default `neutral`. Only affects color: the note is text anyway.",
          "values": ["neutral", "success", "warning", "danger"]
        },
        {
          "name": "messages",
          "type": "Partial<{ trendUp: (percent: string) => string; trendDown: (percent: string) => string; trendFlat: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "tone",
          "type": "'neutral' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Color of the value. Use `danger` or `warning` only when the number asks for action, `success` when everything is fine. Default `neutral`.\nA colored value must be backed by text: pass `note` or `trend` (type error otherwise).\nAccepts any `StatCardTone`, so a computed tone such as `flag ? 'warning' : 'neutral'`\ntype-checks as long as `note` or `trend` is present.",
          "values": ["neutral", "success", "warning", "danger"]
        },
        {
          "name": "note",
          "type": "ReactNode",
          "required": false,
          "description": "Context or change in words: \"3 sales · average ticket $120\"."
        },
        {
          "name": "trend",
          "type": "StatCardTrend",
          "required": false,
          "description": "A change versus a previous period, rendered as text through the messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["md", "lg"],
        "tone": ["neutral", "success", "warning", "danger"]
      },
      "messages": {
        "providerKey": "StatCard",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "trendUp",
            "kind": "function",
            "default": "Up {percent}",
            "signature": "(percent: string)",
            "description": "Text of an upward trend. `percent` is already formatted for the locale (\"12%\")."
          },
          {
            "key": "trendDown",
            "kind": "function",
            "default": "Down {percent}",
            "signature": "(percent: string)",
            "description": "Text of a downward trend."
          },
          {
            "key": "trendFlat",
            "kind": "string",
            "default": "No change",
            "description": "Text of a trend without change."
          }
        ]
      },
      "cssVariables": [
        "--ds-danger",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-num",
        "--ds-fs-num-lg",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-lh-num",
        "--ds-lh-num-lg",
        "--ds-line",
        "--ds-radius-lg",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-stat-card-gap",
        "--ds-stat-card-padding",
        "--ds-success",
        "--ds-surface",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-tracking-display",
        "--ds-tracking-title",
        "--ds-warning",
        "--ds-weight-medium",
        "--ds-weight-regular",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/StatCard",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-stat-card--default",
            "hasPlay": true
          },
          {
            "name": "Large",
            "id": "components-stat-card--large",
            "hasPlay": false
          },
          {
            "name": "WithAside",
            "id": "components-stat-card--with-aside",
            "hasPlay": false
          },
          {
            "name": "WarningTone",
            "id": "components-stat-card--warning-tone",
            "description": "A colored value is always explained in words: the note says why.",
            "hasPlay": false
          },
          {
            "name": "DangerTone",
            "id": "components-stat-card--danger-tone",
            "hasPlay": false
          },
          {
            "name": "SuccessTone",
            "id": "components-stat-card--success-tone",
            "hasPlay": false
          },
          {
            "name": "TrendUp",
            "id": "components-stat-card--trend-up",
            "description": "The trend is written as text (\"Up 12%\"); the arrow and the color only reinforce it.",
            "hasPlay": true
          },
          {
            "name": "TrendUpIsBad",
            "id": "components-stat-card--trend-up-is-bad",
            "description": "When up is bad (costs, returns), flip the trend tone.",
            "hasPlay": false
          },
          {
            "name": "TrendDownAndFlat",
            "id": "components-stat-card--trend-down-and-flat",
            "hasPlay": false
          },
          {
            "name": "WithProportionBar",
            "id": "components-stat-card--with-proportion-bar",
            "description": "The children slot sits between the value and the note, for a small visual.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-stat-card--long-text",
            "hasPlay": false
          },
          {
            "name": "Grid",
            "id": "components-stat-card--grid",
            "description": "Auto-fit grid of cards, at most four per row.",
            "hasPlay": false
          },
          {
            "name": "PortugueseBrazil",
            "id": "components-stat-card--portuguese-brazil",
            "description": "The trend text and number formats follow the provider locale.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/StatCard/StatCard.md",
        "summary": "An indicator card for the top of a screen: a mono caption, a large mono number, an optional small visual and a short note or trend in words.",
        "whenToUse": [
          "A handful of key numbers at the top of a page (Today, Purchases, Finance, Reports).",
          "A number with context: \"3 sales · average ticket $120.00\", or a change versus a previous period."
        ],
        "whenNotToUse": [
          "Many numbers that need comparing or sorting: use `DataTable`.",
          "A value the user edits: use a field.",
          "A status without a number: use `Badge`."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A handful of key numbers at the top of a page (Today, Purchases, Finance, Reports).\n- A number with context: \"3 sales · average ticket $120.00\", or a change versus a previous period."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Many numbers that need comparing or sorting: use `DataTable`.\n- A value the user edits: use a field.\n- A status without a number: use `Badge`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndl.root (surface, 1px line, radius lg, padding 20)\n  dt   label        mono caption, uppercase by CSS\n  dd   value        mono 24px (md) or 30px (lg); colored by tone\n  dd   aside        short mono text at the end of the label row (optional)\n  dd   children     small visual, e.g. a 4px ProportionBar (optional)\n  dd   note         trend (arrow + \"Up 12%\" + comparison) and/or note text\n```"
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop       | Type                                        | Default   | Notes                                                                                   |\n| ---------- | ------------------------------------------- | --------- | --------------------------------------------------------------------------------------- |\n| `label`    | `ReactNode`                                 | -         | A few words: \"Sold\", \"Overdue\"                                                          |\n| `value`    | `ReactNode`                                 | -         | Format with `formatCurrency` / `formatNumber`                                           |\n| `size`     | `md \\| lg`                                  | `md`      | `--ds-fs-num` / `--ds-fs-num-lg`                                                        |\n| `tone`     | `neutral \\| success \\| warning \\| danger`   | `neutral` | Color of the value. Any other tone requires `note` or `trend` (type error, dev warning) |\n| `note`     | `ReactNode`                                 |           | Context in words                                                                        |\n| `noteTone` | `neutral \\| success \\| warning \\| danger`   | `neutral` | Color of the note text                                                                  |\n| `trend`    | `{ direction, value?, tone?, comparedTo? }` |           | Change as text: \"Up 12%\", \"Down 4.5%\", \"No change\". `value` is a ratio (`0.12`)         |\n| `aside`    | `ReactNode`                                 |           | \"42%\"                                                                                   |\n| `children` | `ReactNode`                                 |           | Between the value and the note                                                          |\n| `messages` | `Partial<messages>`                         |           | `trendUp(percent)`, `trendDown(percent)`, `trendFlat`                                   |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- It is a description list: the label is the term (`dt`), the value and the note are its descriptions (`dd`). A screen reader announces \"Sold, $37,655.30, Up 12% vs previous 30 days\".\n- Color is never the only signal. A non-neutral `tone` must come with a `note` or a `trend` (enforced by types, with a one-time development warning for untyped callers). The trend direction is written as text and the arrow icon is decorative.\n- Contrast: the signal colors on `surface` pass 4.5:1; notes use `text-2`; the caption uses `text-3` (do not put the card on a raised or hover surface).\n- No interaction. If the card should open something, wrap the label in a link or add a button in the note area."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Tone only changes when there is something to do: `danger`/`warning` for numbers that ask for action, `success` for \"all done\". Do not decorate.\n- At most four cards per row; use a grid with `repeat(auto-fit, minmax(240px, 1fr))` so large numbers are not cut.\n- Trends say what changed and compared to what (\"Up 12% vs previous 30 days\"). When up is bad (costs, returns) set `trend.tone: 'danger'`."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<StatCard\n  label=\"Sold\"\n  value={formatCurrency(3765530, 'USD', locale)}\n  trend={{ direction: 'up', value: 0.12, comparedTo: 'vs previous 30 days' }}\n/>\n<StatCard label=\"Overdue\" value=\"$96.40\" tone=\"danger\" note=\"Water bill since Oct 28\" />\n```\n\nA computed tone type-checks as long as `note` or `trend` is passed; a literal non-neutral tone without either is still a type error:\n\n```tsx\n<StatCard\n  label=\"Overdue\"\n  value={total}\n  tone={overdue ? 'warning' : 'neutral'}\n  note={`${count} bills`}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Semantics: `dl`/`dt`/`dd` instead of anonymous `span`s.\n- `tone` needs text: `note` or the new `trend` prop, which renders \"Up 12%\" through messages with an arrow, so a change is never only a colored number.\n- `noteTone` is `neutral | success | warning | danger` (was `muted | ...`); notes use `text-2` instead of `text-3`.\n- Sizes use the `num` / `num-lg` type tokens; padding and gap are component tokens (`--ds-stat-card-*`). No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Many numbers that need comparing or sorting: use `DataTable`."
          },
          {
            "name": "Badge",
            "reason": "A status without a number: use `Badge`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": []
      }
    },
    {
      "name": "StatusSteps",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { StatusSteps } from 'loui-ds';",
      "folder": "src/components/StatusSteps",
      "sourceFile": "src/components/StatusSteps/StatusSteps.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A horizontal indicator of progress through a linear status flow (Draft, Packed, Shipped).\nIt is an ordered list; every step states its progress as text read by assistive technology\n(\"Completed\", \"Current\", \"Not started\", from messages), and the current step has\n`aria-current=\"step\"`. The marker (number or check) and the connector lines are decorative.\n\nWith `onSelect` each step becomes a real button, so the status can be changed from here.",
      "props": [
        {
          "name": "steps",
          "type": "readonly StatusStep[]",
          "required": true,
          "description": "The steps, in order."
        },
        {
          "name": "current",
          "type": "number",
          "required": true,
          "description": "Index of the current step (0-based). Steps before it are complete."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the list (\"Order status\"). Required: name this or use `aria-labelledby`."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the list."
        },
        {
          "name": "complete",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "When `true`, the current step is complete too (the whole flow is done)."
        },
        {
          "name": "onSelect",
          "type": "(index: number, step: StatusStep) => void",
          "required": false,
          "description": "Makes every step a button. Called with the index and the step; the consumer decides\nwhether going back or skipping ahead is allowed. Reversible changes need no confirmation\n(offer Undo)."
        },
        {
          "name": "messages",
          "type": "Partial<{ complete: string; current: string; upcoming: string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "OlHTMLAttributes",
          "domGeneric": true,
          "count": 3
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "StatusSteps",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "complete",
            "kind": "string",
            "default": "Completed",
            "description": "Read after the label of a step that is done."
          },
          {
            "key": "current",
            "kind": "string",
            "default": "Current",
            "description": "Read after the label of the step in progress."
          },
          {
            "key": "upcoming",
            "kind": "string",
            "default": "Not started",
            "description": "Read after the label of a step that has not started."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-lh-meta",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-primary-bg",
        "--ds-radius-md",
        "--ds-radius-pill",
        "--ds-space-2",
        "--ds-status-steps-connector-thickness",
        "--ds-status-steps-content-z-index",
        "--ds-status-steps-gap",
        "--ds-status-steps-marker-border-width",
        "--ds-status-steps-marker-hover-border",
        "--ds-status-steps-marker-size",
        "--ds-status-steps-padding-block",
        "--ds-success",
        "--ds-surface",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-inverse",
        "--ds-weight-medium",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/StatusSteps",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-status-steps--default",
            "hasPlay": true
          },
          {
            "name": "FirstStep",
            "id": "components-status-steps--first-step",
            "hasPlay": false
          },
          {
            "name": "Complete",
            "id": "components-status-steps--complete",
            "hasPlay": false
          },
          {
            "name": "ManySteps",
            "id": "components-status-steps--many-steps",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-status-steps--long-labels",
            "hasPlay": false
          },
          {
            "name": "Interactive",
            "id": "components-status-steps--interactive",
            "description": "Steps are buttons. Tab moves between them; Enter and Space select.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-status-steps--controlled",
            "hasPlay": true
          },
          {
            "name": "Portuguese",
            "id": "components-status-steps--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-status-steps--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-status-steps--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/StatusSteps/StatusSteps.md",
        "summary": "A horizontal indicator of progress through a linear status flow (Draft, Packed, Shipped, Delivered). It is an ordered list; each step states its progress as text for assistive technology, and the steps can optionally be buttons to change the status.\n\n- Handoff source: `components/erp/StatusSteps.jsx` (TODO `CMP-PAT-05`).",
        "whenToUse": [
          "Show where a record is in a fixed, ordered flow, optionally letting people move it.",
          "A flow of 3 to 6 short steps."
        ],
        "whenNotToUse": [
          "A multi-page form with validation and Back/Next: build it with `Tabs` or your router.",
          "Non-linear or parallel states: use `Badge`.",
          "A percentage of something: use `Progress`."
        ],
        "keyboard": [],
        "examples": [
          "<StatusSteps\n  aria-label=\"Order status\"\n  steps={[\n    { id: 'draft', label: 'Draft' },\n    { id: 'packed', label: 'Packed' },\n    { id: 'shipped', label: 'Shipped' },\n  ]}\n  current={1}\n  onSelect={(index) => setStatus(index)}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Show where a record is in a fixed, ordered flow, optionally letting people move it.\n- A flow of 3 to 6 short steps."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A multi-page form with validation and Back/Next: build it with `Tabs` or your router.\n- Non-linear or parallel states: use `Badge`.\n- A percentage of something: use `Progress`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nol aria-label=\"Order status\"\n  li[data-state=complete|current|upcoming]\n    span | button            (button only with onSelect; aria-current=\"step\" on the current one)\n      marker                 (number or check, aria-hidden)\n      label\n      visually hidden state  (\"Completed\", \"Current\", \"Not started\")\n  connector lines            (CSS pseudo-elements, decorative)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                              | Default      | Notes                                                          |\n| -------------------------------- | --------------------------------- | ------------ | -------------------------------------------------------------- |\n| `steps`                          | `StatusStep[]`                    | required     | `{ id, label }` in order.                                      |\n| `current`                        | `number`                          | required     | 0-based index of the current step. Earlier steps are complete. |\n| `complete`                       | `boolean`                         | `false`      | The current step is complete too (whole flow done).            |\n| `onSelect`                       | `(index, step) => void`           | none         | Turns every step into a button.                                |\n| `aria-label` / `aria-labelledby` | `string`                          | one required | Names the list (\"Order status\"). Enforced by the types.        |\n| `messages`                       | `{ complete, current, upcoming }` | none         | Overrides the state texts for this instance.                   |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `ol` / `li` semantics; the position is implied by the list (\"2 of 4\").\n- Every step's state is real text read after its label (`Completed`, `Current`, `Not started`, from messages). The number or check is `aria-hidden`; color and the check are not the only signal.\n- The current step has `aria-current=\"step\"`.\n- With `onSelect`, steps are native buttons: Tab / Shift+Tab move, Enter and Space select, focus has the accent ring. There is no roving focus because this is a list of buttons, not a composite widget.\n- Contrast: upcoming labels use `--ds-text-2` and the upcoming marker border `--ds-line-control` (3:1).\n- Forced colors: current and complete markers use `Highlight`; connectors use `ButtonText`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns or past participles in sentence case (\"Packed\", \"In production\").\n- Moving a status is reversible: do not confirm; offer Undo in a toast."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<StatusSteps\n  aria-label=\"Order status\"\n  steps={[\n    { id: 'draft', label: 'Draft' },\n    { id: 'packed', label: 'Packed' },\n    { id: 'shipped', label: 'Shipped' },\n  ]}\n  current={1}\n  onSelect={(index) => setStatus(index)}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `div`s become an ordered list; interactive steps are real `button`s instead of `div role=\"button\"`.\n- Step state is announced as text (it was color and a check only). The hard-coded Portuguese tooltip is gone; all text comes from messages (`en`, `pt-BR`).\n- Steps need an `id`; strings are no longer accepted. The step number is formatted for the locale.\n- Sizes, gaps and the connector thickness are tokens (`--ds-status-steps-*`); hover is CSS, not React state."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Tabs",
            "reason": "A multi-page form with validation and Back/Next: build it with `Tabs` or your router."
          },
          {
            "name": "Badge",
            "reason": "Non-linear or parallel states: use `Badge`."
          },
          {
            "name": "Progress",
            "reason": "A percentage of something: use `Progress`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Stepper",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Stepper } from 'loui-ds';",
      "folder": "src/components/Stepper",
      "sourceFile": "src/components/Stepper/Stepper.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Progress through a multi-step flow such as a setup assistant. It is an ordered list; every\nstep states its progress as text for assistive technology (\"Completed\", \"Current\", \"Not\nstarted\") and the current step has `aria-current=\"step\"`. Markers and connectors are\ndecorative. Completed steps can be buttons with `onStepPress`.\n\nIn narrow containers the horizontal orientation shows only the markers and a visible summary\n(\"Step 3 of 9\"), so nine steps fit in 320 px. The width at which this happens grows with the step count\n(6, 7 to 8 and 9 or more steps switch at 600, 720 and 800 px), and a label that does not fit\nis clipped after two lines instead of breaking inside a word.",
      "props": [
        {
          "name": "steps",
          "type": "readonly StepperStep[]",
          "required": true,
          "description": "The steps, in order."
        },
        {
          "name": "current",
          "type": "number",
          "required": true,
          "description": "Index of the current step (0-based). Steps before it are completed."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the list (\"New agent steps\"). Required: name this or use `aria-labelledby`."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the list."
        },
        {
          "name": "complete",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "When `true`, the current step is completed too (the whole flow is done)."
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'",
          "description": "`horizontal` (default) or `vertical`. Horizontal collapses to markers and \"Step 3 of 9\" in narrow containers.",
          "values": ["horizontal", "vertical"]
        },
        {
          "name": "onStepPress",
          "type": "(index: number, step: StepperStep) => void",
          "required": false,
          "description": "Makes every completed step a button that calls this with its index and the step, so people\ncan go back. The current and upcoming steps are never buttons: the flow itself (Next)\nmoves forward."
        },
        {
          "name": "messages",
          "type": "Partial<{ complete: string; current: string; upcoming: string; summary: (current: string, total: string) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 51
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Stepper",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "complete",
            "kind": "string",
            "default": "Completed",
            "description": "Read after the label of a step that is done."
          },
          {
            "key": "current",
            "kind": "string",
            "default": "Current",
            "description": "Read after the label of the step in progress."
          },
          {
            "key": "upcoming",
            "kind": "string",
            "default": "Not started",
            "description": "Read after the label of a step that has not started."
          },
          {
            "key": "summary",
            "kind": "function",
            "default": "Step {current} of {total}",
            "signature": "(current: string, total: string)",
            "description": "Visible summary of the progress when the labels do not fit; `current` and `total` are formatted numbers."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-primary-bg",
        "--ds-radius-md",
        "--ds-radius-pill",
        "--ds-space-2",
        "--ds-stepper-connector-thickness",
        "--ds-stepper-content-z-index",
        "--ds-stepper-gap",
        "--ds-stepper-marker-border-width",
        "--ds-stepper-marker-size",
        "--ds-stepper-padding-block",
        "--ds-stepper-step-gap",
        "--ds-success",
        "--ds-surface",
        "--ds-surface-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-inverse",
        "--ds-weight-medium",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Stepper",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-stepper--default",
            "hasPlay": false
          },
          {
            "name": "FirstStep",
            "id": "components-stepper--first-step",
            "hasPlay": false
          },
          {
            "name": "AllComplete",
            "id": "components-stepper--all-complete",
            "hasPlay": false
          },
          {
            "name": "Vertical",
            "id": "components-stepper--vertical",
            "hasPlay": false
          },
          {
            "name": "NineStepsNarrow",
            "id": "components-stepper--nine-steps-narrow",
            "description": "Nine steps in 320 px: the labels collapse into the summary \"Step 3 of 9\".",
            "hasPlay": false
          },
          {
            "name": "NineStepsWide",
            "id": "components-stepper--nine-steps-wide",
            "hasPlay": false
          },
          {
            "name": "NineLongLabels700",
            "id": "components-stepper--nine-long-labels700",
            "description": "Nine long labels at 700 px: too tight for words, so the compact \"Step 3 of 9\" form is used.",
            "hasPlay": false
          },
          {
            "name": "NineLongLabels900",
            "id": "components-stepper--nine-long-labels900",
            "description": "Nine long labels at 900 px: labels stay on whole words, clipped after two lines when needed.",
            "hasPlay": false
          },
          {
            "name": "LongLabels",
            "id": "components-stepper--long-labels",
            "hasPlay": false
          },
          {
            "name": "GoBackToCompletedSteps",
            "id": "components-stepper--go-back-to-completed-steps",
            "description": "Completed steps are buttons. Tab to \"Basics\" and press Enter to go back.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-stepper--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Stepper/Stepper.md",
        "summary": "Progress through a multi-step flow, such as a 9-step setup assistant. It is an ordered list; each step states its progress as text for assistive technology, and completed steps can optionally be buttons to go back.",
        "whenToUse": [
          "A guided flow with several steps shown one at a time (assistant, onboarding, checkout), where people need to know where they are and how much is left.",
          "Flows of 3 to 10 steps. In narrow containers it collapses to markers plus \"Step 3 of 9\"."
        ],
        "whenNotToUse": [
          "The status of a record in a fixed flow (Draft, Packed, Shipped): use `StatusSteps`.",
          "A percentage of something: use `Progress`.",
          "Switching between views of the same content: use `Tabs`."
        ],
        "keyboard": [
          "Keyboard: completed step buttons are in the Tab order and activate with `Enter` or `Space`. Upcoming steps are not focusable, so people cannot skip ahead; move forward with the flow's own Next button."
        ],
        "examples": [
          "<Stepper aria-label=\"Setup progress\" steps={steps} current={step} />\n\n<Stepper\n  aria-label=\"Setup progress\"\n  orientation=\"vertical\"\n  steps={steps}\n  current={step}\n  onStepPress={(index) => setStep(index)}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A guided flow with several steps shown one at a time (assistant, onboarding, checkout), where people need to know where they are and how much is left.\n- Flows of 3 to 10 steps. In narrow containers it collapses to markers plus \"Step 3 of 9\"."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- The status of a record in a fixed flow (Draft, Packed, Shipped): use `StatusSteps`.\n- A percentage of something: use `Progress`.\n- Switching between views of the same content: use `Tabs`.\n\n### Stepper and StatusSteps\n\n|                    | Stepper                                        | StatusSteps                              |\n| ------------------ | ---------------------------------------------- | ---------------------------------------- |\n| Describes          | Your progress through a flow you are doing now | The state a record is in                 |\n| Typical length     | 3 to 10 steps                                  | 3 to 6 steps                             |\n| Interaction        | Only completed steps can be pressed (go back)  | Every step can be pressed (change state) |\n| Orientation        | Horizontal or vertical                         | Horizontal                               |\n| Narrow containers  | Markers plus \"Step 3 of 9\"                     | Same layout                              |\n| Per-step help text | `description` (vertical)                       | None                                     |"
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[data-orientation]\n  p (aria-hidden)           \"Step 3 of 9: Label\", visible only in narrow containers\n  ol aria-label=\"Setup progress\"\n    li[data-state=complete|current|upcoming]\n      span | button         (button only for completed steps with onStepPress; aria-current=\"step\" on the current one)\n        marker              (number or check, aria-hidden)\n        label + visually hidden state (\"Completed\", \"Current\", \"Not started\")\n        description         (vertical only)\n  connector lines           (CSS pseudo-elements, decorative)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop          | Type                                       | Default        | Notes                                                               |\n| ------------- | ------------------------------------------ | -------------- | ------------------------------------------------------------------- |\n| `aria-label`  | `string`                                   |                | Required unless `aria-labelledby` is given: names the list          |\n| `steps`       | `{ id, label, description? }[]`            |                | In order                                                            |\n| `current`     | `number`                                   |                | 0-based index of the current step; earlier steps are completed      |\n| `complete`    | `boolean`                                  | `false`        | The current step is completed too (flow finished)                   |\n| `orientation` | `'horizontal' \\| 'vertical'`               | `'horizontal'` |                                                                     |\n| `onStepPress` | `(index, step) => void`                    |                | Makes completed steps buttons. Current and upcoming steps never are |\n| `messages`    | `{ complete, current, upcoming, summary }` |                | Per-instance overrides                                              |\n\nOther `div` attributes and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A real ordered list named by `aria-label` or `aria-labelledby`. The current step has `aria-current=\"step\"` (on the button when it is one).\n- Progress is text, not only color or a check: every step ends with a visually hidden \"Completed\", \"Current\" or \"Not started\" (messages).\n- Markers, connectors and the \"Step 3 of 9\" summary are decorative duplicates (`aria-hidden`); the list conveys the same information.\n- Keyboard: completed step buttons are in the Tab order and activate with `Enter` or `Space`. Upcoming steps are not focusable, so people cannot skip ahead; move forward with the flow's own Next button.\n- Target size: the marker is 24 px, so step buttons meet WCAG 2.5.8 even at 320 px with 9 steps.\n- Reflow: labels never break inside a word; one that does not fit is clipped after two lines. The container width below which the horizontal stepper collapses grows with the step count: 480 px, 600 px for 6 steps, 720 px for 7 or 8 steps and 800 px for 9 or more. Below that width the horizontal labels are visually hidden (still read by screen readers) and a visible \"Step N of M: label\" line appears. The vertical orientation needs no collapse.\n- Forced colors: markers and connectors use system colors; the state text still names every step."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels: a few words, sentence case, nouns (\"Source\", \"Review\"), not sentences.\n- The description says what the step is for, in one line (\"Name and purpose\").\n- Name the list for what it is: \"New agent steps\", not \"Steps\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Stepper aria-label=\"Setup progress\" steps={steps} current={step} />\n\n<Stepper\n  aria-label=\"Setup progress\"\n  orientation=\"vertical\"\n  steps={steps}\n  current={step}\n  onStepPress={(index) => setStep(index)}\n/>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "StatusSteps",
            "reason": "The status of a record in a fixed flow (Draft, Packed, Shipped): use `StatusSteps`."
          },
          {
            "name": "Progress",
            "reason": "A percentage of something: use `Progress`."
          },
          {
            "name": "Tabs",
            "reason": "Switching between views of the same content: use `Tabs`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "SubMenu",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SubMenu } from 'loui-ds';",
      "folder": "src/components/Menu",
      "sourceFile": "src/components/Menu/Menu.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An item that opens a nested menu. Arrow Right (Arrow Left in right-to-left) opens it and\nArrow Left closes it; Escape closes the submenu first, then the menu.",
      "props": [
        {
          "name": "label",
          "type": "ReactNode",
          "required": true,
          "description": "Label of the item that opens the submenu; end it with an ellipsis when it opens more choices (\"Move to…\")."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "`MenuItem`, `MenuSection` and `MenuSeparator` elements of the submenu."
        },
        {
          "name": "textValue",
          "type": "string",
          "required": false,
          "description": "Used for typeahead and as the item's name when `label` is not a string."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the item: it cannot be opened or focused by typeahead."
        },
        {
          "name": "delay",
          "type": "number",
          "required": false,
          "description": "Milliseconds the pointer must rest on the item before the submenu opens. Default 200."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the submenu list."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-soft-hover",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-small",
        "--ds-item-h",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-line-strong",
        "--ds-menu-indicator-size",
        "--ds-menu-max-width",
        "--ds-menu-min-width",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-select-popover-max-height",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Components/Menu",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-menu--default",
            "description": "Enter, Space or Arrow Down opens it and focuses the first item; Escape closes and restores focus.",
            "hasPlay": true
          },
          {
            "name": "EscapeRestoresFocus",
            "id": "components-menu--escape-restores-focus",
            "hasPlay": true
          },
          {
            "name": "TypeaheadAndHomeEnd",
            "id": "components-menu--typeahead-and-home-end",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-menu--open",
            "description": "Open for the visual review: shortcut hint, disabled item, danger item.",
            "hasPlay": false
          },
          {
            "name": "CheckboxItems",
            "id": "components-menu--checkbox-items",
            "hasPlay": false
          },
          {
            "name": "RadioItems",
            "id": "components-menu--radio-items",
            "hasPlay": false
          },
          {
            "name": "WithSections",
            "id": "components-menu--with-sections",
            "hasPlay": false
          },
          {
            "name": "WithSubmenu",
            "id": "components-menu--with-submenu",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-menu--long-labels",
            "hasPlay": false
          },
          {
            "name": "MoreActions",
            "id": "components-menu--more-actions",
            "description": "The `···` overflow trigger: an icon button named from the message catalog (\"More actions\").",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Menu/Menu.md",
        "summary": "A menu of commands that opens from a trigger. Built on React Aria `MenuTrigger`, `Popover` and `Menu`. Exports `Menu`, `MenuItem`, `MenuSection`, `MenuSeparator`, `SubMenu` and `MoreActionsMenu`.\n\n- Handoff source: `components/navigation/Menu` (CMP-NAV-02).",
        "whenToUse": [
          "Row, card or toolbar actions that do not deserve their own button.",
          "View options with check or radio items (columns shown, sort order, density)."
        ],
        "whenNotToUse": [
          "Choosing a value for a field: use `Select` or `Combobox`.",
          "Content the user types into or operates (filters, forms): use `Popover`.",
          "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.",
          "The only way to reach an important action: keep primary actions visible."
        ],
        "keyboard": [],
        "examples": [
          "<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Row, card or toolbar actions that do not deserve their own button.\n- View options with check or radio items (columns shown, sort order, density)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing a value for a field: use `Select` or `Combobox`.\n- Content the user types into or operates (filters, forms): use `Popover`.\n- Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`.\n- The only way to reach an important action: keep primary actions visible."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nMenuTrigger\n  trigger            Button | IconButton    aria-haspopup=true, aria-expanded\n  Popover            raised surface, radius lg, overlay shadow, z popover\n    Menu             role=menu, named by the trigger (aria-labelledby)\n      MenuItem       role=menuitem | menuitemcheckbox | menuitemradio, height --ds-item-h\n        indicator    check / dot (selectable menus)\n        label\n        shortcut     mono hint, optional\n        chevron      when it opens a SubMenu\n      MenuSection    role=group, caption optional (mono uppercase)\n      MenuSeparator  1px line\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Menu`\n\n| Prop                                      | Type                               | Default        | Notes                                                    |\n| ----------------------------------------- | ---------------------------------- | -------------- | -------------------------------------------------------- |\n| `trigger`                                 | `ReactElement`                     | none           | A `Button` or `IconButton`. Its name becomes the menu's. |\n| `children`                                | `ReactNode`                        | none           | Items, sections, separators, submenus.                   |\n| `placement`                               | React Aria `Placement`             | `bottom start` | Flips on collision.                                      |\n| `onAction`                                | `(key) => void`                    |                | Called for plain items with their `id`.                  |\n| `selectionMode`                           | `'none' \\| 'single' \\| 'multiple'` | `none`         | `single` = radio items, `multiple` = checkbox items.     |\n| `selectedKeys` / `defaultSelectedKeys`    | `Selection`                        |                | Controlled / uncontrolled selection.                     |\n| `onSelectionChange`                       | `(keys) => void`                   |                |                                                          |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                    |                | Controlled / uncontrolled open state.                    |\n\n`MenuItem`: `id`, `children` (label), `tone` (`neutral` | `danger`), `shortcut`, `isDisabled`, `onAction`, `href`, `textValue`.\n`MenuSection`: `title` (caption and accessible name), own `selectionMode` for a radio group inside a command menu.\n`SubMenu`: `label`, `children`, `textValue`, `isDisabled`, `delay`.\n`MoreActionsMenu`: a `Menu` with an `IconButton` (ellipsis) trigger named \"More actions\" from the catalog; `size`, `messages`, default placement `bottom end`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "Keyboard contract (AGENTS.md section 8):\n\n| Key                                     | Behavior                                                        |\n| --------------------------------------- | --------------------------------------------------------------- |\n| `Enter`, `Space`, `ArrowDown` (trigger) | Opens the menu and focuses the first item                       |\n| `ArrowUp` (trigger)                     | Opens the menu and focuses the last item                        |\n| `ArrowUp` / `ArrowDown`                 | Move between items; disabled items are skipped                  |\n| `Home` / `End`                          | First / last item                                               |\n| Typing                                  | Typeahead: focus moves to the item whose label starts with it   |\n| `Enter` / `Space`                       | Activates the item (checkbox and radio items toggle) and closes |\n| `ArrowRight` / `ArrowLeft`              | Open / close a submenu (mirrored in right-to-left)              |\n| `Escape`                                | Closes one level; focus returns to the trigger                  |\n\n- Items are real `menuitem`, `menuitemcheckbox` or `menuitemradio` elements with `aria-checked`; the handoff used `tabIndex=0` divs with no keyboard activation.\n- Danger is red text plus the item's own label (\"Delete order\"); the colour is never the only signal. `tone=\"danger\"` does not confirm: open a `ConfirmDialog` from `onAction`.\n- The shortcut hint is visual; bind the key yourself. It uses `--ds-text-2` so the hint stays legible next to the label.\n- Focus is shown with the accent ring; forced colors use a `Highlight` outline.\n- Layer: `--ds-z-popover` (see Dialog doc, \"Layers\", for menus opened inside a Dialog or Sheet).\n- The `MoreActionsMenu` trigger has a tooltip that repeats its name."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Items are a verb plus object in sentence case (\"Duplicate order\"). End with an ellipsis only when the item opens something that needs more input (\"Move to…\").\n- Order: most common first, destructive last, separated by a `MenuSeparator`."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<MoreActionsMenu onAction={(key) => run(String(key))}>\n  <MenuItem id=\"edit\" shortcut=\"Ctrl+E\">Edit order</MenuItem>\n  <MenuItem id=\"duplicate\">Duplicate order</MenuItem>\n  <MenuSeparator />\n  <MenuItem id=\"delete\" tone=\"danger\">Delete order</MenuItem>\n</MoreActionsMenu>\n\n<Menu trigger={<Button>View</Button>} selectionMode=\"multiple\" defaultSelectedKeys={['id']}>\n  <MenuItem id=\"id\">Show ID</MenuItem>\n  <MenuItem id=\"status\">Show status</MenuItem>\n</Menu>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- React Aria provides roving focus, typeahead, activation, selection and focus restore; the handoff menu was a list of divs with click handlers.\n- The trigger is part of the component (`trigger` prop); the handoff `Menu` was a bare list placed by the caller.\n- New: selection modes, sections, submenus, disabled items, `MoreActionsMenu`.\n- The handoff `width` prop is replaced by `--ds-menu-min-width` (grows with content, at least the trigger width) and `--ds-menu-max-width` (360px; a longer label wraps instead of stretching the popover, TEST-020)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Select",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Combobox",
            "reason": "Choosing a value for a field: use `Select` or `Combobox`."
          },
          {
            "name": "Popover",
            "reason": "Content the user types into or operates (filters, forms): use `Popover`."
          },
          {
            "name": "Tabs",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Sidebar",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          },
          {
            "name": "Breadcrumb",
            "reason": "Navigation between pages: use `Tabs`, `Sidebar` or `Breadcrumb`."
          }
        ],
        "uses": ["Icon", "IconButton"],
        "usedBy": []
      }
    },
    {
      "name": "SuggestionChip",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { SuggestionChip } from 'loui-ds';",
      "folder": "src/components/SuggestionChip",
      "sourceFile": "src/components/SuggestionChip/SuggestionChip.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A dashed chip that proposes a piece of text. Pressing it **inserts** that text into a\nnearby field (for example a message composer); it never submits or navigates.\n\nThe dashed border and the leading plus tell sighted users that it adds something. The plus\nis decorative: screen readers hear the label, so word the label as the text itself and put\nthe chips in a labelled group (\"Suggested replies\") so the purpose is announced once.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The suggested text. The chip is named by it and, when pressed, the consumer inserts\nexactly this text (or the text it stands for) into the field it belongs to."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLButtonElement>",
          "required": false,
          "description": "Ref to the underlying `<button>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaBaseButtonProps",
          "domGeneric": false,
          "count": 16,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-current",
              "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\"",
              "required": false
            },
            {
              "name": "aria-disabled",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-expanded",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "aria-pressed",
              "type": "boolean | \"true\" | \"false\" | \"mixed\"",
              "required": false
            },
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "formAction",
              "type": "string | ((formData: FormData) => void | Promise<void>)",
              "required": false
            },
            {
              "name": "formEncType",
              "type": "string",
              "required": false
            },
            {
              "name": "formMethod",
              "type": "string",
              "required": false
            },
            {
              "name": "formNoValidate",
              "type": "boolean",
              "required": false
            },
            {
              "name": "formTarget",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            },
            {
              "name": "preventFocusOnPress",
              "type": "boolean",
              "required": false
            },
            {
              "name": "type",
              "type": "'button' | 'submit' | 'reset'",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"button\", ButtonRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-control-h-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-lg",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/SuggestionChip",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-suggestion-chip--default",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-suggestion-chip--disabled",
            "hasPlay": true
          },
          {
            "name": "LongText",
            "id": "components-suggestion-chip--long-text",
            "hasPlay": false
          },
          {
            "name": "InsertsText",
            "id": "components-suggestion-chip--inserts-text",
            "description": "The chip inserts text into the field; Enter and Space both press it.",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-suggestion-chip--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-suggestion-chip--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/SuggestionChip/SuggestionChip.md",
        "summary": "A dashed chip that proposes a piece of text. Pressing it **inserts** that text into a nearby field (a message composer, a note, a search box). Built on React Aria `Button`.\n\n- Handoff source: `components/forms/SuggestionChip.jsx` (TODO `CMP-FRM-10`).",
        "whenToUse": [
          "Suggested replies, snippets, prompts or tags that save typing.",
          "The result of pressing is always \"this text appears in the field\"; the user still reviews and sends it."
        ],
        "whenNotToUse": [
          "To run an action (save, send, delete): use `Button`.",
          "To filter or toggle a state: use `Tag` in a `TagGroup` with selection, or `SegmentedControl`.",
          "To navigate: use `Link`."
        ],
        "keyboard": ["A real `<button type=\"button\">`: `Enter` and `Space` press it."],
        "examples": [
          "<div role=\"group\" aria-label=\"Suggested replies\">\n  {replies.map((reply) => (\n    <SuggestionChip key={reply} onPress={() => insert(reply)}>\n      {reply}\n    </SuggestionChip>\n  ))}\n</div>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Suggested replies, snippets, prompts or tags that save typing.\n- The result of pressing is always \"this text appears in the field\"; the user still reviews and sends it."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To run an action (save, send, delete): use `Button`.\n- To filter or toggle a state: use `Tag` in a `TagGroup` with selection, or `SegmentedControl`.\n- To navigate: use `Link`."
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "`SuggestionChip` does not know about the field. `onPress` is where the consumer inserts the text (append at the caret, replace the selection, or set the value) and, usually, returns focus to the field. The chip never submits a form and never sends anything."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                      | Notes                                                             |\n| ------------ | ------------------------- | ----------------------------------------------------------------- |\n| `children`   | `ReactNode`               | The suggested text; also the accessible name.                     |\n| `onPress`    | `(e: PressEvent) => void` | Insert the text here.                                             |\n| `isDisabled` | `boolean`                 | Uses the shared disabled opacity (`--ds-opacity-disabled`, 0.35). |\n| `className`  | `string`                  | Merged with the component class.                                  |\n\nOther React Aria `Button` props (`aria-*`, `autoFocus`, `ref`, ...) pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A real `<button type=\"button\">`: `Enter` and `Space` press it.\n- The leading plus is decorative (`aria-hidden`) and the dashed border is visual. Neither is the only cue: the chip sits in a group whose label says what the chips are, so give the container a name (`role=\"group\"` with `aria-label=\"Suggested replies\"`).\n- Word the label as the text itself (\"Your order has shipped.\"), not as an instruction. Screen reader users then hear exactly what will be inserted.\n- Long text wraps inside the available width; the chip keeps a 32px minimum height.\n- Disabled: reduced opacity and no hover. The handoff's .45 and .35 values are unified into one token.\n- Focus: accent border and ring."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- The label is the text to insert, in sentence case, in the voice of the product (see AGENTS.md section 6).\n- Keep it to one sentence; very long snippets belong in a template picker."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<div role=\"group\" aria-label=\"Suggested replies\">\n  {replies.map((reply) => (\n    <SuggestionChip key={reply} onPress={() => insert(reply)}>\n      {reply}\n    </SuggestionChip>\n  ))}\n</div>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria `Button`; hover is CSS (`data-hovered`), not React state.\n- The plus is the `Icon` plus (`PlusIcon`), decorative. Behavior (insert, never send) is documented.\n- One disabled opacity token instead of a local .45."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Button",
            "reason": "To run an action (save, send, delete): use `Button`."
          },
          {
            "name": "Tag",
            "reason": "To filter or toggle a state: use `Tag` in a `TagGroup` with selection, or `SegmentedControl`."
          },
          {
            "name": "TagGroup",
            "reason": "To filter or toggle a state: use `Tag` in a `TagGroup` with selection, or `SegmentedControl`."
          },
          {
            "name": "SegmentedControl",
            "reason": "To filter or toggle a state: use `Tag` in a `TagGroup` with selection, or `SegmentedControl`."
          },
          {
            "name": "Link",
            "reason": "To navigate: use `Link`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": []
      }
    },
    {
      "name": "Swatch",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Swatch } from 'loui-ds';",
      "folder": "src/components/Swatch",
      "sourceFile": "src/components/Swatch/Swatch.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A small circle filled with a color, with a subtle inset ring so dark colors stay visible on\ndark surfaces. It needs either a `label` (the color's name) or `decorative`, so the color is\nnever the only carrier of meaning.\n\nBuilt as a plain element rather than React Aria `ColorSwatch`: that one paints an inline\n`background` and names itself with a generated description (\"vibrant blue\"), while this one\nsets only the `--ds-swatch-color` custom property and uses the name you give it.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "Name of the color (\"Navy\"). Required whenever the swatch is the only thing that identifies\nthe color: a color on its own says nothing to a screen reader or to a color-blind user."
        },
        {
          "name": "decorative",
          "type": "true",
          "required": false,
          "description": "Hides the swatch from assistive technology. Use it only when the color is also named by\nadjacent text (a table cell, an option label)."
        },
        {
          "name": "color",
          "type": "string | null",
          "required": false,
          "description": "The color to show, as `#rgb` or `#rrggbb`. `undefined`, `null` and `''` mean \"no color\" and\nrender an empty dashed circle (use it for model data that may have no color). Any other\nvalue throws in development and renders the same empty circle in production."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Diameter token: `sm` 16px, `md` 20px (default), `lg` 32px.",
          "values": ["sm", "md", "lg"]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 50
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-line-control",
        "--ds-radius-pill",
        "--ds-swatch-color",
        "--ds-swatch-ring",
        "--ds-swatch-size-lg",
        "--ds-swatch-size-md",
        "--ds-swatch-size-sm"
      ],
      "stories": {
        "title": "Components/Swatch",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-swatch--default",
            "hasPlay": true
          },
          {
            "name": "Sizes",
            "id": "components-swatch--sizes",
            "hasPlay": false
          },
          {
            "name": "ExtremeColors",
            "id": "components-swatch--extreme-colors",
            "description": "Near-black and near-white colors stay visible thanks to the inset ring.",
            "hasPlay": false
          },
          {
            "name": "Decorative",
            "id": "components-swatch--decorative",
            "description": "Next to text that already names the color, the swatch is decorative.",
            "hasPlay": true
          },
          {
            "name": "ShortHex",
            "id": "components-swatch--short-hex",
            "hasPlay": false
          },
          {
            "name": "NoColor",
            "id": "components-swatch--no-color",
            "description": "Model data without a color (`undefined`, `null` or `''`) renders an empty dashed circle.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Swatch/Swatch.md",
        "summary": "A small circle filled with a color, with a subtle inset ring so dark colors stay visible on dark surfaces. It shows a color; it never names it, so every swatch is either labelled or explicitly decorative.\n\n- Handoff source: `components/erp/Swatch.jsx` (TODO `CMP-PAT-03`).",
        "whenToUse": [
          "Next to the name of a color, tag or category (a legend, a table cell, an option).",
          "On its own, with a `label`, when the color is the whole content (a color chip in a toolbar)."
        ],
        "whenNotToUse": [
          "To carry a status: use `Badge` (text plus tone).",
          "To pick a color: use `ColorOption` from `loui-ds/patterns`.",
          "For arbitrary CSS colors (`rgb()`, names): only `#rgb` and `#rrggbb` are accepted."
        ],
        "keyboard": [],
        "examples": [
          "<Swatch color=\"#1f3a93\" label=\"Navy\" />\n\n<span>\n  <Swatch color=\"#c0392b\" decorative size=\"sm\" /> Brick red\n</span>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Next to the name of a color, tag or category (a legend, a table cell, an option).\n- On its own, with a `label`, when the color is the whole content (a color chip in a toolbar)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To carry a status: use `Badge` (text plus tone).\n- To pick a color: use `ColorOption` from `loui-ds/patterns`.\n- For arbitrary CSS colors (`rgb()`, names): only `#rgb` and `#rrggbb` are accepted."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan[role=img] aria-label=\"Navy\"        (labelled)\nspan[aria-hidden]                        (decorative)\n  background: var(--ds-swatch-color)\n  inset ring: var(--ds-swatch-ring)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop         | Type                   | Default | Notes                                                                                                       |\n| ------------ | ---------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |\n| `color`      | `string \\| null`       | -       | `#rgb` or `#rrggbb`. Missing (`undefined`, `null`, `''`) = empty circle. Other values throw in development. |\n| `label`      | `string`               | -       | Name of the color. Required unless `decorative`.                                                            |\n| `decorative` | `true`                 | -       | Hides the swatch from assistive technology. Use only next to a name.                                        |\n| `size`       | `'sm' \\| 'md' \\| 'lg'` | `'md'`  | 16px, 20px or 32px (`--ds-swatch-size-*`).                                                                  |\n\nThe types make `label` and `decorative` mutually exclusive and require one of them.\n\n### Missing color\n\nModel data often has no color (a category without one, a `transparent` default that was never set). `undefined`, `null` and `''` render an empty dashed circle and never throw, so the call site does not need a guard. It is still a swatch: keep it `decorative` next to a name, or give it a `label` such as \"No color\". Any other non-hex value (`transparent`, `red`, `rgb(...)`) is a bug and throws in development; use the exported `isHexColor(value)` guard (a plain boolean) to decide yourself:\n\n```tsx\nimport { Swatch, isHexColor } from 'loui-ds';\n\n<Swatch color={isHexColor(category.color) ? category.color : undefined} decorative />;\n```"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A color on its own is not information: it is invisible to screen readers and ambiguous for color-blind users, hence the required `label` or `decorative`.\n- Labelled swatches are `role=\"img\"`. Decorative ones are `aria-hidden`.\n- The inset ring uses `--ds-swatch-ring`, a low-contrast outline that keeps near-black swatches visible. It is not a boundary meant to reach 3:1: the name next to or on the swatch is what identifies it.\n- Forced colors: `forced-color-adjust: none` keeps the fill, and a `ButtonText` border is added."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are plain color names in sentence case (\"Navy\", \"Brick red\"), not hex codes."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Swatch color=\"#1f3a93\" label=\"Navy\" />\n\n<span>\n  <Swatch color=\"#c0392b\" decorative size=\"sm\" /> Brick red\n</span>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `label` or `decorative` is required (the handoff had neither by default and used `title`, a mouse-only tooltip).\n- The color is applied through the `--ds-swatch-color` custom property, never as an inline `background` (ADR-0002 amendment). The inset ring is a token (`--ds-swatch-ring`) instead of `rgba(255,255,255,.12)`.\n- The hex value is validated: development throws a `RangeError` for a non-empty invalid value, production renders an empty dashed circle. A missing color (`undefined`, `null`, `''`) is accepted and renders the same empty circle (GAP-043).\n- Sizes are tokens (`sm`/`md`/`lg`) instead of a free pixel number.\n- React Aria `ColorSwatch` was evaluated and not used: it paints an inline `background` and names itself with a generated color description (\"vibrant blue\") that cannot be replaced by the product's own name."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Badge",
            "reason": "To carry a status: use `Badge` (text plus tone)."
          },
          {
            "name": "ColorOption",
            "reason": "To pick a color: use `ColorOption` from `loui-ds/patterns`."
          }
        ],
        "uses": [],
        "usedBy": ["ApprovalCard", "ColorField", "ColorOption", "MatrixGrid", "RankList"]
      }
    },
    {
      "name": "Switch",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Switch } from 'loui-ds';",
      "folder": "src/components/Switch",
      "sourceFile": "src/components/Switch/Switch.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "An on/off setting that applies immediately. Renders a native checkbox with `role=\"switch\"`\n(so `name` / `value` take part in form submission), a 36x20 track and a 16px knob.\n\nState props follow React Aria: `isSelected`, `defaultSelected` and `onChange(isSelected)`.\nFor a choice that is confirmed later with a Save button, prefer `Checkbox`.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Secondary text under the label. Linked with `aria-describedby`, not part of the name."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the switch is invalid and the error is displayed."
        },
        {
          "name": "labelPlacement",
          "type": "'start' | 'end'",
          "required": false,
          "default": "'start'",
          "description": "Where the label sits relative to the track. `start` (default): text on the leading side, track\non the trailing side, spread across the row, the settings-list layout. `end`: track first.",
          "values": ["start", "end"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance (`requiredMark`)."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`. Use `inputRef` for the `<input>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaSwitchProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", SwitchFieldRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SwitchFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "inputRef",
              "type": "Ref<HTMLInputElement | null>",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleStateOptions",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(isSelected: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: boolean) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-focus-ring-danger",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-space-1",
        "--ds-space-4",
        "--ds-switch-knob-offset",
        "--ds-switch-knob-size",
        "--ds-switch-track-height",
        "--ds-switch-track-width",
        "--ds-text",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/Switch",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-switch--default",
            "hasPlay": true
          },
          {
            "name": "On",
            "id": "components-switch--on",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-switch--with-description",
            "hasPlay": true
          },
          {
            "name": "LabelAtEnd",
            "id": "components-switch--label-at-end",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-switch--disabled",
            "hasPlay": false
          },
          {
            "name": "ReadOnly",
            "id": "components-switch--read-only",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-switch--required",
            "hasPlay": false
          },
          {
            "name": "Invalid",
            "id": "components-switch--invalid",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-switch--long-label",
            "hasPlay": false
          },
          {
            "name": "WithoutVisibleLabel",
            "id": "components-switch--without-visible-label",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Switch/Switch.md",
        "summary": "An on/off setting that takes effect immediately. A native checkbox with `role=\"switch\"`, a 36x20 track and a 16px knob. Built on React Aria `SwitchField` + `SwitchButton` (the non-deprecated pair that supports a description and an error outside the label). Shared field behavior is in [Field](../Field/Field.md).",
        "whenToUse": [
          "Settings that apply as soon as they change (\"Email notifications\"), with the state obvious from the label."
        ],
        "whenNotToUse": [
          "Choices confirmed later with a Save button or submitted as an agreement: `Checkbox`.",
          "One of several options: `RadioGroup`."
        ],
        "keyboard": [
          "`Space` toggles the focused switch. `Enter` does not toggle (it submits the form, as for any native checkbox)."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Settings that apply as soon as they change (\"Email notifications\"), with the state obvious from the label."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choices confirmed later with a Save button or submitted as an agreement: `Checkbox`.\n- One of several options: `RadioGroup`."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                                  | Notes                                                                                                               |\n| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |\n| `label`                                                               | Visible label. Without it pass `aria-label` or `aria-labelledby` (renders only the track).                          |\n| `description`                                                         | Secondary text under the label, linked by `aria-describedby`, not part of the name.                                 |\n| `labelPlacement`                                                      | `start` (default): text on the leading side, track on the trailing side, spread across the row. `end`: track first. |\n| `isSelected` / `defaultSelected` / `onChange(isSelected)`             | React Aria naming.                                                                                                  |\n| `name`, `value`                                                       | Submitted with the form while on (`value` defaults to `on`).                                                        |\n| `isDisabled`, `isReadOnly`, `isRequired`, `isInvalid`, `errorMessage` | Standard states.                                                                                                    |\n| `inputRef` / `ref`                                                    | `inputRef` is the `<input>`, `ref` the field root.                                                                  |"
          },
          {
            "id": "look",
            "heading": "Look",
            "content": "- Off: track in `--ds-line-control` (3:1 or better against the page, also on hover); on: `--ds-accent`. The knob uses `--ds-text` and moves with `inset-inline-start`, so it mirrors in right-to-left.\n- Sizes come from component tokens: `--ds-switch-track-width`, `--ds-switch-track-height`, `--ds-switch-knob-size`, `--ds-switch-knob-offset`.\n- The row is at least as tall as the track and the whole row is the click target, with one toggle per click.\n- Reduced motion: transitions are removed by the base layer.\n- Forced colors: the track gets a system border and a `Highlight` fill when on; the knob uses `CanvasText`."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `Space` toggles the focused switch. `Enter` does not toggle (it submits the form, as for any native checkbox).\n- Announced as \"switch, on/off\", named by the label only; the description is `aria-describedby`.\n- State is conveyed by position and by color, and by the announced state."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Switch label=\"Email notifications\" description=\"A summary every morning.\" defaultSelected />\n<Switch label=\"Dark mode\" labelPlacement=\"end\" name=\"theme\" value=\"dark\" />\n<Switch aria-label=\"Enable sync\" />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A native checkbox with `role=\"switch\"` replaces `role=\"switch\"` on a span inside a clickable row: there is no double toggle, and the control has an accessible name and description.\n- The off track meets 3:1 (`--ds-line-control` instead of `--line-strong`).\n- Layout is `labelPlacement`; the text-left, switch-right layout of the handoff is the default.\n- Dimensions are component tokens; controlled props follow React Aria."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Checkbox",
            "reason": "Choices confirmed later with a Save button or submitted as an agreement: `Checkbox`."
          },
          {
            "name": "RadioGroup",
            "reason": "One of several options: `RadioGroup`."
          }
        ],
        "uses": ["FieldMessages", "RequiredMark"],
        "usedBy": []
      }
    },
    {
      "name": "Tab",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Tab } from 'loui-ds';",
      "folder": "src/components/Tabs",
      "sourceFile": "src/components/Tabs/Tabs.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "One tab of a `TabList`, with an optional count and shortcut hint.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "Tab label: a noun in sentence case (\"Orders\")."
        },
        {
          "name": "count",
          "type": "string | number",
          "required": false,
          "description": "A count shown after the label. Numbers are formatted for the active locale. The count is\npart of the tab's accessible name, so pair it with `countLabel` when the bare number is\nambiguous."
        },
        {
          "name": "countLabel",
          "type": "string",
          "required": false,
          "description": "Accessible text for the count (\"3 pending\"). When set, it replaces the visible count in the\naccessible name. Recommended whenever `countTone` is not `neutral`."
        },
        {
          "name": "countTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'neutral'",
          "description": "Tone of the count. `neutral` (default) is plain mono text; the other tones draw a soft pill.\nColor never carries the meaning alone: use `countLabel` to say what the count means.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "shortcut",
          "type": "readonly string[]",
          "required": false,
          "description": "Keys of a shortcut hint shown after the label, as for `Kbd` (`['1']`, `['Mod', 'K']`).\nDecorative: hidden from assistive technology, and it registers nothing. Register the\nshortcut itself with `useHotkey`."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the tab element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TabsRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "LinkDOMProps",
          "domGeneric": false,
          "count": 8,
          "props": [
            {
              "name": "download",
              "type": "string | boolean",
              "required": false
            },
            {
              "name": "href",
              "type": "string",
              "required": false
            },
            {
              "name": "hrefLang",
              "type": "string",
              "required": false
            },
            {
              "name": "ping",
              "type": "string",
              "required": false
            },
            {
              "name": "referrerPolicy",
              "type": "'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'",
              "required": false
            },
            {
              "name": "rel",
              "type": "string",
              "required": false
            },
            {
              "name": "routerOptions",
              "type": "never",
              "required": false
            },
            {
              "name": "target",
              "type": "HTMLAttributeAnchorTarget",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TabProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "id",
              "type": "Key",
              "required": false
            },
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-success-soft",
        "--ds-tabs-count-padding-inline",
        "--ds-tabs-count-size",
        "--ds-tabs-divider-width",
        "--ds-tabs-gap",
        "--ds-tabs-indicator-height",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Tabs",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tabs--default",
            "hasPlay": true
          },
          {
            "name": "ArrowKeys",
            "id": "components-tabs--arrow-keys",
            "description": "Roving tab stop: Tab enters the list once, arrows move and select, Home and End jump.",
            "hasPlay": true
          },
          {
            "name": "FocusMovesIntoPanel",
            "id": "components-tabs--focus-moves-into-panel",
            "description": "Tab moves from the selected tab into its panel.",
            "hasPlay": true
          },
          {
            "name": "ManualActivation",
            "id": "components-tabs--manual-activation",
            "description": "Manual activation: arrows only move focus; Enter or Space selects.",
            "hasPlay": true
          },
          {
            "name": "Counts",
            "id": "components-tabs--counts",
            "hasPlay": true
          },
          {
            "name": "ShortcutHints",
            "id": "components-tabs--shortcut-hints",
            "description": "The shortcut hint is decorative: it is not part of the accessible name.",
            "hasPlay": true
          },
          {
            "name": "DisabledTab",
            "id": "components-tabs--disabled-tab",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-tabs--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-tabs--long-labels",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tabs--right-to-left",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-tabs--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tabs/Tabs.md",
        "summary": "Tabs switch between panels of related content on the same page. Built on React Aria `Tabs`, `TabList`, `Tab` and `TabPanel`: one tab stop, arrow keys between tabs, panels linked to their tabs.\n\n- Handoff source: `components/navigation/Tabs.jsx` (TODO `CMP-NAV-01`).",
        "whenToUse": [
          "Several views of the same object (Details, Items, History) where only one is shown at a time.",
          "Filtering a list into a few named groups that each have their own panel."
        ],
        "whenNotToUse": [
          "To pick a value without panels (a view mode, a unit): use `SegmentedControl`.",
          "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)",
          "For more than about seven tabs, or for steps of a process."
        ],
        "keyboard": [
          "Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped."
        ],
        "examples": [
          "<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Several views of the same object (Details, Items, History) where only one is shown at a time.\n- Filtering a list into a few named groups that each have their own panel."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To pick a value without panels (a view mode, a unit): use `SegmentedControl`.\n- To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)\n- For more than about seven tabs, or for steps of a process."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTabs                         (root, flex column)\n  TabList  aria-label        (role=tablist, 1px bottom line)\n    Tab id                   (role=tab; 2px ink underline when selected)\n      label\n      count                  (optional; neutral text or a toned pill)\n      shortcut hint          (optional Kbd, aria-hidden)\n  TabPanel id                (role=tabpanel, labelled by its tab)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Component  | Prop                                 | Notes                                                                                             |\n| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `Tabs`     | `selectedKey` / `defaultSelectedKey` | Controlled / uncontrolled selection (React Aria naming). Defaults to the first enabled tab.       |\n|            | `onSelectionChange(key)`             | Called when the user selects a tab.                                                               |\n|            | `keyboardActivation`                 | `'automatic'` (default): arrows select. `'manual'`: arrows move focus, `Enter` / `Space` select.  |\n|            | `isDisabled`, `disabledKeys`         | Disable all or some tabs.                                                                         |\n| `TabList`  | `aria-label` / `aria-labelledby`     | Name the list. Required when there is more than one tab list on the page.                         |\n| `Tab`      | `id`                                 | Required; must equal the `id` of its `TabPanel`.                                                  |\n|            | `count`                              | Number (formatted for the locale) or string shown after the label.                                |\n|            | `countTone`                          | `neutral` (default, plain mono text), `accent`, `success`, `warning`, `danger` (soft pill).       |\n|            | `countLabel`                         | Accessible text for the count (\"3 pending\"). Replaces the bare number in the accessible name.     |\n|            | `shortcut`                           | `Kbd` keys (`['1']`). Decorative: `aria-hidden`, registers nothing. Register it with `useHotkey`. |\n|            | `isDisabled`, `href`                 | Disabled tab; or a tab that is a link.                                                            |\n| `TabPanel` | `id`, `shouldForceMount`             | `id` matches the tab. Force-mounted inactive panels are inert.                                    |\n\nAll parts accept `className` and `ref`; other props pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Roles: `tablist` > `tab` (with `aria-selected`, `aria-controls`) and `tabpanel` (`aria-labelledby` the tab).\n- Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped.\n- Activation: automatic by default. Use `keyboardActivation=\"manual\"` when showing a panel is slow or has side effects, so arrowing across tabs does not trigger work.\n- Count: the visible count is part of the tab name (\"Orders 12\"). With `countLabel` the visible number is hidden from assistive technology and the label is read instead (\"Pending 3 pending\"). Color never carries the meaning, so a toned count always needs words.\n- Shortcut hint: decorative; it does not change the name. Tell users about the shortcut elsewhere (help, tooltip) if it matters.\n- Contrast: inactive tab text is `--ds-text-3` on `--ds-bg` (passes). Do not place tabs on a raised or hover surface without checking.\n- Focus: accent ring on the tab and on a focused panel. Forced colors: the selected underline uses `Highlight`.\n- Reduced motion: only a color transition, neutralized by the base styles."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns in sentence case (\"Order items\"), one or two words.\n- Do not repeat the count in the label. `countLabel` says what the number counts (\"3 pending\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Compound parts (`Tabs`, `TabList`, `Tab`, `TabPanel`) instead of an `items` array, so panels can be linked. The handoff had no panels.\n- Real `tablist` semantics with roving focus, `Home` / `End`, `aria-controls` / `aria-labelledby`, and automatic or manual activation. The handoff only had click handlers.\n- The count and shortcut live on `Tab`; counts are formatted with `Intl` and can carry a text alternative.\n- The vertical orientation is not supported (the prop is omitted).\n- No inline strings, so there is no message catalog; the shortcut hint reuses `Kbd`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "To pick a value without panels (a view mode, a unit): use `SegmentedControl`."
          },
          {
            "name": "NavItem",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Link",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          }
        ],
        "uses": ["Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "TabList",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { TabList } from 'loui-ds';",
      "folder": "src/components/Tabs",
      "sourceFile": "src/components/Tabs/Tabs.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The row of tabs. Name it with `aria-label` (or `aria-labelledby`) so screen readers can\ntell several tab lists apart.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The `Tab`s. Each `Tab` needs an `id` that matches a `TabPanel`."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the `tablist` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "items",
              "type": "Iterable<object>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "CollectionProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "dependencies",
              "type": "readonly any[]",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TabsRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-success-soft",
        "--ds-tabs-count-padding-inline",
        "--ds-tabs-count-size",
        "--ds-tabs-divider-width",
        "--ds-tabs-gap",
        "--ds-tabs-indicator-height",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Tabs",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tabs--default",
            "hasPlay": true
          },
          {
            "name": "ArrowKeys",
            "id": "components-tabs--arrow-keys",
            "description": "Roving tab stop: Tab enters the list once, arrows move and select, Home and End jump.",
            "hasPlay": true
          },
          {
            "name": "FocusMovesIntoPanel",
            "id": "components-tabs--focus-moves-into-panel",
            "description": "Tab moves from the selected tab into its panel.",
            "hasPlay": true
          },
          {
            "name": "ManualActivation",
            "id": "components-tabs--manual-activation",
            "description": "Manual activation: arrows only move focus; Enter or Space selects.",
            "hasPlay": true
          },
          {
            "name": "Counts",
            "id": "components-tabs--counts",
            "hasPlay": true
          },
          {
            "name": "ShortcutHints",
            "id": "components-tabs--shortcut-hints",
            "description": "The shortcut hint is decorative: it is not part of the accessible name.",
            "hasPlay": true
          },
          {
            "name": "DisabledTab",
            "id": "components-tabs--disabled-tab",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-tabs--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-tabs--long-labels",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tabs--right-to-left",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-tabs--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tabs/Tabs.md",
        "summary": "Tabs switch between panels of related content on the same page. Built on React Aria `Tabs`, `TabList`, `Tab` and `TabPanel`: one tab stop, arrow keys between tabs, panels linked to their tabs.\n\n- Handoff source: `components/navigation/Tabs.jsx` (TODO `CMP-NAV-01`).",
        "whenToUse": [
          "Several views of the same object (Details, Items, History) where only one is shown at a time.",
          "Filtering a list into a few named groups that each have their own panel."
        ],
        "whenNotToUse": [
          "To pick a value without panels (a view mode, a unit): use `SegmentedControl`.",
          "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)",
          "For more than about seven tabs, or for steps of a process."
        ],
        "keyboard": [
          "Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped."
        ],
        "examples": [
          "<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Several views of the same object (Details, Items, History) where only one is shown at a time.\n- Filtering a list into a few named groups that each have their own panel."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To pick a value without panels (a view mode, a unit): use `SegmentedControl`.\n- To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)\n- For more than about seven tabs, or for steps of a process."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTabs                         (root, flex column)\n  TabList  aria-label        (role=tablist, 1px bottom line)\n    Tab id                   (role=tab; 2px ink underline when selected)\n      label\n      count                  (optional; neutral text or a toned pill)\n      shortcut hint          (optional Kbd, aria-hidden)\n  TabPanel id                (role=tabpanel, labelled by its tab)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Component  | Prop                                 | Notes                                                                                             |\n| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `Tabs`     | `selectedKey` / `defaultSelectedKey` | Controlled / uncontrolled selection (React Aria naming). Defaults to the first enabled tab.       |\n|            | `onSelectionChange(key)`             | Called when the user selects a tab.                                                               |\n|            | `keyboardActivation`                 | `'automatic'` (default): arrows select. `'manual'`: arrows move focus, `Enter` / `Space` select.  |\n|            | `isDisabled`, `disabledKeys`         | Disable all or some tabs.                                                                         |\n| `TabList`  | `aria-label` / `aria-labelledby`     | Name the list. Required when there is more than one tab list on the page.                         |\n| `Tab`      | `id`                                 | Required; must equal the `id` of its `TabPanel`.                                                  |\n|            | `count`                              | Number (formatted for the locale) or string shown after the label.                                |\n|            | `countTone`                          | `neutral` (default, plain mono text), `accent`, `success`, `warning`, `danger` (soft pill).       |\n|            | `countLabel`                         | Accessible text for the count (\"3 pending\"). Replaces the bare number in the accessible name.     |\n|            | `shortcut`                           | `Kbd` keys (`['1']`). Decorative: `aria-hidden`, registers nothing. Register it with `useHotkey`. |\n|            | `isDisabled`, `href`                 | Disabled tab; or a tab that is a link.                                                            |\n| `TabPanel` | `id`, `shouldForceMount`             | `id` matches the tab. Force-mounted inactive panels are inert.                                    |\n\nAll parts accept `className` and `ref`; other props pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Roles: `tablist` > `tab` (with `aria-selected`, `aria-controls`) and `tabpanel` (`aria-labelledby` the tab).\n- Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped.\n- Activation: automatic by default. Use `keyboardActivation=\"manual\"` when showing a panel is slow or has side effects, so arrowing across tabs does not trigger work.\n- Count: the visible count is part of the tab name (\"Orders 12\"). With `countLabel` the visible number is hidden from assistive technology and the label is read instead (\"Pending 3 pending\"). Color never carries the meaning, so a toned count always needs words.\n- Shortcut hint: decorative; it does not change the name. Tell users about the shortcut elsewhere (help, tooltip) if it matters.\n- Contrast: inactive tab text is `--ds-text-3` on `--ds-bg` (passes). Do not place tabs on a raised or hover surface without checking.\n- Focus: accent ring on the tab and on a focused panel. Forced colors: the selected underline uses `Highlight`.\n- Reduced motion: only a color transition, neutralized by the base styles."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns in sentence case (\"Order items\"), one or two words.\n- Do not repeat the count in the label. `countLabel` says what the number counts (\"3 pending\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Compound parts (`Tabs`, `TabList`, `Tab`, `TabPanel`) instead of an `items` array, so panels can be linked. The handoff had no panels.\n- Real `tablist` semantics with roving focus, `Home` / `End`, `aria-controls` / `aria-labelledby`, and automatic or manual activation. The handoff only had click handlers.\n- The count and shortcut live on `Tab`; counts are formatted with `Intl` and can carry a text alternative.\n- The vertical orientation is not supported (the prop is omitted).\n- No inline strings, so there is no message catalog; the shortcut hint reuses `Kbd`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "To pick a value without panels (a view mode, a unit): use `SegmentedControl`."
          },
          {
            "name": "NavItem",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Link",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Tab",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          }
        ],
        "uses": ["Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "TabPanel",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { TabPanel } from 'loui-ds';",
      "folder": "src/components/Tabs",
      "sourceFile": "src/components/Tabs/Tabs.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The content of one tab. `id` must equal the `id` of its `Tab`; the panel is then labelled by\nthe tab and the tab points to the panel (`aria-controls`). The panel is focusable when it\nhas no focusable content, so keyboard users can reach it.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Panel content."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the `tabpanel` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTabPanelProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "Key",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TabsRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TabPanelProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "shouldForceMount",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-success-soft",
        "--ds-tabs-count-padding-inline",
        "--ds-tabs-count-size",
        "--ds-tabs-divider-width",
        "--ds-tabs-gap",
        "--ds-tabs-indicator-height",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Tabs",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tabs--default",
            "hasPlay": true
          },
          {
            "name": "ArrowKeys",
            "id": "components-tabs--arrow-keys",
            "description": "Roving tab stop: Tab enters the list once, arrows move and select, Home and End jump.",
            "hasPlay": true
          },
          {
            "name": "FocusMovesIntoPanel",
            "id": "components-tabs--focus-moves-into-panel",
            "description": "Tab moves from the selected tab into its panel.",
            "hasPlay": true
          },
          {
            "name": "ManualActivation",
            "id": "components-tabs--manual-activation",
            "description": "Manual activation: arrows only move focus; Enter or Space selects.",
            "hasPlay": true
          },
          {
            "name": "Counts",
            "id": "components-tabs--counts",
            "hasPlay": true
          },
          {
            "name": "ShortcutHints",
            "id": "components-tabs--shortcut-hints",
            "description": "The shortcut hint is decorative: it is not part of the accessible name.",
            "hasPlay": true
          },
          {
            "name": "DisabledTab",
            "id": "components-tabs--disabled-tab",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-tabs--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-tabs--long-labels",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tabs--right-to-left",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-tabs--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tabs/Tabs.md",
        "summary": "Tabs switch between panels of related content on the same page. Built on React Aria `Tabs`, `TabList`, `Tab` and `TabPanel`: one tab stop, arrow keys between tabs, panels linked to their tabs.\n\n- Handoff source: `components/navigation/Tabs.jsx` (TODO `CMP-NAV-01`).",
        "whenToUse": [
          "Several views of the same object (Details, Items, History) where only one is shown at a time.",
          "Filtering a list into a few named groups that each have their own panel."
        ],
        "whenNotToUse": [
          "To pick a value without panels (a view mode, a unit): use `SegmentedControl`.",
          "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)",
          "For more than about seven tabs, or for steps of a process."
        ],
        "keyboard": [
          "Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped."
        ],
        "examples": [
          "<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Several views of the same object (Details, Items, History) where only one is shown at a time.\n- Filtering a list into a few named groups that each have their own panel."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To pick a value without panels (a view mode, a unit): use `SegmentedControl`.\n- To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)\n- For more than about seven tabs, or for steps of a process."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTabs                         (root, flex column)\n  TabList  aria-label        (role=tablist, 1px bottom line)\n    Tab id                   (role=tab; 2px ink underline when selected)\n      label\n      count                  (optional; neutral text or a toned pill)\n      shortcut hint          (optional Kbd, aria-hidden)\n  TabPanel id                (role=tabpanel, labelled by its tab)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Component  | Prop                                 | Notes                                                                                             |\n| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `Tabs`     | `selectedKey` / `defaultSelectedKey` | Controlled / uncontrolled selection (React Aria naming). Defaults to the first enabled tab.       |\n|            | `onSelectionChange(key)`             | Called when the user selects a tab.                                                               |\n|            | `keyboardActivation`                 | `'automatic'` (default): arrows select. `'manual'`: arrows move focus, `Enter` / `Space` select.  |\n|            | `isDisabled`, `disabledKeys`         | Disable all or some tabs.                                                                         |\n| `TabList`  | `aria-label` / `aria-labelledby`     | Name the list. Required when there is more than one tab list on the page.                         |\n| `Tab`      | `id`                                 | Required; must equal the `id` of its `TabPanel`.                                                  |\n|            | `count`                              | Number (formatted for the locale) or string shown after the label.                                |\n|            | `countTone`                          | `neutral` (default, plain mono text), `accent`, `success`, `warning`, `danger` (soft pill).       |\n|            | `countLabel`                         | Accessible text for the count (\"3 pending\"). Replaces the bare number in the accessible name.     |\n|            | `shortcut`                           | `Kbd` keys (`['1']`). Decorative: `aria-hidden`, registers nothing. Register it with `useHotkey`. |\n|            | `isDisabled`, `href`                 | Disabled tab; or a tab that is a link.                                                            |\n| `TabPanel` | `id`, `shouldForceMount`             | `id` matches the tab. Force-mounted inactive panels are inert.                                    |\n\nAll parts accept `className` and `ref`; other props pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Roles: `tablist` > `tab` (with `aria-selected`, `aria-controls`) and `tabpanel` (`aria-labelledby` the tab).\n- Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped.\n- Activation: automatic by default. Use `keyboardActivation=\"manual\"` when showing a panel is slow or has side effects, so arrowing across tabs does not trigger work.\n- Count: the visible count is part of the tab name (\"Orders 12\"). With `countLabel` the visible number is hidden from assistive technology and the label is read instead (\"Pending 3 pending\"). Color never carries the meaning, so a toned count always needs words.\n- Shortcut hint: decorative; it does not change the name. Tell users about the shortcut elsewhere (help, tooltip) if it matters.\n- Contrast: inactive tab text is `--ds-text-3` on `--ds-bg` (passes). Do not place tabs on a raised or hover surface without checking.\n- Focus: accent ring on the tab and on a focused panel. Forced colors: the selected underline uses `Highlight`.\n- Reduced motion: only a color transition, neutralized by the base styles."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns in sentence case (\"Order items\"), one or two words.\n- Do not repeat the count in the label. `countLabel` says what the number counts (\"3 pending\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Compound parts (`Tabs`, `TabList`, `Tab`, `TabPanel`) instead of an `items` array, so panels can be linked. The handoff had no panels.\n- Real `tablist` semantics with roving focus, `Home` / `End`, `aria-controls` / `aria-labelledby`, and automatic or manual activation. The handoff only had click handlers.\n- The count and shortcut live on `Tab`; counts are formatted with `Intl` and can carry a text alternative.\n- The vertical orientation is not supported (the prop is omitted).\n- No inline strings, so there is no message catalog; the shortcut hint reuses `Kbd`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "To pick a value without panels (a view mode, a unit): use `SegmentedControl`."
          },
          {
            "name": "NavItem",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Link",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Tab",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          }
        ],
        "uses": ["Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Tabs",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Tabs } from 'loui-ds';",
      "folder": "src/components/Tabs",
      "sourceFile": "src/components/Tabs/Tabs.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Tabs switch between panels of related content. Built on React Aria `Tabs`: one tab stop,\n`Left` / `Right` / `Home` / `End` move between tabs, `Tab` moves into the panel.\n\nSelection follows RAC: `selectedKey` / `defaultSelectedKey` / `onSelectionChange`.\n`keyboardActivation=\"manual\"` makes the arrow keys only move focus; `Enter` or `Space`\nthen activates the tab (use it when showing a panel is slow or has side effects).",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "A `TabList` followed by one `TabPanel` per tab."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the root element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTabListProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "keyboardActivation",
              "type": "'automatic' | 'manual'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TabsRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "TabListProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "defaultSelectedKey",
              "type": "Key",
              "required": false
            },
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onSelectionChange",
              "type": "(key: Key) => void",
              "required": false
            },
            {
              "name": "selectedKey",
              "type": "Key",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-danger",
        "--ds-danger-soft",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-caption",
        "--ds-lh-small",
        "--ds-line",
        "--ds-opacity-disabled",
        "--ds-radius-pill",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-success-soft",
        "--ds-tabs-count-padding-inline",
        "--ds-tabs-count-size",
        "--ds-tabs-divider-width",
        "--ds-tabs-gap",
        "--ds-tabs-indicator-height",
        "--ds-target-min",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-warning",
        "--ds-warning-soft",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Tabs",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tabs--default",
            "hasPlay": true
          },
          {
            "name": "ArrowKeys",
            "id": "components-tabs--arrow-keys",
            "description": "Roving tab stop: Tab enters the list once, arrows move and select, Home and End jump.",
            "hasPlay": true
          },
          {
            "name": "FocusMovesIntoPanel",
            "id": "components-tabs--focus-moves-into-panel",
            "description": "Tab moves from the selected tab into its panel.",
            "hasPlay": true
          },
          {
            "name": "ManualActivation",
            "id": "components-tabs--manual-activation",
            "description": "Manual activation: arrows only move focus; Enter or Space selects.",
            "hasPlay": true
          },
          {
            "name": "Counts",
            "id": "components-tabs--counts",
            "hasPlay": true
          },
          {
            "name": "ShortcutHints",
            "id": "components-tabs--shortcut-hints",
            "description": "The shortcut hint is decorative: it is not part of the accessible name.",
            "hasPlay": true
          },
          {
            "name": "DisabledTab",
            "id": "components-tabs--disabled-tab",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-tabs--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabels",
            "id": "components-tabs--long-labels",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tabs--right-to-left",
            "hasPlay": true
          },
          {
            "name": "Focused",
            "id": "components-tabs--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tabs/Tabs.md",
        "summary": "Tabs switch between panels of related content on the same page. Built on React Aria `Tabs`, `TabList`, `Tab` and `TabPanel`: one tab stop, arrow keys between tabs, panels linked to their tabs.\n\n- Handoff source: `components/navigation/Tabs.jsx` (TODO `CMP-NAV-01`).",
        "whenToUse": [
          "Several views of the same object (Details, Items, History) where only one is shown at a time.",
          "Filtering a list into a few named groups that each have their own panel."
        ],
        "whenNotToUse": [
          "To pick a value without panels (a view mode, a unit): use `SegmentedControl`.",
          "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)",
          "For more than about seven tabs, or for steps of a process."
        ],
        "keyboard": [
          "Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped."
        ],
        "examples": [
          "<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Several views of the same object (Details, Items, History) where only one is shown at a time.\n- Filtering a list into a few named groups that each have their own panel."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To pick a value without panels (a view mode, a unit): use `SegmentedControl`.\n- To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)\n- For more than about seven tabs, or for steps of a process."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTabs                         (root, flex column)\n  TabList  aria-label        (role=tablist, 1px bottom line)\n    Tab id                   (role=tab; 2px ink underline when selected)\n      label\n      count                  (optional; neutral text or a toned pill)\n      shortcut hint          (optional Kbd, aria-hidden)\n  TabPanel id                (role=tabpanel, labelled by its tab)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Component  | Prop                                 | Notes                                                                                             |\n| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `Tabs`     | `selectedKey` / `defaultSelectedKey` | Controlled / uncontrolled selection (React Aria naming). Defaults to the first enabled tab.       |\n|            | `onSelectionChange(key)`             | Called when the user selects a tab.                                                               |\n|            | `keyboardActivation`                 | `'automatic'` (default): arrows select. `'manual'`: arrows move focus, `Enter` / `Space` select.  |\n|            | `isDisabled`, `disabledKeys`         | Disable all or some tabs.                                                                         |\n| `TabList`  | `aria-label` / `aria-labelledby`     | Name the list. Required when there is more than one tab list on the page.                         |\n| `Tab`      | `id`                                 | Required; must equal the `id` of its `TabPanel`.                                                  |\n|            | `count`                              | Number (formatted for the locale) or string shown after the label.                                |\n|            | `countTone`                          | `neutral` (default, plain mono text), `accent`, `success`, `warning`, `danger` (soft pill).       |\n|            | `countLabel`                         | Accessible text for the count (\"3 pending\"). Replaces the bare number in the accessible name.     |\n|            | `shortcut`                           | `Kbd` keys (`['1']`). Decorative: `aria-hidden`, registers nothing. Register it with `useHotkey`. |\n|            | `isDisabled`, `href`                 | Disabled tab; or a tab that is a link.                                                            |\n| `TabPanel` | `id`, `shouldForceMount`             | `id` matches the tab. Force-mounted inactive panels are inert.                                    |\n\nAll parts accept `className` and `ref`; other props pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Roles: `tablist` > `tab` (with `aria-selected`, `aria-controls`) and `tabpanel` (`aria-labelledby` the tab).\n- Keyboard: `Tab` enters the list once (roving tab stop) and the next `Tab` goes to the panel; `←` / `→` move (and wrap), `Home` / `End` jump. In right-to-left locales the arrows swap. Disabled tabs are skipped.\n- Activation: automatic by default. Use `keyboardActivation=\"manual\"` when showing a panel is slow or has side effects, so arrowing across tabs does not trigger work.\n- Count: the visible count is part of the tab name (\"Orders 12\"). With `countLabel` the visible number is hidden from assistive technology and the label is read instead (\"Pending 3 pending\"). Color never carries the meaning, so a toned count always needs words.\n- Shortcut hint: decorative; it does not change the name. Tell users about the shortcut elsewhere (help, tooltip) if it matters.\n- Contrast: inactive tab text is `--ds-text-3` on `--ds-bg` (passes). Do not place tabs on a raised or hover surface without checking.\n- Focus: accent ring on the tab and on a focused panel. Forced colors: the selected underline uses `Highlight`.\n- Reduced motion: only a color transition, neutralized by the base styles."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Labels are nouns in sentence case (\"Order items\"), one or two words.\n- Do not repeat the count in the label. `countLabel` says what the number counts (\"3 pending\")."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tabs defaultSelectedKey=\"items\">\n  <TabList aria-label=\"Order sections\">\n    <Tab id=\"details\">Details</Tab>\n    <Tab id=\"items\" count={12} countTone=\"warning\" countLabel=\"12 pending\">\n      Items\n    </Tab>\n    <Tab id=\"history\" shortcut={['3']}>\n      History\n    </Tab>\n  </TabList>\n  <TabPanel id=\"details\">…</TabPanel>\n  <TabPanel id=\"items\">…</TabPanel>\n  <TabPanel id=\"history\">…</TabPanel>\n</Tabs>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Compound parts (`Tabs`, `TabList`, `Tab`, `TabPanel`) instead of an `items` array, so panels can be linked. The handoff had no panels.\n- Real `tablist` semantics with roving focus, `Home` / `End`, `aria-controls` / `aria-labelledby`, and automatic or manual activation. The handoff only had click handlers.\n- The count and shortcut live on `Tab`; counts are formatted with `Intl` and can carry a text alternative.\n- The vertical orientation is not supported (the prop is omitted).\n- No inline strings, so there is no message catalog; the shortcut hint reuses `Kbd`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SegmentedControl",
            "reason": "To pick a value without panels (a view mode, a unit): use `SegmentedControl`."
          },
          {
            "name": "NavItem",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Link",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          },
          {
            "name": "Tab",
            "reason": "To move between pages or routes: use `NavItem` in a navigation list, or `Link`s. (`Tab` does accept `href`, but a primary navigation is not a tab list.)"
          }
        ],
        "uses": ["Kbd", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Tag",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Tag } from 'loui-ds';",
      "folder": "src/components/Tag",
      "sourceFile": "src/components/Tag/Tag.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A mono outlined label for versions, SKUs and filters. Standalone it is static text (with an\noptional remove button); inside a `TagGroup` it is a keyboard-navigable item that can be\nremoved with Backspace or Delete.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The text of the tag. Plain text, or any node together with `textValue`."
        },
        {
          "name": "textValue",
          "type": "string",
          "required": false,
          "description": "Plain-text version of the tag. Required when `children` is not a string: it names the tag\nfor screen readers and is used in the name of the remove button (\"Remove {textValue}\")."
        },
        {
          "name": "id",
          "type": "Key",
          "required": false,
          "description": "Inside a `TagGroup`: the key of the tag (used by `onRemove`, selection and `disabledKeys`).\nOutside a group: the DOM `id`."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Disables the tag (inside a group: keyboard removal and selection too)."
        },
        {
          "name": "onRemove",
          "type": "() => void",
          "required": false,
          "description": "Standalone tag only: shows a remove button that calls this. Inside a `TagGroup` use the\ngroup's `onRemove` instead (it also enables Backspace and Delete on the focused tag)."
        },
        {
          "name": "messages",
          "type": "Partial<{ remove: (label: string) => string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Tag",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "remove",
            "kind": "function",
            "default": "Remove {label}",
            "signature": "(label: string)",
            "description": "Name of the remove button of a tag: it includes the text of the tag."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-surface-hover",
        "--ds-tag-height",
        "--ds-tag-padding-inline",
        "--ds-tag-remove-size",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Tag",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tag--default",
            "hasPlay": false
          },
          {
            "name": "Removable",
            "id": "components-tag--removable",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-tag--disabled",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-tag--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tag--right-to-left",
            "hasPlay": false
          },
          {
            "name": "RemovableGroupStory",
            "id": "components-tag--removable-group-story",
            "description": "Arrow keys move between tags; Backspace or Delete removes the focused one.",
            "hasPlay": true
          },
          {
            "name": "GroupStatic",
            "id": "components-tag--group-static",
            "hasPlay": false
          },
          {
            "name": "GroupSelectable",
            "id": "components-tag--group-selectable",
            "hasPlay": true
          },
          {
            "name": "GroupWithDisabledTag",
            "id": "components-tag--group-with-disabled-tag",
            "hasPlay": false
          },
          {
            "name": "GroupEmpty",
            "id": "components-tag--group-empty",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Tag/Tag.md",
        "summary": "A mono outlined label for versions, SKUs and filters. `Tag` is static text on its own (with an optional remove button); inside a `TagGroup` it is an item with keyboard navigation, selection and removal. `TagGroup` is built on React Aria `TagGroup`, `TagList` and `Tag`.",
        "whenToUse": [
          "Show an attribute or identifier: `v2.4.0`, `ORD-1048`, `notebook`.",
          "Show applied filters that the user can remove (`TagGroup` with `onRemove`).",
          "Let the user toggle filters (`TagGroup` with `selectionMode`)."
        ],
        "whenNotToUse": [
          "For a status (Shipped, Failed): use `Badge`.",
          "For a command: use `Button`.",
          "For choosing one option from a list: use `Select` or `RadioGroup`."
        ],
        "keyboard": [],
        "examples": [
          "<Tag>v2.4.0</Tag>\n\n<TagGroup label=\"Product\" onRemove={(keys) => setTags((t) => t.filter((x) => !keys.has(x)))}>\n  {tags.map((tag) => <Tag key={tag} id={tag}>{tag}</Tag>)}\n</TagGroup>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Show an attribute or identifier: `v2.4.0`, `ORD-1048`, `notebook`.\n- Show applied filters that the user can remove (`TagGroup` with `onRemove`).\n- Let the user toggle filters (`TagGroup` with `selectionMode`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a status (Shipped, Failed): use `Badge`.\n- For a command: use `Button`.\n- For choosing one option from a list: use `Select` or `RadioGroup`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTagGroup           (grid; optional visible label)\n  Label\n  TagList          (flex wrap)\n    Tag            (row)\n      text\n      remove button  only when the group has onRemove (24x24 target)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Tag`\n\n| Prop         | Type                | Notes                                                                         |\n| ------------ | ------------------- | ----------------------------------------------------------------------------- |\n| `children`   | `ReactNode`         | The text                                                                      |\n| `textValue`  | `string`            | Required when `children` is not a string: names the tag and its remove button |\n| `id`         | `Key`               | In a group: the key. Standalone: the DOM id                                   |\n| `isDisabled` | `boolean`           | Dashed border and reduced opacity; not removable or selectable                |\n| `onRemove`   | `() => void`        | Standalone only: shows a remove button. In a group use the group's `onRemove` |\n| `messages`   | `{ remove(label) }` | Per-instance override of the remove button name                               |\n\n`TagGroup`\n\n| Prop                                                                                        | Notes                                                            |\n| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| `label`                                                                                     | Visible label. Without it set `aria-label` or `aria-labelledby`  |\n| `onRemove(keys: Set<Key>)`                                                                  | Enables removal: Backspace, Delete and the remove button call it |\n| `selectionMode`, `selectedKeys`, `defaultSelectedKeys`, `onSelectionChange`, `disabledKeys` | React Aria selection API                                         |\n| `items`, `children` (function)                                                              | Dynamic collections, as in React Aria                            |\n| `renderEmptyState`                                                                          | Content shown when no tag is left (\"No filters applied\")         |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A group is a `grid` with one `row` per tag. Arrow keys move between tags (roving focus); the focused tag is the only one in the tab order.\n- **Backspace and Delete remove the focused tag** (or all selected tags) when the group has `onRemove`. Keyboard users are told so by a description that React Aria adds to the focused tag.\n- **The remove button is named \"Remove {text}\"** from the message catalog (pt-BR: \"Remover {text}\"), not just \"Remove\". The button is also a tab stop after its tag, and is a 24 by 24 pixel target (WCAG 2.5.8).\n- After a removal, focus moves to a neighbouring tag (React Aria); handle the empty state with `renderEmptyState` and move focus to something sensible yourself if the whole group disappears.\n- Selection is exposed as `aria-selected`, and shown with the accent border and tint (a permitted use of the accent). Selection is never signalled by color alone: the state is announced.\n- A standalone `Tag` is plain text. With `onRemove` it contains a real `<button>` (Enter or Space); Backspace and Delete do nothing there because the tag itself is not focusable. Use a `TagGroup` when keyboard removal matters.\n- Text is `--ds-text-2` on the page surface; disabled tags are exempt from contrast and carry `aria-disabled`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Short, lowercase or code-like values are fine (`v2.4.0`, `ORD-1048`); filters use the user's own words.\n- Write a `textValue` that reads well after \"Remove\": \"Remove notebook\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tag>v2.4.0</Tag>\n\n<TagGroup label=\"Product\" onRemove={(keys) => setTags((t) => t.filter((x) => !keys.has(x)))}>\n  {tags.map((tag) => <Tag key={tag} id={tag}>{tag}</Tag>)}\n</TagGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The remove button name includes the tag text (it was a bare \"Remover\").\n- 24 px target for the remove button; height, padding and button size are tokens (`--ds-tag-*`).\n- `TagGroup` adds keyboard removal, roving focus, selection and an empty state.\n- The \"×\" glyph is the bundled `X` icon, hidden from assistive technology.\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Badge",
            "reason": "For a status (Shipped, Failed): use `Badge`."
          },
          {
            "name": "Button",
            "reason": "For a command: use `Button`."
          },
          {
            "name": "Select",
            "reason": "For choosing one option from a list: use `Select` or `RadioGroup`."
          },
          {
            "name": "RadioGroup",
            "reason": "For choosing one option from a list: use `Select` or `RadioGroup`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": ["TagInput"]
      }
    },
    {
      "name": "TagGroup",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { TagGroup } from 'loui-ds';",
      "folder": "src/components/Tag",
      "sourceFile": "src/components/Tag/Tag.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A group of tags with one tab stop and arrow-key navigation. With `onRemove`, Backspace and\nDelete remove the focused tag (or the selected tags) and every tag gets a remove button.\nBuilt on React Aria `TagGroup`.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode | ((item: T) => ReactNode)",
          "required": true,
          "description": "Static `Tag` children, or a render function when `items` is given."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label of the group. Without it, give the group an `aria-label` or `aria-labelledby`."
        },
        {
          "name": "onRemove",
          "type": "(keys: Set<Key>) => void",
          "required": false,
          "description": "Called with the keys of the tags to remove (Backspace, Delete or the remove button). Makes the tags removable."
        },
        {
          "name": "items",
          "type": "Iterable<T>",
          "required": false,
          "description": "Data for the render function. Each item needs an `id` (or pass `id` to the rendered `Tag`)."
        },
        {
          "name": "renderEmptyState",
          "type": "() => ReactNode",
          "required": false,
          "description": "Content shown when there are no tags (for example \"No filters applied\")."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 4,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-label",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-labelledby",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTagGroupProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "escapeKeyBehavior",
              "type": "'none' | 'clearSelection'",
              "required": false
            },
            {
              "name": "selectionBehavior",
              "type": "'toggle' | 'replace'",
              "required": false
            },
            {
              "name": "shouldSelectOnPressUp",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "CollectionBase",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "disabledKeys",
              "type": "Iterable<Key>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "GridListProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "onAction",
              "type": "(key: Key) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "MultipleSelection",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "defaultSelectedKeys",
              "type": "\"all\" | Iterable<Key>",
              "required": false
            },
            {
              "name": "disallowEmptySelection",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onSelectionChange",
              "type": "(keys: Selection) => void",
              "required": false
            },
            {
              "name": "selectedKeys",
              "type": "\"all\" | Iterable<Key>",
              "required": false
            },
            {
              "name": "selectionMode",
              "type": "'none' | 'single' | 'multiple'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-accent",
        "--ds-accent-soft",
        "--ds-focus-ring",
        "--ds-font-mono",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-surface-hover",
        "--ds-tag-height",
        "--ds-tag-padding-inline",
        "--ds-tag-remove-size",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Tag",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tag--default",
            "hasPlay": false
          },
          {
            "name": "Removable",
            "id": "components-tag--removable",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-tag--disabled",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-tag--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-tag--right-to-left",
            "hasPlay": false
          },
          {
            "name": "RemovableGroupStory",
            "id": "components-tag--removable-group-story",
            "description": "Arrow keys move between tags; Backspace or Delete removes the focused one.",
            "hasPlay": true
          },
          {
            "name": "GroupStatic",
            "id": "components-tag--group-static",
            "hasPlay": false
          },
          {
            "name": "GroupSelectable",
            "id": "components-tag--group-selectable",
            "hasPlay": true
          },
          {
            "name": "GroupWithDisabledTag",
            "id": "components-tag--group-with-disabled-tag",
            "hasPlay": false
          },
          {
            "name": "GroupEmpty",
            "id": "components-tag--group-empty",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Tag/Tag.md",
        "summary": "A mono outlined label for versions, SKUs and filters. `Tag` is static text on its own (with an optional remove button); inside a `TagGroup` it is an item with keyboard navigation, selection and removal. `TagGroup` is built on React Aria `TagGroup`, `TagList` and `Tag`.",
        "whenToUse": [
          "Show an attribute or identifier: `v2.4.0`, `ORD-1048`, `notebook`.",
          "Show applied filters that the user can remove (`TagGroup` with `onRemove`).",
          "Let the user toggle filters (`TagGroup` with `selectionMode`)."
        ],
        "whenNotToUse": [
          "For a status (Shipped, Failed): use `Badge`.",
          "For a command: use `Button`.",
          "For choosing one option from a list: use `Select` or `RadioGroup`."
        ],
        "keyboard": [],
        "examples": [
          "<Tag>v2.4.0</Tag>\n\n<TagGroup label=\"Product\" onRemove={(keys) => setTags((t) => t.filter((x) => !keys.has(x)))}>\n  {tags.map((tag) => <Tag key={tag} id={tag}>{tag}</Tag>)}\n</TagGroup>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Show an attribute or identifier: `v2.4.0`, `ORD-1048`, `notebook`.\n- Show applied filters that the user can remove (`TagGroup` with `onRemove`).\n- Let the user toggle filters (`TagGroup` with `selectionMode`)."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For a status (Shipped, Failed): use `Badge`.\n- For a command: use `Button`.\n- For choosing one option from a list: use `Select` or `RadioGroup`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTagGroup           (grid; optional visible label)\n  Label\n  TagList          (flex wrap)\n    Tag            (row)\n      text\n      remove button  only when the group has onRemove (24x24 target)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "`Tag`\n\n| Prop         | Type                | Notes                                                                         |\n| ------------ | ------------------- | ----------------------------------------------------------------------------- |\n| `children`   | `ReactNode`         | The text                                                                      |\n| `textValue`  | `string`            | Required when `children` is not a string: names the tag and its remove button |\n| `id`         | `Key`               | In a group: the key. Standalone: the DOM id                                   |\n| `isDisabled` | `boolean`           | Dashed border and reduced opacity; not removable or selectable                |\n| `onRemove`   | `() => void`        | Standalone only: shows a remove button. In a group use the group's `onRemove` |\n| `messages`   | `{ remove(label) }` | Per-instance override of the remove button name                               |\n\n`TagGroup`\n\n| Prop                                                                                        | Notes                                                            |\n| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| `label`                                                                                     | Visible label. Without it set `aria-label` or `aria-labelledby`  |\n| `onRemove(keys: Set<Key>)`                                                                  | Enables removal: Backspace, Delete and the remove button call it |\n| `selectionMode`, `selectedKeys`, `defaultSelectedKeys`, `onSelectionChange`, `disabledKeys` | React Aria selection API                                         |\n| `items`, `children` (function)                                                              | Dynamic collections, as in React Aria                            |\n| `renderEmptyState`                                                                          | Content shown when no tag is left (\"No filters applied\")         |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A group is a `grid` with one `row` per tag. Arrow keys move between tags (roving focus); the focused tag is the only one in the tab order.\n- **Backspace and Delete remove the focused tag** (or all selected tags) when the group has `onRemove`. Keyboard users are told so by a description that React Aria adds to the focused tag.\n- **The remove button is named \"Remove {text}\"** from the message catalog (pt-BR: \"Remover {text}\"), not just \"Remove\". The button is also a tab stop after its tag, and is a 24 by 24 pixel target (WCAG 2.5.8).\n- After a removal, focus moves to a neighbouring tag (React Aria); handle the empty state with `renderEmptyState` and move focus to something sensible yourself if the whole group disappears.\n- Selection is exposed as `aria-selected`, and shown with the accent border and tint (a permitted use of the accent). Selection is never signalled by color alone: the state is announced.\n- A standalone `Tag` is plain text. With `onRemove` it contains a real `<button>` (Enter or Space); Backspace and Delete do nothing there because the tag itself is not focusable. Use a `TagGroup` when keyboard removal matters.\n- Text is `--ds-text-2` on the page surface; disabled tags are exempt from contrast and carry `aria-disabled`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Short, lowercase or code-like values are fine (`v2.4.0`, `ORD-1048`); filters use the user's own words.\n- Write a `textValue` that reads well after \"Remove\": \"Remove notebook\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tag>v2.4.0</Tag>\n\n<TagGroup label=\"Product\" onRemove={(keys) => setTags((t) => t.filter((x) => !keys.has(x)))}>\n  {tags.map((tag) => <Tag key={tag} id={tag}>{tag}</Tag>)}\n</TagGroup>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The remove button name includes the tag text (it was a bare \"Remover\").\n- 24 px target for the remove button; height, padding and button size are tokens (`--ds-tag-*`).\n- `TagGroup` adds keyboard removal, roving focus, selection and an empty state.\n- The \"×\" glyph is the bundled `X` icon, hidden from assistive technology.\n- No `style` prop."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Badge",
            "reason": "For a status (Shipped, Failed): use `Badge`."
          },
          {
            "name": "Button",
            "reason": "For a command: use `Button`."
          },
          {
            "name": "Select",
            "reason": "For choosing one option from a list: use `Select` or `RadioGroup`."
          },
          {
            "name": "RadioGroup",
            "reason": "For choosing one option from a list: use `Select` or `RadioGroup`."
          }
        ],
        "uses": ["Icon"],
        "usedBy": ["TagInput"]
      }
    },
    {
      "name": "TagInput",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { TagInput } from 'loui-ds';",
      "folder": "src/components/TagInput",
      "sourceFile": "src/components/TagInput/TagInput.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A text field that turns what you type into removable tags: `Enter` or a comma adds the\ntag, pasting `a, b, c` adds three, `Backspace` on the empty input removes the last one.\nDuplicates are ignored with a polite message; `maxTags` and `validateTag` reject with an\nerror that is read aloud.\n\nBuilt from `TagGroup` / `Tag` and React Aria `TextField`. The tags are a keyboard-navigable\nlist (one tab stop; `Shift+Tab` from the input reaches it, `Backspace` or `Delete` on a tag\nremoves it), the input is a normal text box. The value is the list of tags; with `name` each\ntag is submitted as a hidden field.\n\nFor choosing from known options use `Combobox`; for filters that are always visible use\n`ToggleGroup`.",
      "props": [
        {
          "name": "value",
          "type": "readonly string[]",
          "required": false,
          "description": "The tags (controlled). Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "readonly string[]",
          "required": false,
          "description": "The initial tags (uncontrolled)."
        },
        {
          "name": "onChange",
          "type": "((tags: string[]) => void)",
          "required": false,
          "description": "Called with the full list of tags whenever a tag is added or removed."
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "Name used to submit the tags with a form. Each tag is submitted as its own hidden field with\nthis name (read them with `formData.getAll(name)`). The text being typed is never submitted."
        },
        {
          "name": "form",
          "type": "string",
          "required": false,
          "description": "The `id` of the `<form>` the hidden fields belong to, when the field is outside it."
        },
        {
          "name": "inputValue",
          "type": "string",
          "required": false,
          "description": "The text in the input, not yet a tag (controlled). Pair with `onInputChange`."
        },
        {
          "name": "defaultInputValue",
          "type": "string",
          "required": false,
          "description": "The initial text in the input (uncontrolled)."
        },
        {
          "name": "onInputChange",
          "type": "((text: string) => void)",
          "required": false,
          "description": "Called with the text of the input as the user types."
        },
        {
          "name": "delimiters",
          "type": "readonly string[]",
          "required": false,
          "default": "[',']",
          "description": "Characters that end a tag while typing and split pasted text. `Enter` always adds the tag,\nand line breaks always split pasted text. Default `[',']`. Pass `[]` to use only `Enter`."
        },
        {
          "name": "maxTags",
          "type": "number",
          "required": false,
          "description": "Maximum number of tags. Trying to add another shows an error and keeps the text."
        },
        {
          "name": "validateTag",
          "type": "((tag: string) => string | null)",
          "required": false,
          "description": "Decides whether a tag may be added. Return a message to reject it (\"Use letters and digits\nonly.\"); return nothing to accept it. The message is shown as the field error and read\naloud. It receives the trimmed text."
        },
        {
          "name": "caseSensitive",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Treat \"Notebook\" and \"notebook\" as different tags. Default: they are the same, and the second is ignored."
        },
        {
          "name": "addOnBlur",
          "type": "boolean",
          "required": false,
          "default": "true",
          "description": "Turns the text left in the input into a tag when the input loses focus. Default `true`."
        },
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text from your own validation, shown while the field is invalid (`isInvalid`). The\nmessages of `validateTag` and `maxTags` take its place while they apply."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field has no tags and no text. Never a substitute for the label."
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'",
          "description": "Control height: `sm` 32px, `md` 40px (default), `lg` 48px. The box grows when tags wrap.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "inputClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the text `<input>`."
        },
        {
          "name": "inputRef",
          "type": "Ref<HTMLInputElement>",
          "required": false,
          "description": "Ref to the text `<input>`. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ tagsLabel: (fieldLabel?: string) => string; added: (tag: string) => string; addedMany: (count: number) => string; removed: (tag: string) => string; duplicate: (tag: string) => string; duplicates: (count: number) => string; maxReached: (max: number) => string; } & { ...; }>",
          "required": false,
          "description": "Message overrides for this instance: `tagsLabel`, `added`, `addedMany`, `removed`,\n`duplicate`, `duplicates`, `maxReached`, `requiredMark`. The remove button of a tag uses the\n`Tag` messages."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTextFieldProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "aria-activedescendant",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-autocomplete",
              "type": "'none' | 'inline' | 'list' | 'both'",
              "required": false
            },
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"false\" | \"true\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "enterKeyHint",
              "type": "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TextFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            },
            {
              "name": "autoCorrect",
              "type": "string",
              "required": false
            },
            {
              "name": "spellCheck",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "size": ["sm", "md", "lg"]
      },
      "messages": {
        "providerKey": "TagInput",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "tagsLabel",
            "kind": "function",
            "default": "{fieldLabel?}, added tags",
            "signature": "(fieldLabel?: string)",
            "description": "Name of the list of added tags. `fieldLabel` is the text label of the field, when it has one."
          },
          {
            "key": "added",
            "kind": "function",
            "default": "Added “{tag}”.",
            "signature": "(tag: string)",
            "description": "Announced (polite) after one tag is added."
          },
          {
            "key": "addedMany",
            "kind": "function",
            "default": "Added 2 tags.",
            "signature": "(count: number)",
            "description": "Announced (polite) after several tags are added at once (paste)."
          },
          {
            "key": "removed",
            "kind": "function",
            "default": "Removed “{tag}”.",
            "signature": "(tag: string)",
            "description": "Announced (polite) after a tag is removed with Backspace on the empty input."
          },
          {
            "key": "duplicate",
            "kind": "function",
            "default": "“{tag}” is already added.",
            "signature": "(tag: string)",
            "description": "Shown and announced (polite) when a typed tag is already in the list and was ignored."
          },
          {
            "key": "duplicates",
            "kind": "function",
            "default": "2 duplicate tags were skipped.",
            "signature": "(count: number)",
            "description": "Shown and announced (polite) when several pasted tags were ignored as duplicates."
          },
          {
            "key": "maxReached",
            "kind": "function",
            "default": "You can add up to 2 tags.",
            "signature": "(max: number)",
            "description": "Error shown when the user tries to add a tag beyond the maximum."
          }
        ]
      },
      "cssVariables": [
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-fs-label",
        "--ds-lh-label",
        "--ds-tag-height",
        "--ds-tag-input-gap",
        "--ds-tag-input-input-min-width",
        "--ds-tag-input-padding-block",
        "--ds-tag-input-padding-inline",
        "--ds-text-2",
        "--ds-text-3"
      ],
      "stories": {
        "title": "Components/TagInput",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tag-input--default",
            "hasPlay": true
          },
          {
            "name": "AddWithKeyboard",
            "id": "components-tag-input--add-with-keyboard",
            "description": "Enter and the comma add a tag; the input is cleared each time.",
            "hasPlay": true
          },
          {
            "name": "WithTags",
            "id": "components-tag-input--with-tags",
            "hasPlay": true
          },
          {
            "name": "BackspaceRemovesLast",
            "id": "components-tag-input--backspace-removes-last",
            "description": "`Backspace` on the empty input removes the last tag.",
            "hasPlay": true
          },
          {
            "name": "RemoveWithKeyboard",
            "id": "components-tag-input--remove-with-keyboard",
            "description": "The input is a tab stop of its own; `Shift+Tab` reaches the tags, where `Delete` removes one.",
            "hasPlay": true
          },
          {
            "name": "Duplicate",
            "id": "components-tag-input--duplicate",
            "description": "A duplicate is ignored (case does not matter) and a polite message says so.",
            "hasPlay": true
          },
          {
            "name": "MaxTags",
            "id": "components-tag-input--max-tags",
            "hasPlay": true
          },
          {
            "name": "Validation",
            "id": "components-tag-input--validation",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-tag-input--invalid",
            "hasPlay": false
          },
          {
            "name": "Required",
            "id": "components-tag-input--required",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-tag-input--disabled",
            "hasPlay": true
          },
          {
            "name": "ReadOnly",
            "id": "components-tag-input--read-only",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "components-tag-input--small",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-tag-input--large",
            "hasPlay": false
          },
          {
            "name": "ManyTagsWrap",
            "id": "components-tag-input--many-tags-wrap",
            "hasPlay": false
          },
          {
            "name": "LongTag",
            "id": "components-tag-input--long-tag",
            "hasPlay": false
          },
          {
            "name": "SemicolonDelimiter",
            "id": "components-tag-input--semicolon-delimiter",
            "hasPlay": false
          },
          {
            "name": "Controlled",
            "id": "components-tag-input--controlled",
            "hasPlay": true
          },
          {
            "name": "InForm",
            "id": "components-tag-input--in-form",
            "hasPlay": true
          },
          {
            "name": "Portuguese",
            "id": "components-tag-input--portuguese",
            "hasPlay": true
          },
          {
            "name": "RightToLeft",
            "id": "components-tag-input--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-tag-input--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/TagInput/TagInput.md",
        "summary": "A text field that turns what you type into removable tags: `Enter` or a comma adds a tag, pasting `a, b, c` adds three, `Backspace` on the empty input removes the last one. Built from `TagGroup` / `Tag` and React Aria `TextField`. The value is the list of tags; with `name` each tag is submitted as a hidden form field.\n\n- TODO `GAP-005`.",
        "whenToUse": [
          "Free-form lists the user types: keywords, e-mail recipients, labels, SKUs.",
          "When the list can grow beyond what you want to show as checkboxes."
        ],
        "whenNotToUse": [
          "Choosing from known options: use `Combobox` (or `Select`).",
          "A fixed set of filters that are always visible: use `ToggleGroup`.",
          "Showing tags the user cannot edit: use `TagGroup`."
        ],
        "keyboard": [
          "The input is a normal `textbox` named by the label. The tags are a `grid` named \"Products, added tags\" with one `row` per tag (React Aria `TagGroup`): `Shift+Tab` from the input reaches the tags; a row, then its remove button, are tab stops; `←` / `→` move between tags; `Backspace` or `Delete` on a focused tag removes it. After the last tag is removed focus returns to the input."
        ],
        "examples": [
          "<TagInput label=\"Products\" name=\"products\" value={products} onChange={setProducts} maxTags={10} />\n\n<TagInput\n  label=\"Recipients\"\n  delimiters={[',', ';']}\n  validateTag={(tag) => (tag.includes('@') ? undefined : 'Enter a complete email address.')}\n/>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Free-form lists the user types: keywords, e-mail recipients, labels, SKUs.\n- When the list can grow beyond what you want to show as checkboxes."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Choosing from known options: use `Combobox` (or `Select`).\n- A fixed set of filters that are always visible: use `ToggleGroup`.\n- Showing tags the user cannot edit: use `TagGroup`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (field root)\n  Label\n  div.control                     (the bordered box, wraps over several rows)\n    TagGroup[role=grid] aria-label    (only while there are tags; one row per tag)\n      Tag > text + remove button (24x24)\n    input[type=text]              (the textbox; named by the Label)\n  Description\n  FieldError                      (role=alert: validation and maximum)\n  div[role=status]                (polite: added, removed, duplicate; visible for a duplicate)\n  input[type=hidden] x N          (only with `name`)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                                     | Type                           | Default      | Notes                                                                                                |\n| -------------------------------------------------------- | ------------------------------ | ------------ | ---------------------------------------------------------------------------------------------------- |\n| `label` / `aria-label` / `aria-labelledby`               | `ReactNode` / `string`         | one required | Names the field. Enforced by the types.                                                              |\n| `value` / `defaultValue`                                 | `readonly string[]`            | `[]`         | Controlled / uncontrolled tags.                                                                      |\n| `onChange`                                               | `(tags: string[]) => void`     | none         | Called with the full list after each addition or removal.                                            |\n| `inputValue` / `defaultInputValue` / `onInputChange`     | `string`                       | `''`         | The text being typed, when you need to own it.                                                       |\n| `name` / `form`                                          | `string`                       | none         | Each tag is submitted as its own field (`formData.getAll(name)`). The draft text is never submitted. |\n| `delimiters`                                             | `readonly string[]`            | `[',']`      | End a tag while typing and split pasted text. `Enter` always adds; line breaks always split a paste. |\n| `maxTags`                                                | `number`                       | none         | Error \"You can add up to N tags.\" when exceeded; the text is kept.                                   |\n| `validateTag`                                            | `(tag) => string \\| undefined` | none         | Return a message to reject a tag. Shown as the field error and read aloud.                           |\n| `caseSensitive`                                          | `boolean`                      | `false`      | By default `Notebook` and `notebook` are the same tag.                                               |\n| `addOnBlur`                                              | `boolean`                      | `true`       | Turns the text left in the input into a tag on blur.                                                 |\n| `description` / `errorMessage`                           | `ReactNode`                    | none         | Helper text; your own error while `isInvalid`.                                                       |\n| `placeholder`                                            | `string`                       | none         | Shown only while there are no tags.                                                                  |\n| `size`                                                   | `'sm' \\| 'md' \\| 'lg'`         | `'md'`       | Minimum height 32 / 40 / 48px; the box grows when tags wrap.                                         |\n| `isDisabled` / `isReadOnly` / `isRequired` / `isInvalid` | `boolean`                      | `false`      | `isRequired` is satisfied by one tag. Read-only and disabled fields show no remove buttons.          |\n| `messages`                                               | see below                      | none         | Per-instance message overrides.                                                                      |"
          },
          {
            "id": "behavior",
            "heading": "Behavior",
            "content": "| Input                                        | Result                                                                                                          |\n| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |\n| `Enter` with text                            | Adds the tag (trimmed), clears the input. Does not submit the form. `Enter` on an empty input submits as usual. |\n| A delimiter key (`,`)                        | Same as `Enter`; the character is never typed.                                                                  |\n| Paste with delimiters or line breaks         | Adds every piece. Duplicates are skipped; a message counts them. A paste without delimiters is a normal paste.  |\n| `Backspace` on an empty input                | Removes the last tag (announced: \"Removed “headphones”.\").                                                      |\n| Duplicate                                    | Ignored, input cleared, polite message \"“notebook” is already added.\" Not an error.                             |\n| Beyond `maxTags` / rejected by `validateTag` | Error (`role=\"alert\"`, field invalid), nothing is added, the text stays so it can be fixed.                     |\n| Blur with text                               | Adds it (`addOnBlur`).                                                                                          |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The input is a normal `textbox` named by the label. The tags are a `grid` named \"Products, added tags\" with one `row` per tag (React Aria `TagGroup`): `Shift+Tab` from the input reaches the tags; a row, then its remove button, are tab stops; `←` / `→` move between tags; `Backspace` or `Delete` on a focused tag removes it. After the last tag is removed focus returns to the input.\n- Remove buttons are 24x24 and named \"Remove notebook\" (message `Tag.remove`).\n- Additions, removals and duplicates are announced in one polite `role=\"status\"` region; errors are in the field's `role=\"alert\"` message, linked with `aria-describedby`. Only duplicates are also shown as text below the box, so the notice is not color-only; the error is text and the box border turns danger.\n- The click target of the whole box focuses the input.\n- Contrast: the box border is `--ds-line-control` (3:1) in every state, as in the other fields."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Label as a noun phrase (\"Products\", \"Recipients\"). Placeholder: an example or an action (\"Add a product\"), never the label.\n- `validateTag` messages say how to fix it: \"Enter a complete email address.\"\n- Tags are shown as typed (trimmed); sorting is up to you."
          },
          {
            "id": "messages-messages-prop-or-dsprovider-messages-taginput",
            "heading": "Messages (`messages` prop, or `DSProvider messages={{ TagInput: … }}`)",
            "content": "`tagsLabel(fieldLabel?)`, `added(tag)`, `addedMany(count)`, `removed(tag)`, `duplicate(tag)`, `duplicates(count)`, `maxReached(max)`, `requiredMark`. English and `pt-BR` ship with the library. The remove button uses the `Tag` messages."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<TagInput label=\"Products\" name=\"products\" value={products} onChange={setProducts} maxTags={10} />\n\n<TagInput\n  label=\"Recipients\"\n  delimiters={[',', ';']}\n  validateTag={(tag) => (tag.includes('@') ? undefined : 'Enter a complete email address.')}\n/>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Combobox",
            "reason": "Choosing from known options: use `Combobox` (or `Select`)."
          },
          {
            "name": "Select",
            "reason": "Choosing from known options: use `Combobox` (or `Select`)."
          },
          {
            "name": "ToggleGroup",
            "reason": "A fixed set of filters that are always visible: use `ToggleGroup`."
          },
          {
            "name": "TagGroup",
            "reason": "Showing tags the user cannot edit: use `TagGroup`."
          }
        ],
        "uses": ["FieldMessages", "Label", "Tag", "TagGroup"],
        "usedBy": []
      }
    },
    {
      "name": "Text",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Text } from 'loui-ds';",
      "folder": "src/components/Text",
      "sourceFile": "src/components/Text/Text.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Body text on the type scale. It sets size, line height, color tone and an optional mono\nface; the element (`as`) carries the meaning. Use `Heading` for headings and `Code` for code.",
      "props": [
        {
          "name": "size",
          "type": "'body' | 'caption' | 'label' | 'meta' | 'small'",
          "required": false,
          "default": "'body'",
          "description": "Type scale step: `body` 15px, `small` 14px, `label` 13px, `meta` 12px, or `caption` 11.5px.\n`caption` is always Geist Mono, uppercase by CSS, with extra tracking: write the source text\nin sentence case. Default `body`.",
          "values": ["body", "caption", "label", "meta", "small"]
        },
        {
          "name": "tone",
          "type": "'default' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger'",
          "required": false,
          "default": "'default'",
          "description": "Text color: `default` (`--ds-text`), `secondary` (`--ds-text-2`, supporting text) or\n`tertiary` (`--ds-text-3`, hints). Tertiary passes 4.5:1 on every surface, but signals\nthe lowest importance: use it for meta and hints, never for primary content. `success`,\n`warning` and `danger` are signal colours (4.5:1 on the base surfaces): always pair them with\nwords that carry the meaning, never colour alone. Default `default`.",
          "values": ["default", "secondary", "tertiary", "success", "warning", "danger"]
        },
        {
          "name": "mono",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Geist Mono with tabular figures, for codes, identifiers, amounts and shortcuts."
        },
        {
          "name": "truncate",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Keeps the text on one line and cuts it with an ellipsis. Put the full text in `title` or nearby for sighted users."
        },
        {
          "name": "as",
          "type": "'span' | 'dd' | 'div' | 'dt' | 'em' | 'figcaption' | 'label' | 'legend' | 'li' | 'p' | 'small' | 'strong'",
          "required": false,
          "default": "'span'",
          "description": "Element to render. Default `span`, which is safe inside any inline context; use `p` for a paragraph.",
          "values": [
            "span",
            "dd",
            "div",
            "dt",
            "em",
            "figcaption",
            "label",
            "legend",
            "li",
            "p",
            "small",
            "strong"
          ]
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {
        "size": ["body", "caption", "label", "meta", "small"],
        "tone": ["default", "secondary", "tertiary", "success", "warning", "danger"]
      },
      "cssVariables": [
        "--ds-danger",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-body",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tracking-caption",
        "--ds-warning",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Typography/Text",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "typography-text--default",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "typography-text--small",
            "hasPlay": false
          },
          {
            "name": "Label",
            "id": "typography-text--label",
            "hasPlay": false
          },
          {
            "name": "Meta_",
            "id": "typography-text--meta",
            "hasPlay": false
          },
          {
            "name": "Caption",
            "id": "typography-text--caption",
            "description": "Mono, uppercase by CSS and tracked: the source text stays in sentence case.",
            "hasPlay": false
          },
          {
            "name": "Secondary",
            "id": "typography-text--secondary",
            "hasPlay": false
          },
          {
            "name": "Tertiary",
            "id": "typography-text--tertiary",
            "description": "Tertiary text is for hints on the base surfaces. It is too faint on raised and hover surfaces.",
            "hasPlay": false
          },
          {
            "name": "Success",
            "id": "typography-text--success",
            "description": "Signal tones always pair with words that carry the meaning.",
            "hasPlay": false
          },
          {
            "name": "Warning",
            "id": "typography-text--warning",
            "hasPlay": false
          },
          {
            "name": "Danger",
            "id": "typography-text--danger",
            "hasPlay": false
          },
          {
            "name": "Mono",
            "id": "typography-text--mono",
            "hasPlay": false
          },
          {
            "name": "AsParagraph",
            "id": "typography-text--as-paragraph",
            "hasPlay": true
          },
          {
            "name": "Truncated",
            "id": "typography-text--truncated",
            "description": "One line, cut with an ellipsis.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "typography-text--long-text",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "typography-text--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Text/Text.md",
        "summary": "Body text on the type scale, with a tone, an optional mono face and truncation.",
        "whenToUse": [
          "Paragraphs, supporting lines, labels, counts, metadata and small captions.",
          "Anywhere a size from the type scale or a secondary tone is needed on plain text."
        ],
        "whenNotToUse": [
          "Headings: use `Heading`.",
          "Code: use `Code`.",
          "Links: use `Link`. Field labels: use the `Label` of the field."
        ],
        "keyboard": [],
        "examples": [
          "<Text as=\"p\">Your order has shipped.</Text>\n<Text size=\"small\" tone=\"secondary\">Updated 2 minutes ago</Text>\n<Text size=\"caption\" tone=\"secondary\">Shipping</Text>\n<Text mono>ORD-1048</Text>\n<Text truncate>{longFileName}</Text>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Paragraphs, supporting lines, labels, counts, metadata and small captions.\n- Anywhere a size from the type scale or a secondary tone is needed on plain text."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Headings: use `Heading`.\n- Code: use `Code`.\n- Links: use `Link`. Field labels: use the `Label` of the field."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan (or the element in `as`)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop       | Type                                                                           | Default     | Notes                                                                                    |\n| ---------- | ------------------------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------- |\n| `size`     | `'body' \\| 'small' \\| 'label' \\| 'meta' \\| 'caption'`                          | `'body'`    | 15/24 · 14/22 · 13/18 · 12/16 · 11.5/16 px. `caption` is mono, uppercase, tracked        |\n| `tone`     | `'default' \\| 'secondary' \\| 'tertiary' \\| 'success' \\| 'warning' \\| 'danger'` | `'default'` | `--ds-text`, `--ds-text-2`, `--ds-text-3`, `--ds-success`, `--ds-warning`, `--ds-danger` |\n| `mono`     | `boolean`                                                                      | `false`     | Geist Mono with tabular figures, for codes, amounts and shortcuts                        |\n| `truncate` | `boolean`                                                                      | `false`     | One line with an ellipsis. Becomes a block                                               |\n| `as`       | `'span' \\| 'p' \\| 'div' \\| 'label' \\| 'strong' \\| 'em' …`                      | `'span'`    | Pick the element for the meaning                                                         |\n\nOther attributes and `ref` go to the root."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- `tertiary` passes 4.5:1 on every surface (TOK-004), but it carries the lowest importance: use it for hints and meta, never for primary content.\n- `success`, `warning` and `danger` pass 4.5:1 on bg, surface and raised. The colour only reinforces: the words must carry the meaning (\"Select at least one color.\"), never colour alone. For an error tied to a group of controls use `ErrorText`.\n- Truncated text hides content visually: show the full value elsewhere (a `title`, a tooltip, a detail view) when it matters.\n- The caption's uppercase is CSS only, so screen readers read the sentence-case source.\n- Long unbroken values wrap (`overflow-wrap: break-word`)."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Sentence case. Numbers, codes and amounts go in `mono`, formatted with `Intl` (see `utils`)."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Text as=\"p\">Your order has shipped.</Text>\n<Text size=\"small\" tone=\"secondary\">Updated 2 minutes ago</Text>\n<Text size=\"caption\" tone=\"secondary\">Shipping</Text>\n<Text mono>ORD-1048</Text>\n<Text truncate>{longFileName}</Text>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- The handoff repeated font sizes and colors per element in inline styles. Text maps them to the type and text tokens, with `caption` defined once (mono, uppercase, +8% tracking)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Heading",
            "reason": "Headings: use `Heading`."
          },
          {
            "name": "Code",
            "reason": "Code: use `Code`."
          },
          {
            "name": "Link",
            "reason": "Links: use `Link`. Field labels: use the `Label` of the field."
          },
          {
            "name": "Label",
            "reason": "Links: use `Link`. Field labels: use the `Label` of the field."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "Textarea",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Textarea } from 'loui-ds';",
      "folder": "src/components/Textarea",
      "sourceFile": "src/components/Textarea/Textarea.tsx",
      "status": "experimental",
      "statusSource": "default",
      "description": "Multi-line text field built on React Aria `TextField` + `TextArea`. Same contract as `Input`.",
      "props": [
        {
          "name": "description",
          "type": "ReactNode",
          "required": false,
          "description": "Helper text under the control. Linked with `aria-describedby`."
        },
        {
          "name": "descriptionTone",
          "type": "'neutral' | 'accent' | 'success' | 'warning' | 'danger'",
          "required": false,
          "description": "Tone of the description: `success`, `warning`, `danger` or `accent` for a live check such as\n\"Matches\" or \"Over by $5\". The text carries the meaning; colour only reinforces it.",
          "values": ["neutral", "accent", "success", "warning", "danger"]
        },
        {
          "name": "errorMessage",
          "type": "FieldErrorMessage",
          "required": false,
          "description": "Error text, shown while the field is invalid and the error is displayed. See `Input`."
        },
        {
          "name": "aside",
          "type": "ReactNode",
          "required": false,
          "description": "Text on the label line, aligned to the end (for example \"Optional\" or \"Markdown supported\").\nIt is part of the field's description for assistive technology, not of its name."
        },
        {
          "name": "placeholder",
          "type": "string",
          "required": false,
          "description": "Text shown while the field is empty. Never a substitute for the label."
        },
        {
          "name": "rows",
          "type": "number",
          "required": false,
          "default": "3",
          "description": "Visible lines before the user types. Default 3."
        },
        {
          "name": "resize",
          "type": "'none' | 'vertical' | 'horizontal' | 'both'",
          "required": false,
          "default": "'vertical'",
          "description": "Whether the user can drag the corner to resize. Default `vertical`. Ignored with `autoGrow`.",
          "values": ["none", "vertical", "horizontal", "both"]
        },
        {
          "name": "autoGrow",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Grows with its content instead of scrolling."
        },
        {
          "name": "showCount",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Shows \"used / max\" on the label line and announces the remaining characters politely once\ntyping pauses. Needs `maxLength`."
        },
        {
          "name": "fullWidth",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Take the free space of a flex row (an `Inline`): the field grows to fill it and can shrink\nbelow its content width. Off by default. In a block context the field already spans the width."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the field root."
        },
        {
          "name": "textareaClassName",
          "type": "string",
          "required": false,
          "description": "Class name for the `<textarea>` element."
        },
        {
          "name": "textareaRef",
          "type": "Ref<HTMLTextAreaElement>",
          "required": false,
          "description": "Ref to the `<textarea>` element. The `ref` prop points at the field root."
        },
        {
          "name": "messages",
          "type": "Partial<{ count: (used: string, max: string) => string; remaining: (n: number) => string; over: (n: number) => string; } & { requiredMark: string; }>",
          "required": false,
          "description": "Message overrides for this instance: `count`, `remaining`, `over`, `requiredMark`."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the field root `<div>`."
        },
        {
          "name": "label",
          "type": "ReactNode",
          "required": false,
          "description": "Visible label. Prefer it: sighted and assistive technology users see the same name."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name when there is no visible label (for example a search box with an icon)."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the field."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "AriaTextFieldProps",
          "domGeneric": false,
          "count": 5,
          "props": [
            {
              "name": "aria-activedescendant",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-autocomplete",
              "type": "'none' | 'both' | 'inline' | 'list'",
              "required": false
            },
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"false\" | \"true\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "enterKeyHint",
              "type": "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaValidationProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "aria-errormessage",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "DOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "id",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "DOMRenderProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "render",
              "type": "DOMRenderFunction<\"div\", TextFieldRenderProps>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<HTMLInputElement, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputBase",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isReadOnly",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "InputDOMProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "form",
              "type": "string",
              "required": false
            },
            {
              "name": "name",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "RACValidation",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "validationBehavior",
              "type": "'native' | 'aria'",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "TextFieldProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isInvalid",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMEvents",
          "domGeneric": false,
          "count": 9,
          "props": [
            {
              "name": "onBeforeInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionEnd",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionStart",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCompositionUpdate",
              "type": "CompositionEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCopy",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onCut",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onInput",
              "type": "FormEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onPaste",
              "type": "ClipboardEventHandler<HTMLInputElement>",
              "required": false
            },
            {
              "name": "onSelect",
              "type": "ReactEventHandler<HTMLInputElement>",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "TextInputDOMProps",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "autoComplete",
              "type": "string",
              "required": false
            },
            {
              "name": "autoCorrect",
              "type": "string",
              "required": false
            },
            {
              "name": "maxLength",
              "type": "number",
              "required": false
            },
            {
              "name": "minLength",
              "type": "number",
              "required": false
            },
            {
              "name": "pattern",
              "type": "string",
              "required": false
            },
            {
              "name": "spellCheck",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "Validation",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "isRequired",
              "type": "boolean",
              "required": false
            },
            {
              "name": "validate",
              "type": "(value: string) => true | ValidationError | null",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "ValueBase",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultValue",
              "type": "string",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(value: string) => void",
              "required": false
            },
            {
              "name": "value",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Textarea",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "count",
            "kind": "function",
            "default": "{used} / {max}",
            "signature": "(used: string, max: string)",
            "description": "Visible counter next to the label, for example \"12 / 200\". Arguments are already formatted."
          },
          {
            "key": "remaining",
            "kind": "function",
            "default": "2 characters remaining",
            "signature": "(n: number)",
            "description": "Polite announcement, made once typing pauses."
          },
          {
            "key": "over",
            "kind": "function",
            "default": "2 characters over the limit",
            "signature": "(n: number)",
            "description": "Announcement when a controlled value exceeds `maxLength`."
          }
        ]
      },
      "cssVariables": [
        "--ds-danger",
        "--ds-font-mono",
        "--ds-fs-meta",
        "--ds-lh-meta",
        "--ds-space-2",
        "--ds-text-3",
        "--ds-textarea-padding-block",
        "--ds-textarea-padding-inline"
      ],
      "stories": {
        "title": "Components/Textarea",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-textarea--default",
            "hasPlay": true
          },
          {
            "name": "WithDescription",
            "id": "components-textarea--with-description",
            "hasPlay": false
          },
          {
            "name": "WithAside",
            "id": "components-textarea--with-aside",
            "hasPlay": true
          },
          {
            "name": "Required",
            "id": "components-textarea--required",
            "hasPlay": true
          },
          {
            "name": "Invalid",
            "id": "components-textarea--invalid",
            "hasPlay": true
          },
          {
            "name": "Disabled",
            "id": "components-textarea--disabled",
            "hasPlay": false
          },
          {
            "name": "ReadOnly",
            "id": "components-textarea--read-only",
            "hasPlay": true
          },
          {
            "name": "AutoGrow",
            "id": "components-textarea--auto-grow",
            "hasPlay": true
          },
          {
            "name": "WithCharacterCount",
            "id": "components-textarea--with-character-count",
            "hasPlay": true
          },
          {
            "name": "NoResize",
            "id": "components-textarea--no-resize",
            "hasPlay": false
          },
          {
            "name": "LongLabel",
            "id": "components-textarea--long-label",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-textarea--focused",
            "hasPlay": true
          },
          {
            "name": "FullWidthInRow",
            "id": "components-textarea--full-width-in-row",
            "description": "In an `Inline` row the field takes the free space next to the button.",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Textarea/Textarea.md",
        "summary": "Multi-line text field. Same contract as [Input](../Input/Input.md) (label, description, error, states, validation timing from [Field](../Field/Field.md)), plus an aside, auto-growing height and a character count. Built on React Aria `TextField` + `TextArea`.",
        "whenToUse": ["Notes, comments, descriptions, messages longer than a line."],
        "whenNotToUse": [
          "One line of text: `Input`.",
          "Rich text or Markdown editing: a dedicated editor."
        ],
        "keyboard": [
          "Keyboard: standard text editing; `Enter` inserts a newline; `Tab` leaves the field."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Notes, comments, descriptions, messages longer than a line."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- One line of text: `Input`.\n- Rich text or Markdown editing: a dedicated editor."
          },
          {
            "id": "props-key-ones",
            "heading": "Props (key ones)",
            "content": "| Prop                                                      | Notes                                                                                                                    |\n| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `label`, `description`, `descriptionTone`, `errorMessage` | As in `Input`.                                                                                                           |\n| `aside`                                                   | Text on the label line, aligned to the end (\"Optional\", \"Markdown supported\"). Part of the description, not of the name. |\n| `rows`                                                    | Visible lines. Default 3.                                                                                                |\n| `resize`                                                  | `none`, `vertical` (default), `horizontal`, `both`.                                                                      |\n| `autoGrow`                                                | Grows with its content; the manual resize handle is off.                                                                 |\n| `fullWidth`                                               | Take the free space of a flex row such as `Inline` (`flex: 1 1 auto`, can shrink). Off by default.                       |\n| `maxLength` + `showCount`                                 | Shows \"used / max\" on the label line and announces the remaining characters politely.                                    |\n| `value` / `defaultValue` / `onChange(value)`              | React Aria naming; `onChange` receives the string.                                                                       |\n| `textareaRef` / `ref`                                     | `textareaRef` is the `<textarea>`, `ref` the field root.                                                                 |\n| `messages`                                                | `count`, `remaining`, `over`, `requiredMark`.                                                                            |"
          },
          {
            "id": "character-count",
            "heading": "Character count",
            "content": "- The visible counter (\"12 / 200\", formatted for the locale) is `aria-hidden`: announcing it on every keystroke would drown the user.\n- A separate visually hidden `role=\"status\"` (polite) region announces \"N characters remaining\" once typing has paused for one second. It stays silent until the value changes, and says \"N characters over the limit\" when a controlled value exceeds `maxLength`.\n- Plural forms and the pt-BR text come from `Textarea.messages.ts`. Override `count`, `remaining` and `over` per instance or via `DSProvider messages={{ Textarea: { ... } }}`.\n- `showCount` has no effect without `maxLength`. The native `maxLength` stops typing at the limit."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Native `<textarea>` named by its label; `aside` and `description` are in `aria-describedby` (description first, then aside).\n- Keyboard: standard text editing; `Enter` inserts a newline; `Tab` leaves the field.\n- Auto-grow keeps all content visible (no inner scrollbar), which helps zoom and reflow."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<Textarea label=\"Notes\" aside=\"Optional\" description=\"Plain text only.\" maxLength={500} showCount />\n<Textarea label=\"Message\" autoGrow rows={2} isRequired errorMessage=\"Write a message.\" />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- `aside` is associated with the field; it was a floating span.\n- New: `autoGrow`, character count with a debounced polite announcement, read-only, shared validation timing.\n- Focus styling is CSS and survives consumer `onFocus`/`onBlur`.\n- `resize` accepts four values and is ignored with `autoGrow`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Input",
            "reason": "One line of text: `Input`."
          }
        ],
        "uses": ["FieldMessages", "Label", "VisuallyHidden"],
        "usedBy": ["ApprovalCard", "ChatPanel"]
      }
    },
    {
      "name": "Timeline",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Timeline } from 'loui-ds';",
      "folder": "src/components/Timeline",
      "sourceFile": "src/components/Timeline/Timeline.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A chronological list of events. It is an ordered list; each event has a `<time>`, a title, an\noptional description, an optional actor and an optional tone marker. The marker and the line\nare decorative: a tone is also stated as hidden text before the title.\nNewest events come first by default, the order for dated records.",
      "props": [
        {
          "name": "items",
          "type": "readonly TimelineItem[]",
          "required": true,
          "description": "The events."
        },
        {
          "name": "order",
          "type": "'newest-first' | 'oldest-first' | 'as-given'",
          "required": false,
          "default": "'newest-first'",
          "description": "Order of the list: `newest-first` (default, for dated records), `oldest-first`, or\n`as-given` to keep the order of `items`.",
          "values": ["newest-first", "oldest-first", "as-given"]
        },
        {
          "name": "timeZone",
          "type": "string",
          "required": false,
          "description": "IANA time zone used to show date-times (\"America/Sao_Paulo\"). Defaults to the viewer's zone."
        },
        {
          "name": "empty",
          "type": "ReactNode",
          "required": false,
          "description": "Rendered instead of the list when `items` is empty (for example an `EmptyState`)."
        },
        {
          "name": "messages",
          "type": "Partial<{ accent: string; success: string; warning: string; danger: string; actor: (actor: string) => string; }>",
          "required": false,
          "description": "Message overrides for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "OlHTMLAttributes",
          "domGeneric": true,
          "count": 3
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "Timeline",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "accent",
            "kind": "string",
            "default": "Highlighted",
            "description": "Visually hidden prefix read before the title of an event with a tone; color and icon alone cannot carry it."
          },
          {
            "key": "success",
            "kind": "string",
            "default": "Success"
          },
          {
            "key": "warning",
            "kind": "string",
            "default": "Warning"
          },
          {
            "key": "danger",
            "kind": "string",
            "default": "Error"
          },
          {
            "key": "actor",
            "kind": "function",
            "default": "By {actor}",
            "signature": "(actor: string)",
            "description": "Visible label before the person or system that caused the event."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-bg",
        "--ds-danger",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-fs-small",
        "--ds-lh-label",
        "--ds-lh-meta",
        "--ds-lh-small",
        "--ds-line-control",
        "--ds-line-strong",
        "--ds-radius-pill",
        "--ds-space-1",
        "--ds-space-3",
        "--ds-success",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-timeline-connector-thickness",
        "--ds-timeline-dot-size",
        "--ds-timeline-item-gap",
        "--ds-timeline-marker-gap",
        "--ds-timeline-marker-size",
        "--ds-warning",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Timeline",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-timeline--default",
            "description": "Newest first, the default order for dated records.",
            "hasPlay": false
          },
          {
            "name": "OldestFirst",
            "id": "components-timeline--oldest-first",
            "hasPlay": false
          },
          {
            "name": "Tones",
            "id": "components-timeline--tones",
            "hasPlay": false
          },
          {
            "name": "DateOnly",
            "id": "components-timeline--date-only",
            "description": "A date without a time is shown as a date.",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-timeline--long-text",
            "hasPlay": false
          },
          {
            "name": "SingleEvent",
            "id": "components-timeline--single-event",
            "hasPlay": false
          },
          {
            "name": "Empty",
            "id": "components-timeline--empty",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-timeline--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/Timeline/Timeline.md",
        "summary": "A chronological list of events: when it happened, what happened, who did it, and an optional detail and tone marker. It is a semantic ordered list, newest event first by default.",
        "whenToUse": [
          "The history of a record or a process: status changes, comments, deliveries, audit entries.",
          "Events that have a time and read as a story, a few lines each."
        ],
        "whenNotToUse": [
          "Tabular data that people sort and compare: use `DataTable`.",
          "The steps of a flow someone is going through now: use `Stepper`.",
          "The state of a record in a fixed flow: use `StatusSteps`.",
          "Totals and amounts per line: use `LedgerList`."
        ],
        "keyboard": [],
        "examples": [
          "<Timeline\n  aria-label=\"Request history\"\n  items={[\n    { id: 'a', time: '2026-10-28T09:15:00Z', title: 'Request created', actor: 'Ana Costa' },\n    { id: 'b', time: '2026-10-31T16:05:00Z', title: 'Request approved', tone: 'success' },\n  ]}\n/>\n\n<Timeline aria-label=\"Audit log\" items={events} order=\"oldest-first\" timeZone=\"UTC\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- The history of a record or a process: status changes, comments, deliveries, audit entries.\n- Events that have a time and read as a story, a few lines each."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Tabular data that people sort and compare: use `DataTable`.\n- The steps of a flow someone is going through now: use `Stepper`.\n- The state of a record in a fixed flow: use `StatusSteps`.\n- Totals and amounts per line: use `LedgerList`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nol[aria-label]\n  li[data-tone]\n    marker (aria-hidden)         dot (neutral) | icon: accent info, success check, warning triangle, danger circle\n    div\n      title                      VisuallyHidden tone text (\"Success\") + title\n      meta                       \"By <actor>\" · <time dateTime>\n      description                optional\n  connector line                 (CSS pseudo-element, decorative)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop       | Type                                             | Default          | Notes                                                                                  |\n| ---------- | ------------------------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `items`    | `TimelineItem[]`                                 |                  | `{ id, time, title, description?, tone?, actor? }`                                     |\n| `order`    | `'newest-first' \\| 'oldest-first' \\| 'as-given'` | `'newest-first'` | Dated records are newest first (AGENTS.md section 5, rule 5b). Does not mutate `items` |\n| `timeZone` | `string`                                         | viewer's zone    | IANA zone for date-times                                                               |\n| `empty`    | `ReactNode`                                      |                  | Rendered instead of the list when `items` is empty                                     |\n| `messages` | `{ accent, success, warning, danger, actor }`    |                  | Per-instance overrides                                                                 |\n\n`time` is an ISO 8601 date-time, a `Date`, or a date-only `YYYY-MM-DD` (shown as a date without a time, in UTC so the day never shifts). Other `ol` attributes (`aria-label`, `id`) and `ref` go to the list; name it with `aria-label` or `aria-labelledby` when the heading does not."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A real ordered list; each time is a `<time dateTime>` with a machine-readable value, and its text comes from `Intl` for the active locale (never hand-built).\n- The tone is not only color and icon: a visually hidden prefix (\"Success\", \"Warning\", \"Error\", \"Highlighted\", from messages) comes before the title. The marker and the line are `aria-hidden`.\n- The list is not interactive, so it has no keyboard behavior of its own; put links or buttons in the `title` or `description` when an event needs an action.\n- Text uses `text` and `text-2` (4.5:1 on the page surface). Forced colors: markers and line use system colors.\n- Reflow: text wraps (long words break) and the layout is a single column, so it works at 320 px."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: what happened, past tense, sentence case, no exclamation marks (\"Order shipped\", \"Request approved\").\n- Description: one short sentence of detail, only when it adds something.\n- Actor: a person or system name. The component adds \"By\".\n- Do not repeat the time in the title."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Timeline\n  aria-label=\"Request history\"\n  items={[\n    { id: 'a', time: '2026-10-28T09:15:00Z', title: 'Request created', actor: 'Ana Costa' },\n    { id: 'b', time: '2026-10-31T16:05:00Z', title: 'Request approved', tone: 'success' },\n  ]}\n/>\n\n<Timeline aria-label=\"Audit log\" items={events} order=\"oldest-first\" timeZone=\"UTC\" />\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "DataTable",
            "reason": "Tabular data that people sort and compare: use `DataTable`."
          },
          {
            "name": "Stepper",
            "reason": "The steps of a flow someone is going through now: use `Stepper`."
          },
          {
            "name": "StatusSteps",
            "reason": "The state of a record in a fixed flow: use `StatusSteps`."
          },
          {
            "name": "LedgerList",
            "reason": "Totals and amounts per line: use `LedgerList`."
          }
        ],
        "uses": ["Icon", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "ToastRegion",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ToastRegion } from 'loui-ds';",
      "folder": "src/components/Toast",
      "sourceFile": "src/components/Toast/Toast.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "Renders the toasts of a queue. Mount **one** near the app root, inside `DSProvider`, and call\n`toast()` from anywhere. Built on React Aria's toast region (isolated in `toast.rac.ts`).\n\nThe region is a landmark named \"Toast notifications\" that `F6` moves into; the timer of every toast\npauses while the pointer is over the region or focus is inside it (WCAG 2.2.1). Toasts stack\nat the bottom center of the viewport, above dialogs and below tooltips. While a `Sheet` is open\nthey move into the free space beside it (or to the top, below its header, on narrow screens), so\nthey never cover its footer.",
      "props": [
        {
          "name": "queue",
          "type": "ToastQueue",
          "required": false,
          "default": "createToastQueue()",
          "description": "The queue to show. Defaults to the one `toast()` writes to."
        },
        {
          "name": "messages",
          "type": "Partial<{ region: string; dismiss: string; info: string; success: string; warning: string; danger: string; undo: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the region."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the region element."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "Toast",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "region",
            "kind": "string",
            "default": "Toast notifications",
            "description": "Accessible name of the region that holds the toasts (a landmark reachable with F6)."
          },
          {
            "key": "dismiss",
            "kind": "string",
            "default": "Dismiss notification",
            "description": "Name of the dismiss button of a toast."
          },
          {
            "key": "info",
            "kind": "string",
            "default": "Information",
            "description": "Visually hidden prefix that tells screen reader users the kind of toast; the dot alone cannot."
          },
          {
            "key": "success",
            "kind": "string",
            "default": "Success"
          },
          {
            "key": "warning",
            "kind": "string",
            "default": "Warning"
          },
          {
            "key": "danger",
            "kind": "string",
            "default": "Error"
          },
          {
            "key": "undo",
            "kind": "string",
            "default": "Undo",
            "description": "Label of the Undo action added by `toastWithUndo`. A verb."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-small",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-radius-lg",
        "--ds-radius-pill",
        "--ds-shadow-overlay",
        "--ds-sheet-width-lg",
        "--ds-sheet-width-md",
        "--ds-sheet-width-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-success",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-toast-dot-size",
        "--ds-toast-max-width",
        "--ds-toast-min-height",
        "--ds-toast-offset",
        "--ds-topbar-h",
        "--ds-warning",
        "--ds-z-toast"
      ],
      "stories": {
        "title": "Components/Toast",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "AllTones",
            "id": "components-toast--all-tones",
            "description": "Everything a toast can show, for the visual review. The toasts stay until dismissed.",
            "hasPlay": false
          },
          {
            "name": "WithAction",
            "id": "components-toast--with-action",
            "hasPlay": false
          },
          {
            "name": "LongMessage",
            "id": "components-toast--long-message",
            "hasPlay": false
          },
          {
            "name": "UndoPattern",
            "id": "components-toast--undo-pattern",
            "description": "The undo pattern: do the action at once, tell the user in a toast, offer Undo. No\nconfirmation dialog for reversible actions. Press the button, then Tab to Undo.",
            "hasPlay": true
          },
          {
            "name": "PausesOnHoverAndFocus",
            "id": "components-toast--pauses-on-hover-and-focus",
            "description": "Hovering or focusing the toast pauses its timer (WCAG 2.2.1).",
            "hasPlay": true
          },
          {
            "name": "KeyboardRegion",
            "id": "components-toast--keyboard-region",
            "description": "F6 moves focus into the Toast notifications region.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Toast/Toast.md",
        "summary": "A short, temporary message about something that just happened, with an optional action (usually Undo). Mount one `ToastRegion` and call `toast()` from anywhere. Built on React Aria's toast region and queue.\n\n- Handoff source: `components/feedback/Toast` (CMP-FBK-02). Undo recipe: GAP-008 groundwork.",
        "whenToUse": [
          "Feedback for an action that finished: \"Order #1051 created\", \"3 items archived\".",
          "Reversible actions, with **Undo**."
        ],
        "whenNotToUse": [
          "A state that stays on the page (\"Scheduled maintenance\"): use `Alert`.",
          "A field error: use the field's error message.",
          "Anything the user must read to proceed: use `Dialog` or `Alert`. A toast disappears.",
          "The only place an essential result is shown: toasts can be missed."
        ],
        "keyboard": [],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Feedback for an action that finished: \"Order #1051 created\", \"3 items archived\".\n- Reversible actions, with **Undo**."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A state that stays on the page (\"Scheduled maintenance\"): use `Alert`.\n- A field error: use the field's error message.\n- Anything the user must read to proceed: use `Dialog` or `Alert`. A toast disappears.\n- The only place an essential result is shown: toasts can be missed."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nToastRegion          role=region, aria-label \"Toast notifications\", landmark (F6); fixed, bottom center; z toast\n  Toast              role=alertdialog (React Aria), raised surface, strong line, radius lg, overlay shadow\n    dot              decorative, colored by tone\n    message          \"Success: \" (visually hidden) + text\n    Button           optional action (\"Undo\")\n    IconButton       \"Dismiss notification\"\n```"
          },
          {
            "id": "api",
            "heading": "API",
            "content": "```tsx\n// once, near the root, inside DSProvider\n<ToastRegion />;\n\n// anywhere\nimport { toast } from 'loui-ds';\ntoast('Draft saved'); // info\ntoast.success('Order #1051 created');\ntoast.warning('Stock is low for 2 items');\ntoast.danger('Payment failed', { timeout: null }); // stays until dismissed\ntoast.success('3 items archived', { action: { label: 'Undo', onPress: restore } });\n```\n\n| Option    | Type                                   | Default | Notes                                                   |\n| --------- | -------------------------------------- | ------- | ------------------------------------------------------- |\n| `tone`    | `info \\| success \\| warning \\| danger` | `info`  | Feedback components use `info`, not `neutral`/`accent`. |\n| `action`  | `{ label, onPress }`                   |         | One action. The toast closes after `onPress`.           |\n| `timeout` | `number \\| null`                       | `5000`  | Milliseconds. `null` = until dismissed.                 |\n| `onClose` | `() => void`                           |         | Timeout, dismiss or action.                             |\n\n`toastWithUndo(message, { onUndo })` shows a toast with a localized Undo action (see below). `toast(...)` returns a key; `toast.close(key)` and `toast.clear()` remove toasts. `useToast()` returns the same stable object. `createToastQueue()` / `createToastApi(queue)` and `<ToastRegion queue>` give an isolated queue (tests, a second region). `ToastRegion` also takes `messages` and `className`. At most four toasts show at once; the rest wait."
          },
          {
            "id": "default-timeout-5000-ms",
            "heading": "Default timeout: 5000 ms",
            "content": "The handoff closed toasts after 3.6 s, which is too short to read a sentence and reach Undo (WCAG 2.2.1 Timing Adjustable). The default is 5 s **and** the timer pauses while the pointer is over the toast region or focus is inside it, so a keyboard or slow reader can always finish. Use `timeout: null` for failures the user has to act on."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The region is a landmark named \"Toast notifications\". **F6** moves focus into it (and back), so Undo is keyboard reachable. React Aria registers a region as a landmark only if it is mounted with its first toast, so `ToastRegion` renders nothing while the queue is empty and mounts the region when a toast arrives.\n- Each toast is announced by React Aria's toast semantics. The tone is spoken as a prefix (\"Success: ...\", \"Error: ...\") from messages; the colored dot is decorative and `aria-hidden`.\n- Pause on hover and on focus within the region; the timer resumes on leave or blur.\n- Dismiss button named from messages (\"Dismiss notification\"); action is a real `Button`.\n- Layer `--ds-z-toast` (58): above dialogs, below tooltips. Bottom center keeps it clear of side panels (Sidebar). While a `Sheet` is open (GAP-052) the region moves into the free space beside it by reading `data-ds-sheet` on the document (`:root:has(...)`, CSS only, SSR-safe), so stacked toasts never cover the sheet footer; below 768px the sheet fills the width and toasts go to the top, under the sheet header. Keyboard (F6), pause on hover/focus and reduced motion are unchanged.\n- Reduced motion: the 150 ms fade-in is removed. Forced colors: border and dot use `CanvasText`.\n- `UNSTABLE_*` isolation: all React Aria `UNSTABLE_Toast*` imports live in `toast.rac.ts`. Renaming or stabilizing them is a one-file change."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Past tense, short, no exclamation marks: \"Order #1051 created\", \"3 items archived\".\n- Action: a verb (\"Undo\"). No \"Close\" button text; the dismiss control is an icon.\n- Do not repeat what is already visible on the page."
          },
          {
            "id": "the-undo-pattern-recipe",
            "heading": "The undo pattern (recipe)",
            "content": "\"Reversible actions never ask; they offer Undo\" (AGENTS.md section 5). Do the action immediately, then toast with an Undo that reverses it:\n\n```tsx\nfunction archiveOrder(order: Order) {\n  const previous = order.status;\n  setStatus(order.id, 'archived'); // 1. do it now\n  toast.success(`Order #${order.number} archived`, {\n    // 2. say so, past tense\n    action: {\n      label: 'Undo',\n      onPress: () => setStatus(order.id, previous), // 3. reverse it from the saved state\n    },\n  });\n}\n```\n\nRules:\n\n1. Save what you need to reverse **before** acting; Undo must not depend on state that has moved on.\n2. If the change was already sent to a server, Undo sends the inverse change; do not delay the real action to \"wait for Undo\".\n3. For deletes, soft-delete or keep the record for the Undo window plus a margin, and keep the confirmation dialog (`ConfirmDialog`), then show the toast. For example: confirm, delete, toast \"Order #1048 deleted\" with Undo.\n4. Irreversible actions get a `ConfirmDialog`, not a toast.\n5. Inside components, `useUndoable` wraps steps 1 to 3 and adds a deferred commit with failure handling. In plain modules (stores, services, handlers outside React) use `toastWithUndo`, below.\n\n### Undo outside React: `toastWithUndo`\n\n`toast(..., { action })` has no default label, and a hook cannot run in a plain module. `toastWithUndo` is a non-hook helper that takes the **Undo** label from the message catalogue (`Toast` key `undo`) instead of every call site repeating the word:\n\n```ts\nimport { toastWithUndo } from 'loui-ds';\n\nexport function archiveOrder(order: Order) {\n  const previous = order.status;\n  orders.setStatus(order.id, 'archived'); // do it now\n  toastWithUndo(`Order #${order.number} archived`, {\n    onUndo: () => orders.setStatus(order.id, previous),\n  });\n}\n```\n\n| Option               | Type                | Default        | Notes                                             |\n| -------------------- | ------------------- | -------------- | ------------------------------------------------- |\n| `onUndo`             | `() => void`        | required       | Reverses the change. The toast closes afterwards. |\n| `tone`               | `info               | success        | warning`                                          | `success` | Not `danger`: a failure is not undoable. |\n| `locale`             | `string`            | see below      | BCP 47 tag for the label.                         |\n| `messages`           | `{ undo?: string }` |                | Per-call override; wins over `locale`.            |\n| `toast`              | `ToastApi`          | shared `toast` | An isolated queue (tests, a second region).       |\n| `groupKey`           | `string`            |                | Replaces the previous undo toast of the same key. |\n| `timeout`, `onClose` |                     |                | As in `toast()`.                                  |\n\n`groupKey` (GAP-053) is for quick repeated actions of one kind (four toggles in a grid): a new toast with the same key closes the previous one, so only the latest change offers Undo and earlier ones are final. It does not offer \"Undo 4 changes\": the changes may be on different records and made in any order, so one Undo could not safely reverse them all. Different keys still stack. `useUndoable` takes the same `groupKey` on the action. It returns the toast key, like `toast()`. Outside React there is no context, so the label is resolved in this order: `messages.undo`, then `locale`, then the label the mounted `ToastRegion` publishes for the same queue (the `DSProvider` locale, a `messages={{ Toast: { undo } }}` provider override or the region's `messages` prop), then English. With a mounted region inside `DSProvider` you normally pass nothing. The helper does not defer anything and has no commit step; use `useUndoable` when you need that."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- A queue and region instead of a single stateless `Toast` you position yourself; timeout, pause on hover/focus, keyboard region, dismiss label from messages.\n- 5 s default instead of 3.6 s.\n- Tones `info | success | warning | danger` (the handoff's `neutral | accent` dot colors became `info`); the tone is spoken, not only colored.\n- Position is bottom center, away from side panels."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Alert",
            "reason": "A state that stays on the page (\"Scheduled maintenance\"): use `Alert`."
          },
          {
            "name": "Dialog",
            "reason": "Anything the user must read to proceed: use `Dialog` or `Alert`. A toast disappears."
          }
        ],
        "uses": ["Button", "IconButton", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Toggle",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Toggle } from 'loui-ds';",
      "folder": "src/components/Toggle",
      "sourceFile": "src/components/Toggle/Toggle.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A button that stays pressed: it switches one setting on or off (bold text, a filter, \"show\narchived\"). Built on React Aria `ToggleButton`: a native `<button>` with `aria-pressed`,\ntoggled by `Enter` and `Space`.\n\nUse it for an independent on/off choice that applies at once. For a setting inside a form\nuse `Switch` or `Checkbox`; for several related toggles use `ToggleGroup`; for one value out\nof a few use `SegmentedControl`.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Label: a short noun or verb in sentence case (\"Bold\", \"Show archived\")."
        },
        {
          "name": "leading",
          "type": "ReactNode",
          "required": false,
          "description": "Node before the label, usually `<Icon size=\"sm\" />`. Decorative; the label names the toggle."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Replaces the accessible name when the visible label is not enough. Rarely needed."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the toggle."
        },
        {
          "name": "icon",
          "type": "LucideIcon",
          "required": false,
          "description": "The icon to draw: one of the curated set (for example `StarIcon`). Decorative inside the button."
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "description": "Accessible name of an icon-only toggle, required. Name what the toggle controls, not what\nit looks like and not its state (\"Bold\", \"Show archived\"): the pressed state is announced\nseparately."
        },
        {
          "name": "tooltip",
          "type": "boolean | ToggleTooltipOptions",
          "required": false,
          "description": "Shows `label` in a `Tooltip` on hover and keyboard focus. Pass `true`, or an object to add\na shortcut or change the side. The `label` stays the accessible name."
        },
        {
          "name": "id",
          "type": "string",
          "required": false,
          "description": "Inside a `ToggleGroup`: the value of this toggle in the group's `value` (required there,\nunique in the group). Standalone: the DOM id."
        },
        {
          "name": "variant",
          "type": "'secondary' | 'ghost'",
          "required": false,
          "description": "Visual style, as for `Button`. `secondary` (default) has a border; `ghost` has none at rest.\nInside a `ToggleGroup` it defaults to the group's `variant`.",
          "values": ["secondary", "ghost"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Height token: `sm` 32px, `md` 40px (default), `lg` 48px. Inside a `ToggleGroup` it defaults\nto the group's `size`.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLButtonElement>",
          "required": false,
          "description": "Ref to the underlying `<button>` element."
        }
      ],
      "inheritedProps": [
        {
          "source": "react-aria",
          "interface": "AriaBaseButtonProps",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "aria-controls",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-disabled",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-expanded",
              "type": "boolean | \"true\" | \"false\"",
              "required": false
            },
            {
              "name": "aria-haspopup",
              "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\"",
              "required": false
            },
            {
              "name": "aria-pressed",
              "type": "boolean | \"true\" | \"false\" | \"mixed\"",
              "required": false
            },
            {
              "name": "preventFocusOnPress",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ButtonProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableDOMProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "excludeFromTabOrder",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusableProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "autoFocus",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "FocusEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onBlur",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocus",
              "type": "(e: FocusEvent<Element, Element>) => void",
              "required": false
            },
            {
              "name": "onFocusChange",
              "type": "(isFocused: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "HoverEvents",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "onHoverChange",
              "type": "(isHovering: boolean) => void",
              "required": false
            },
            {
              "name": "onHoverEnd",
              "type": "(e: HoverEvent) => void",
              "required": false
            },
            {
              "name": "onHoverStart",
              "type": "(e: HoverEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "KeyboardEvents",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "onKeyDown",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            },
            {
              "name": "onKeyUp",
              "type": "(e: KeyboardEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "PressEvents",
          "domGeneric": false,
          "count": 6,
          "props": [
            {
              "name": "onClick",
              "type": "(e: MouseEvent<FocusableElement, MouseEvent>) => void",
              "required": false
            },
            {
              "name": "onPress",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressChange",
              "type": "(isPressed: boolean) => void",
              "required": false
            },
            {
              "name": "onPressEnd",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressStart",
              "type": "(e: PressEvent) => void",
              "required": false
            },
            {
              "name": "onPressUp",
              "type": "(e: PressEvent) => void",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria-components",
          "interface": "SlotProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "slot",
              "type": "string | null",
              "required": false
            }
          ]
        },
        {
          "source": "react-aria",
          "interface": "ToggleButtonProps",
          "domGeneric": false,
          "count": 3,
          "props": [
            {
              "name": "defaultSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "isSelected",
              "type": "boolean",
              "required": false
            },
            {
              "name": "onChange",
              "type": "(isSelected: boolean) => void",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 63
        }
      ],
      "variants": {
        "variant": ["secondary", "ghost"],
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-control-h-lg",
        "--ds-control-h-md",
        "--ds-control-h-sm",
        "--ds-duration",
        "--ds-ease",
        "--ds-focus-ring",
        "--ds-font-sans",
        "--ds-fs-body",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-body",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-opacity-disabled",
        "--ds-radius-md",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-space-5",
        "--ds-surface-hover",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-weight-medium"
      ],
      "stories": {
        "title": "Components/Toggle",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-toggle--default",
            "hasPlay": true
          },
          {
            "name": "Pressed",
            "id": "components-toggle--pressed",
            "hasPlay": true
          },
          {
            "name": "Ghost",
            "id": "components-toggle--ghost",
            "hasPlay": false
          },
          {
            "name": "GhostPressed",
            "id": "components-toggle--ghost-pressed",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "components-toggle--small",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-toggle--large",
            "hasPlay": false
          },
          {
            "name": "WithLeadingIcon",
            "id": "components-toggle--with-leading-icon",
            "hasPlay": false
          },
          {
            "name": "IconOnly",
            "id": "components-toggle--icon-only",
            "hasPlay": true
          },
          {
            "name": "IconOnlyWithTooltip",
            "id": "components-toggle--icon-only-with-tooltip",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-toggle--disabled",
            "hasPlay": true
          },
          {
            "name": "DisabledPressed",
            "id": "components-toggle--disabled-pressed",
            "hasPlay": false
          },
          {
            "name": "KeyboardToggle",
            "id": "components-toggle--keyboard-toggle",
            "description": "`Space` and `Enter` toggle the focused button.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-toggle--controlled",
            "hasPlay": true
          },
          {
            "name": "LongLabel",
            "id": "components-toggle--long-label",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-toggle--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-toggle--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Toggle/Toggle.md",
        "summary": "A button that stays pressed: it switches one setting on or off (bold text, a filter, \"show archived\") and applies it at once. A native `<button>` with `aria-pressed`, built on React Aria `ToggleButton`. Looks like `Button` (`secondary` and `ghost`), with a clear pressed state.\n\n- TODO `GAP-005`.",
        "whenToUse": [
          "One independent on/off choice in a toolbar or filter bar: Bold, Show archived, Only mine.",
          "An icon-only on/off control (Favorite, Pin) with a required `label`."
        ],
        "whenNotToUse": [
          "A setting that is saved with a form: use `Switch` or `Checkbox`.",
          "Several related toggles that share a value: use `ToggleGroup`.",
          "One value out of a few options (View: List, Board): use `SegmentedControl`.",
          "An action that does something once: use `Button`."
        ],
        "keyboard": ["Keyboard: `Tab` focuses; `Space` and `Enter` toggle."],
        "examples": [
          "<Toggle isSelected={showArchived} onChange={setShowArchived}>\n  Show archived\n</Toggle>\n\n<Toggle icon={StarIcon} label=\"Favorite\" tooltip variant=\"ghost\" />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- One independent on/off choice in a toolbar or filter bar: Bold, Show archived, Only mine.\n- An icon-only on/off control (Favorite, Pin) with a required `label`."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A setting that is saved with a form: use `Switch` or `Checkbox`.\n- Several related toggles that share a value: use `ToggleGroup`.\n- One value out of a few options (View: List, Board): use `SegmentedControl`.\n- An action that does something once: use `Button`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nbutton[aria-pressed]\n  leading   (optional, decorative)\n  label     (or a single icon when icon-only)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                                   | Default       | Notes                                                            |\n| -------------------------------- | -------------------------------------- | ------------- | ---------------------------------------------------------------- |\n| `children`                       | `ReactNode`                            | required\\*    | Label in sentence case. \\*Not used by an icon-only toggle.       |\n| `icon` + `label`                 | `IconComponent` + `string`             | none          | Icon-only: `label` is required and becomes `aria-label`.         |\n| `tooltip`                        | `boolean \\| { shortcut?, placement? }` | off           | Icon-only only: shows `label` in a `Tooltip` on hover and focus. |\n| `leading`                        | `ReactNode`                            | none          | Decorative node before the label.                                |\n| `variant`                        | `'secondary' \\| 'ghost'`               | `'secondary'` | As for `Button`. Inside a `ToggleGroup`: the group's `variant`.  |\n| `size`                           | `'sm' \\| 'md' \\| 'lg'`                 | `'md'`        | 32 / 40 / 48px. Inside a `ToggleGroup`: the group's `size`.      |\n| `isSelected` / `defaultSelected` | `boolean`                              | `false`       | Controlled / uncontrolled pressed state.                         |\n| `onChange`                       | `(isSelected: boolean) => void`        | none          | Called with the new state.                                       |\n| `id`                             | `string`                               | none          | Inside a `ToggleGroup`: the toggle's value (required there).     |\n| `isDisabled`                     | `boolean`                              | `false`       | Reduced opacity, not focusable.                                  |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Semantics: `<button aria-pressed=\"true|false\">`. Inside a single-selection `ToggleGroup` it is a `radio` with `aria-checked`.\n- Keyboard: `Tab` focuses; `Space` and `Enter` toggle.\n- The pressed state is shown by a brighter border, a raised fill and ink text, never by color alone. Focus is the accent border and ring.\n- Icon-only toggles need a `label` that names what the toggle controls, not its state: the pressed state is announced by the browser. The icon is decorative.\n- Targets are at least 32px (`sm`). Forced colors: the pressed toggle gets a `Highlight` border and an underline."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Label with a short noun or verb phrase in sentence case: \"Bold\", \"Show archived\". Do not change the label when the state changes (\"Hide archived\" would be read as the opposite of the state)."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Toggle isSelected={showArchived} onChange={setShowArchived}>\n  Show archived\n</Toggle>\n\n<Toggle icon={StarIcon} label=\"Favorite\" tooltip variant=\"ghost\" />\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Switch",
            "reason": "A setting that is saved with a form: use `Switch` or `Checkbox`."
          },
          {
            "name": "Checkbox",
            "reason": "A setting that is saved with a form: use `Switch` or `Checkbox`."
          },
          {
            "name": "ToggleGroup",
            "reason": "Several related toggles that share a value: use `ToggleGroup`."
          },
          {
            "name": "SegmentedControl",
            "reason": "One value out of a few options (View: List, Board): use `SegmentedControl`."
          },
          {
            "name": "Button",
            "reason": "An action that does something once: use `Button`."
          }
        ],
        "uses": ["Icon", "Tooltip"],
        "usedBy": ["ResponseFeedback"]
      }
    },
    {
      "name": "ToggleGroup",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { ToggleGroup } from 'loui-ds';",
      "folder": "src/components/ToggleGroup",
      "sourceFile": "src/components/ToggleGroup/ToggleGroup.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A row (or column) of related `Toggle`s that share one value: the list of pressed `id`s.\n\nWith the default `multiple` selection the toggles are independent (bold, italic, underline;\nor a set of filters) and the group is a `toolbar`. With `single` selection it behaves like\na radio group that can also be empty.\n\n`ToggleGroup` vs `SegmentedControl`: a `SegmentedControl` is one value of one setting (View:\nList, Board), exactly one option is always selected. A `ToggleGroup` is a set of on/off\nchoices where none, one or several are on.\n\nBuilt on React Aria `ToggleButtonGroup`: one tab stop, arrow keys move between toggles (they\nstop at the ends), `Space` and `Enter` toggle the focused one.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": "The `Toggle` children. Each needs an `id` that is unique in the group."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the group (\"Text formatting\"). Required: name this or use `aria-labelledby`."
        },
        {
          "name": "aria-labelledby",
          "type": "string",
          "required": false,
          "description": "Id of the element that names the group."
        },
        {
          "name": "selectionMode",
          "type": "'single' | 'multiple'",
          "required": false,
          "default": "'multiple'",
          "description": "`multiple` (default): any number of toggles can be pressed, independently (text formatting,\nfilters); the group is a toolbar. `single`: at most one is pressed; the group is a radio\ngroup. To pick exactly one value of a setting prefer `SegmentedControl`.",
          "values": ["single", "multiple"]
        },
        {
          "name": "value",
          "type": "readonly string[]",
          "required": false,
          "description": "The `id`s of the pressed toggles (controlled). Pair with `onChange`."
        },
        {
          "name": "defaultValue",
          "type": "readonly string[]",
          "required": false,
          "description": "The `id`s of the toggles that start pressed (uncontrolled)."
        },
        {
          "name": "onChange",
          "type": "(value: string[]) => void",
          "required": false,
          "description": "Called with the `id`s of all pressed toggles after the user presses or releases one."
        },
        {
          "name": "disallowEmptySelection",
          "type": "boolean",
          "required": false,
          "description": "Keeps at least one toggle pressed. Pointless in `multiple` mode unless you want that rule."
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'",
          "description": "Layout and arrow keys: `horizontal` (default, `←` `→`) or `vertical` (`↑` `↓`).",
          "values": ["horizontal", "vertical"]
        },
        {
          "name": "variant",
          "type": "'secondary' | 'ghost'",
          "required": false,
          "description": "Visual style handed down to every `Toggle` that does not set its own.",
          "values": ["secondary", "ghost"]
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "description": "Size handed down to every `Toggle` that does not set its own.",
          "values": ["sm", "md", "lg"]
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": ""
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the group element."
        }
      ],
      "inheritedProps": [
        {
          "source": "@react-types/shared",
          "interface": "AriaLabelingProps",
          "domGeneric": false,
          "count": 2,
          "props": [
            {
              "name": "aria-describedby",
              "type": "string",
              "required": false
            },
            {
              "name": "aria-details",
              "type": "string",
              "required": false
            }
          ]
        },
        {
          "source": "react-stately",
          "interface": "ToggleGroupProps",
          "domGeneric": false,
          "count": 1,
          "props": [
            {
              "name": "isDisabled",
              "type": "boolean",
              "required": false
            }
          ]
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMAttributes",
          "domGeneric": true,
          "count": 6
        },
        {
          "source": "@react-types/shared",
          "interface": "GlobalDOMEvents",
          "domGeneric": true,
          "count": 64
        }
      ],
      "variants": {
        "variant": ["secondary", "ghost"],
        "size": ["sm", "md", "lg"]
      },
      "cssVariables": ["--ds-space-2"],
      "stories": {
        "title": "Components/ToggleGroup",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-toggle-group--default",
            "hasPlay": true
          },
          {
            "name": "Preselected",
            "id": "components-toggle-group--preselected",
            "hasPlay": true
          },
          {
            "name": "Ghost",
            "id": "components-toggle-group--ghost",
            "hasPlay": false
          },
          {
            "name": "Small",
            "id": "components-toggle-group--small",
            "hasPlay": false
          },
          {
            "name": "Large",
            "id": "components-toggle-group--large",
            "hasPlay": false
          },
          {
            "name": "Vertical",
            "id": "components-toggle-group--vertical",
            "hasPlay": true
          },
          {
            "name": "IconOnly",
            "id": "components-toggle-group--icon-only",
            "hasPlay": false
          },
          {
            "name": "SingleSelection",
            "id": "components-toggle-group--single-selection",
            "hasPlay": true
          },
          {
            "name": "DisabledToggle",
            "id": "components-toggle-group--disabled-toggle",
            "hasPlay": true
          },
          {
            "name": "DisabledGroup",
            "id": "components-toggle-group--disabled-group",
            "hasPlay": true
          },
          {
            "name": "KeyboardSelection",
            "id": "components-toggle-group--keyboard-selection",
            "description": "One tab stop. Arrows move focus (and stop at the ends), Space and Enter press.",
            "hasPlay": true
          },
          {
            "name": "Controlled",
            "id": "components-toggle-group--controlled",
            "hasPlay": true
          },
          {
            "name": "Wrapping",
            "id": "components-toggle-group--wrapping",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-toggle-group--right-to-left",
            "hasPlay": false
          },
          {
            "name": "Focused",
            "id": "components-toggle-group--focused",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/ToggleGroup/ToggleGroup.md",
        "summary": "A row (or column) of related `Toggle`s that share one value: the list of pressed `id`s. With the default `multiple` selection the toggles are independent (bold, italic, underline; a set of filters). With `single` selection at most one is pressed. Built on React Aria `ToggleButtonGroup`.\n\n- TODO `GAP-005`.",
        "whenToUse": [
          "Text formatting (Bold, Italic), list markers, or a small set of filters that combine."
        ],
        "whenNotToUse": [
          "A single on/off choice: use `Toggle`.",
          "Choosing one value of a setting: use `SegmentedControl`.",
          "A long list of filters: use `CheckboxGroup` or a `Select`/`Combobox` with multiple selection."
        ],
        "keyboard": [
          "Keyboard: one tab stop; `←` / `→` (`↑` / `↓` when vertical) move focus and stop at the ends (React Aria does not wrap); `Space` and `Enter` press the focused toggle. Disabled toggles are skipped."
        ],
        "examples": [
          "<ToggleGroup aria-label=\"Text formatting\" value={marks} onChange={setMarks}>\n  <Toggle id=\"fav\" icon={StarIcon} label=\"Favorite\" />\n  <Toggle id=\"pin\" icon={StarIcon} label=\"Pinned\" />\n</ToggleGroup>\n\n<ToggleGroup aria-label=\"Order filters\" defaultValue={['mine']}>\n  <Toggle id=\"mine\">Only mine</Toggle>\n  <Toggle id=\"late\">Late</Toggle>\n</ToggleGroup>"
        ],
        "sections": [
          {
            "id": "togglegroup-or-segmentedcontrol",
            "heading": "ToggleGroup or SegmentedControl",
            "content": "|           | `SegmentedControl`                           | `ToggleGroup`                                                    |\n| --------- | -------------------------------------------- | ---------------------------------------------------------------- |\n| Meaning   | One value of one setting (View: List, Board) | Independent on/off choices (Bold, Italic; filters)               |\n| Selection | Exactly one option, always                   | None, one or several (`multiple`), or none/one                   |\n| Role      | `radiogroup` of `radio`                      | `toolbar` of `button[aria-pressed]` (`radiogroup` when `single`) |\n| Value     | `string`                                     | `string[]`                                                       |\n| Content   | `options` array with optional counts         | `Toggle` children, text or icon-only                             |\n\nRule of thumb: if choosing one option makes the others \"not selected\" and one must always be on, use `SegmentedControl`. If each choice can stand alone, use `ToggleGroup`. Use `single` here only when \"nothing selected\" is meaningful (for example an optional filter)."
          },
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Text formatting (Bold, Italic), list markers, or a small set of filters that combine."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A single on/off choice: use `Toggle`.\n- Choosing one value of a setting: use `SegmentedControl`.\n- A long list of filters: use `CheckboxGroup` or a `Select`/`Combobox` with multiple selection."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv[role=toolbar] aria-label       (or role=radiogroup when single)\n  button[aria-pressed]             (one `Toggle` per choice, each with an id)\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                             | Type                         | Default          | Notes                                                       |\n| -------------------------------- | ---------------------------- | ---------------- | ----------------------------------------------------------- |\n| `aria-label` / `aria-labelledby` | `string`                     | one required     | Names the group (\"Text formatting\"). Enforced by the types. |\n| `children`                       | `Toggle`s                    | required         | Each `Toggle` needs a unique `id`.                          |\n| `selectionMode`                  | `'multiple' \\| 'single'`     | `'multiple'`     | See the table above.                                        |\n| `value` / `defaultValue`         | `readonly string[]`          | `[]`             | Controlled / uncontrolled list of pressed ids.              |\n| `onChange`                       | `(value: string[]) => void`  | none             | Called with all pressed ids after each change.              |\n| `disallowEmptySelection`         | `boolean`                    | `false`          | Keeps at least one toggle pressed.                          |\n| `orientation`                    | `'horizontal' \\| 'vertical'` | `'horizontal'`   | Layout and arrow keys.                                      |\n| `variant` / `size`               | as `Toggle`                  | `secondary`/`md` | Defaults for the toggles; a toggle's own props win.         |\n| `isDisabled`                     | `boolean`                    | `false`          | Disables every toggle.                                      |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Semantics: `multiple` is a `toolbar` (a group of buttons with `aria-pressed`); `single` is a `radiogroup` of `radio` buttons. The group needs an accessible name (enforced by the types).\n- Keyboard: one tab stop; `←` / `→` (`↑` / `↓` when vertical) move focus and stop at the ends (React Aria does not wrap); `Space` and `Enter` press the focused toggle. Disabled toggles are skipped.\n- Pressed state is shown by border, fill and ink text, not by color alone.\n- Icon-only toggles need a `label`."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Group name: a noun phrase (\"Text formatting\", \"Filters\"). Toggle labels: one word or a short phrase in sentence case."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<ToggleGroup aria-label=\"Text formatting\" value={marks} onChange={setMarks}>\n  <Toggle id=\"fav\" icon={StarIcon} label=\"Favorite\" />\n  <Toggle id=\"pin\" icon={StarIcon} label=\"Pinned\" />\n</ToggleGroup>\n\n<ToggleGroup aria-label=\"Order filters\" defaultValue={['mine']}>\n  <Toggle id=\"mine\">Only mine</Toggle>\n  <Toggle id=\"late\">Late</Toggle>\n</ToggleGroup>\n```"
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Toggle",
            "reason": "A single on/off choice: use `Toggle`."
          },
          {
            "name": "SegmentedControl",
            "reason": "Choosing one value of a setting: use `SegmentedControl`."
          },
          {
            "name": "CheckboxGroup",
            "reason": "A long list of filters: use `CheckboxGroup` or a `Select`/`Combobox` with multiple selection."
          },
          {
            "name": "Select",
            "reason": "A long list of filters: use `CheckboxGroup` or a `Select`/`Combobox` with multiple selection."
          },
          {
            "name": "Combobox",
            "reason": "A long list of filters: use `CheckboxGroup` or a `Select`/`Combobox` with multiple selection."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "ToolCallList",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ToolCallList } from 'loui-ds/patterns';",
      "folder": "src/patterns/ToolCallStep",
      "sourceFile": "src/patterns/ToolCallStep/ToolCallList.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The steps of a multi-step agent run as an ordered list of `ToolCallStep`. Use it under the\nmessage the run belongs to; each step opens its own details.",
      "props": [
        {
          "name": "steps",
          "type": "readonly ToolCallListStep[]",
          "required": true,
          "description": "The steps of the run, in the order they happened."
        },
        {
          "name": "aria-label",
          "type": "string",
          "required": false,
          "description": "Accessible name of the list. Defaults to \"Steps\"; say which run it is (\"Steps of the order check\")."
        },
        {
          "name": "messages",
          "type": "Partial<{ running: string; succeeded: string; failed: string; input: string; output: string; error: string; finishedSucceeded: (name: string) => string; finishedFailed: (name: string) => string; listLabel: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance (also applied to every step)."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 52
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "OlHTMLAttributes",
          "domGeneric": true,
          "count": 3
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ToolCallStep",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "running",
            "kind": "string",
            "default": "Running",
            "description": "Visible status text of a step."
          },
          {
            "key": "succeeded",
            "kind": "string",
            "default": "Succeeded"
          },
          {
            "key": "failed",
            "kind": "string",
            "default": "Failed"
          },
          {
            "key": "input",
            "kind": "string",
            "default": "Input",
            "description": "Labels of the detail blocks, and the accessible names of their code blocks."
          },
          {
            "key": "output",
            "kind": "string",
            "default": "Output"
          },
          {
            "key": "error",
            "kind": "string",
            "default": "Error",
            "description": "Label of the output block when the step failed."
          },
          {
            "key": "finishedSucceeded",
            "kind": "function",
            "default": "{name} succeeded",
            "signature": "(name: string)",
            "description": "Polite announcement when a running step finishes."
          },
          {
            "key": "finishedFailed",
            "kind": "function",
            "default": "{name} failed",
            "signature": "(name: string)"
          },
          {
            "key": "listLabel",
            "kind": "string",
            "default": "Steps",
            "description": "Accessible name of a ToolCallList."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-line",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-line",
        "--ds-radius-lg",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tool-call-step-gap",
        "--ds-tool-call-step-list-gap",
        "--ds-tool-call-step-padding-block",
        "--ds-tool-call-step-padding-inline",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Patterns/ToolCallStep",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-tool-call-step--default",
            "hasPlay": true
          },
          {
            "name": "Running",
            "id": "patterns-tool-call-step--running",
            "hasPlay": false
          },
          {
            "name": "Failed",
            "id": "patterns-tool-call-step--failed",
            "hasPlay": false
          },
          {
            "name": "Expanded",
            "id": "patterns-tool-call-step--expanded",
            "hasPlay": false
          },
          {
            "name": "WithoutDetails",
            "id": "patterns-tool-call-step--without-details",
            "description": "Without input or output there is nothing to open: the header is plain text.",
            "hasPlay": false
          },
          {
            "name": "LongName",
            "id": "patterns-tool-call-step--long-name",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-tool-call-step--right-to-left",
            "hasPlay": false
          },
          {
            "name": "MultiStepRun",
            "id": "patterns-tool-call-step--multi-step-run",
            "description": "A run in progress: the last step finishes and a polite announcement says so.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ToolCallStep/ToolCallStep.md",
        "summary": "One step of an agent run: the tool it called, whether it is running, succeeded or failed, how long it took and, on request, what went in and out. `ToolCallList` shows a multi-step run. Import both from `loui-ds/patterns`.",
        "whenToUse": [
          "Under an assistant message, to show what the agent did before it answered (\"search_orders\", \"draft_reply\").",
          "A run log where the owner wants to check why the agent answered as it did."
        ],
        "whenNotToUse": [
          "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`.",
          "Something the owner has to approve: use `ApprovalCard`."
        ],
        "keyboard": [
          "The header is a button inside a heading (`aria-expanded`, `aria-controls`); `Enter` and `Space` toggle it."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Under an assistant message, to show what the agent did before it answered (\"search_orders\", \"draft_reply\").\n- A run log where the owner wants to check why the agent answered as it did."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`.\n- Something the owner has to approve: use `ApprovalCard`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (ToolCallStep)\n  h{headingLevel} > button   chevron · status icon + text · name (mono) · duration (mono)\n  details (when open)        \"Input\" label + code block · \"Output\" or \"Error\" label + code block\n  status                     visually hidden live region\nol (ToolCallList)            one li per step\n```\n\nWithout `input` and `output` the header is plain text and there is nothing to open."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                                  | Type                                   | Default | Notes                                                                        |\n| ----------------------------------------------------- | -------------------------------------- | ------- | ---------------------------------------------------------------------------- |\n| `name`                                                | `string`                               | -       | Required. Mono; part of the button name.                                     |\n| `status`                                              | `'running' \\| 'succeeded' \\| 'failed'` | -       | Required. Always shown as text plus an icon (Spinner while running).         |\n| `durationMs`                                          | `number`                               |         | Milliseconds. Shown as `85 ms` or `1.2 s`, formatted with the active locale. |\n| `input` / `output`                                    | `ReactNode`                            |         | A string goes in a `Code` block; nodes are rendered as given.                |\n| `isExpanded` / `defaultExpanded` / `onExpandedChange` | `boolean` / `boolean` / `(b) => void`  | closed  | Controlled or uncontrolled details.                                          |\n| `headingLevel`                                        | `1`-`6`                                | `4`     |                                                                              |\n| `messages`                                            | partial catalog                        |         | `running`, `succeeded`, `failed`, `input`, `output`, `error`, announcements. |\n\n`ToolCallList` takes `steps` (each a `ToolCallStep` prop set plus a stable `stepId`), an `aria-label` (default \"Steps\") and `messages`, which it shares with every step."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The header is a button inside a heading (`aria-expanded`, `aria-controls`); `Enter` and `Space` toggle it.\n- Color never carries the status: the text \"Running\", \"Succeeded\" or \"Failed\" is always visible, with an icon.\n- When a running step finishes, a polite live region says \"search_orders succeeded\" or \"search_orders failed\" once. A step that is already finished on first render is silent.\n- Code blocks that overflow become labelled, keyboard-scrollable regions (\"Input\", \"Output\", \"Error\").\n- Reduced motion and forced colors are handled."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Tool names are identifiers: show them as the agent calls them, in mono.\n- Put the error text in `output` when a step fails, and say how to fix it when you can.\n- Do not put secrets, tokens or customer data in `input` or `output`."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<ToolCallList\n  aria-label=\"Steps of the order check\"\n  steps={[\n    {\n      stepId: 's1',\n      name: 'search_orders',\n      status: 'succeeded',\n      durationMs: 1240,\n      input: '{ \"customer\": \"C-204\" }',\n      output: '{ \"count\": 3 }',\n    },\n    { stepId: 's2', name: 'draft_reply', status: 'running' },\n  ]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "No handoff source: new pattern (TODO `AI-013`)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "StatusSteps",
            "reason": "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`."
          },
          {
            "name": "Progress",
            "reason": "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`."
          },
          {
            "name": "ApprovalCard",
            "reason": "Something the owner has to approve: use `ApprovalCard`."
          }
        ],
        "uses": [],
        "usedBy": []
      }
    },
    {
      "name": "ToolCallStep",
      "kind": "component",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { ToolCallStep } from 'loui-ds/patterns';",
      "folder": "src/patterns/ToolCallStep",
      "sourceFile": "src/patterns/ToolCallStep/ToolCallStep.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "One step of an agent run: the tool it called, whether it is running, succeeded or failed, how\nlong it took and, on request, what went in and out.\n\nWith `input` or `output` the header is a button that opens the details (`aria-expanded`,\n`aria-controls`), inside a heading so the steps appear in the outline. Without them it is\nplain text. The status is text plus an icon, never color alone. When a running step finishes,\na polite live region says so once; a step that is already finished on first render is silent.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "Name of the tool or step (\"search_orders\"). Shown in mono; it is the name of the details button."
        },
        {
          "name": "status",
          "type": "'running' | 'succeeded' | 'failed'",
          "required": true,
          "description": "`running` (spinner), `succeeded` or `failed`. The status is always shown as text next to its icon.",
          "values": ["running", "succeeded", "failed"]
        },
        {
          "name": "durationMs",
          "type": "number",
          "required": false,
          "description": "How long the step took, in milliseconds. Omit while it is running."
        },
        {
          "name": "input",
          "type": "ReactNode",
          "required": false,
          "description": "What the tool was called with. A string is shown in a code block; pass nodes for anything\nricher. With `input` or `output` the step gets a details button."
        },
        {
          "name": "output",
          "type": "ReactNode",
          "required": false,
          "description": "What the tool returned, or the error text when `status` is `failed`. Same rules as `input`."
        },
        {
          "name": "isExpanded",
          "type": "boolean",
          "required": false,
          "description": "Controlled state of the details."
        },
        {
          "name": "defaultExpanded",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial state of the details when uncontrolled. Default closed."
        },
        {
          "name": "onExpandedChange",
          "type": "(isExpanded: boolean) => void",
          "required": false,
          "description": "Called when the details open or close."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "4",
          "description": "Heading level of the step header (`h1` to `h6`). Default `4`.",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "messages",
          "type": "Partial<{ running: string; succeeded: string; failed: string; input: string; output: string; error: string; finishedSucceeded: (name: string) => string; finishedFailed: (name: string) => string; listLabel: string; }>",
          "required": false,
          "description": "Override the built-in messages for this instance."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 168
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "messages": {
        "providerKey": "ToolCallStep",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "running",
            "kind": "string",
            "default": "Running",
            "description": "Visible status text of a step."
          },
          {
            "key": "succeeded",
            "kind": "string",
            "default": "Succeeded"
          },
          {
            "key": "failed",
            "kind": "string",
            "default": "Failed"
          },
          {
            "key": "input",
            "kind": "string",
            "default": "Input",
            "description": "Labels of the detail blocks, and the accessible names of their code blocks."
          },
          {
            "key": "output",
            "kind": "string",
            "default": "Output"
          },
          {
            "key": "error",
            "kind": "string",
            "default": "Error",
            "description": "Label of the output block when the step failed."
          },
          {
            "key": "finishedSucceeded",
            "kind": "function",
            "default": "{name} succeeded",
            "signature": "(name: string)",
            "description": "Polite announcement when a running step finishes."
          },
          {
            "key": "finishedFailed",
            "kind": "function",
            "default": "{name} failed",
            "signature": "(name: string)"
          },
          {
            "key": "listLabel",
            "kind": "string",
            "default": "Steps",
            "description": "Accessible name of a ToolCallList."
          }
        ]
      },
      "cssVariables": [
        "--ds-accent",
        "--ds-danger",
        "--ds-danger-line",
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-fs-meta",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-line",
        "--ds-radius-lg",
        "--ds-space-1",
        "--ds-success",
        "--ds-surface",
        "--ds-surface-row-hover",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tool-call-step-gap",
        "--ds-tool-call-step-list-gap",
        "--ds-tool-call-step-padding-block",
        "--ds-tool-call-step-padding-inline",
        "--ds-tracking-caption"
      ],
      "stories": {
        "title": "Patterns/ToolCallStep",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "patterns-tool-call-step--default",
            "hasPlay": true
          },
          {
            "name": "Running",
            "id": "patterns-tool-call-step--running",
            "hasPlay": false
          },
          {
            "name": "Failed",
            "id": "patterns-tool-call-step--failed",
            "hasPlay": false
          },
          {
            "name": "Expanded",
            "id": "patterns-tool-call-step--expanded",
            "hasPlay": false
          },
          {
            "name": "WithoutDetails",
            "id": "patterns-tool-call-step--without-details",
            "description": "Without input or output there is nothing to open: the header is plain text.",
            "hasPlay": false
          },
          {
            "name": "LongName",
            "id": "patterns-tool-call-step--long-name",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "patterns-tool-call-step--right-to-left",
            "hasPlay": false
          },
          {
            "name": "MultiStepRun",
            "id": "patterns-tool-call-step--multi-step-run",
            "description": "A run in progress: the last step finishes and a polite announcement says so.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/patterns/ToolCallStep/ToolCallStep.md",
        "summary": "One step of an agent run: the tool it called, whether it is running, succeeded or failed, how long it took and, on request, what went in and out. `ToolCallList` shows a multi-step run. Import both from `loui-ds/patterns`.",
        "whenToUse": [
          "Under an assistant message, to show what the agent did before it answered (\"search_orders\", \"draft_reply\").",
          "A run log where the owner wants to check why the agent answered as it did."
        ],
        "whenNotToUse": [
          "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`.",
          "Something the owner has to approve: use `ApprovalCard`."
        ],
        "keyboard": [
          "The header is a button inside a heading (`aria-expanded`, `aria-controls`); `Enter` and `Space` toggle it."
        ],
        "examples": [],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Under an assistant message, to show what the agent did before it answered (\"search_orders\", \"draft_reply\").\n- A run log where the owner wants to check why the agent answered as it did."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`.\n- Something the owner has to approve: use `ApprovalCard`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\ndiv (ToolCallStep)\n  h{headingLevel} > button   chevron · status icon + text · name (mono) · duration (mono)\n  details (when open)        \"Input\" label + code block · \"Output\" or \"Error\" label + code block\n  status                     visually hidden live region\nol (ToolCallList)            one li per step\n```\n\nWithout `input` and `output` the header is plain text and there is nothing to open."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                                  | Type                                   | Default | Notes                                                                        |\n| ----------------------------------------------------- | -------------------------------------- | ------- | ---------------------------------------------------------------------------- |\n| `name`                                                | `string`                               | -       | Required. Mono; part of the button name.                                     |\n| `status`                                              | `'running' \\| 'succeeded' \\| 'failed'` | -       | Required. Always shown as text plus an icon (Spinner while running).         |\n| `durationMs`                                          | `number`                               |         | Milliseconds. Shown as `85 ms` or `1.2 s`, formatted with the active locale. |\n| `input` / `output`                                    | `ReactNode`                            |         | A string goes in a `Code` block; nodes are rendered as given.                |\n| `isExpanded` / `defaultExpanded` / `onExpandedChange` | `boolean` / `boolean` / `(b) => void`  | closed  | Controlled or uncontrolled details.                                          |\n| `headingLevel`                                        | `1`-`6`                                | `4`     |                                                                              |\n| `messages`                                            | partial catalog                        |         | `running`, `succeeded`, `failed`, `input`, `output`, `error`, announcements. |\n\n`ToolCallList` takes `steps` (each a `ToolCallStep` prop set plus a stable `stepId`), an `aria-label` (default \"Steps\") and `messages`, which it shares with every step."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The header is a button inside a heading (`aria-expanded`, `aria-controls`); `Enter` and `Space` toggle it.\n- Color never carries the status: the text \"Running\", \"Succeeded\" or \"Failed\" is always visible, with an icon.\n- When a running step finishes, a polite live region says \"search_orders succeeded\" or \"search_orders failed\" once. A step that is already finished on first render is silent.\n- Code blocks that overflow become labelled, keyboard-scrollable regions (\"Input\", \"Output\", \"Error\").\n- Reduced motion and forced colors are handled."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Tool names are identifiers: show them as the agent calls them, in mono.\n- Put the error text in `output` when a step fails, and say how to fix it when you can.\n- Do not put secrets, tokens or customer data in `input` or `output`."
          },
          {
            "id": "example",
            "heading": "Example",
            "content": "```tsx\n<ToolCallList\n  aria-label=\"Steps of the order check\"\n  steps={[\n    {\n      stepId: 's1',\n      name: 'search_orders',\n      status: 'succeeded',\n      durationMs: 1240,\n      input: '{ \"customer\": \"C-204\" }',\n      output: '{ \"count\": 3 }',\n    },\n    { stepId: 's2', name: 'draft_reply', status: 'running' },\n  ]}\n/>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "No handoff source: new pattern (TODO `AI-013`)."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "StatusSteps",
            "reason": "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`."
          },
          {
            "name": "Progress",
            "reason": "Progress of a long job with a known number of steps: use `StatusSteps` or `Progress`."
          },
          {
            "name": "ApprovalCard",
            "reason": "Something the owner has to approve: use `ApprovalCard`."
          }
        ],
        "uses": ["Code", "Icon", "Spinner", "VisuallyHidden"],
        "usedBy": []
      }
    },
    {
      "name": "Tooltip",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Tooltip } from 'loui-ds';",
      "folder": "src/components/Tooltip",
      "sourceFile": "src/components/Tooltip/Tooltip.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "An ink-style hint for a control, built on React Aria `TooltipTrigger` and `Tooltip`.\n\nIt opens on hover (after a delay) and on keyboard focus, closes with `Escape` (WCAG 1.4.13),\nstays open while the pointer is over it, and describes the trigger through `aria-describedby`.\nA tooltip is a supplement: the trigger must already have its own accessible name.",
      "props": [
        {
          "name": "children",
          "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
          "required": true,
          "description": "The element the tooltip describes. It must be focusable and built on React Aria, such as\n`Button`, `IconButton` or `Link`, so the tooltip opens on keyboard focus as well as hover."
        },
        {
          "name": "content",
          "type": "ReactNode",
          "required": true,
          "description": "Short text, a few words. It is linked to the trigger with `aria-describedby`, so it must\nadd detail only: never put the only copy of essential information in a tooltip, because\ntouch users cannot hover and the text is not shown until the trigger is focused."
        },
        {
          "name": "shortcut",
          "type": "string",
          "required": false,
          "description": "Keyboard shortcut shown after the text in mono type (\"⌘K\"). Pair it with `aria-keyshortcuts` on the trigger."
        },
        {
          "name": "placement",
          "type": "'top' | 'bottom' | 'start' | 'end'",
          "required": false,
          "default": "'top'",
          "description": "Side of the trigger, `top` by default. `start` and `end` follow the writing direction.",
          "values": ["top", "bottom", "start", "end"]
        },
        {
          "name": "delay",
          "type": "number",
          "required": false,
          "default": "500",
          "description": "Milliseconds before opening on hover. Default 500. Focus opens it immediately."
        },
        {
          "name": "closeDelay",
          "type": "number",
          "required": false,
          "default": "0",
          "description": "Milliseconds before closing after the pointer leaves. Default 0."
        },
        {
          "name": "isDisabled",
          "type": "boolean",
          "required": false,
          "description": "Never opens when `true`, for example while the trigger is disabled."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called when the tooltip opens or closes."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the tooltip bubble."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLDivElement>",
          "required": false,
          "description": "Ref to the tooltip bubble."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "cssVariables": [
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-caption",
        "--ds-fs-label",
        "--ds-lh-caption",
        "--ds-lh-label",
        "--ds-primary-bg",
        "--ds-primary-fg",
        "--ds-primary-fg-muted",
        "--ds-radius-sm",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-tooltip-max-width",
        "--ds-weight-medium",
        "--ds-weight-regular"
      ],
      "stories": {
        "title": "Components/Tooltip",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tooltip--default",
            "description": "Keyboard focus opens the tooltip at once; `Escape` dismisses it while the trigger keeps focus.",
            "hasPlay": true
          },
          {
            "name": "Open",
            "id": "components-tooltip--open",
            "description": "Open for the visual review.",
            "hasPlay": false
          },
          {
            "name": "WithShortcut",
            "id": "components-tooltip--with-shortcut",
            "hasPlay": false
          },
          {
            "name": "Bottom",
            "id": "components-tooltip--bottom",
            "hasPlay": false
          },
          {
            "name": "Start",
            "id": "components-tooltip--start",
            "hasPlay": false
          },
          {
            "name": "End",
            "id": "components-tooltip--end",
            "hasPlay": false
          },
          {
            "name": "LongText",
            "id": "components-tooltip--long-text",
            "hasPlay": false
          },
          {
            "name": "OnIconButton",
            "id": "components-tooltip--on-icon-button",
            "description": "An icon-only control already has its name; the tooltip repeats it and can add the shortcut.",
            "hasPlay": false
          },
          {
            "name": "Disabled",
            "id": "components-tooltip--disabled",
            "description": "Never opens while disabled.",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tooltip/Tooltip.md",
        "summary": "A short, ink-style hint for a control. Built on React Aria `TooltipTrigger` and `Tooltip`.\n\n- Handoff source: `components/feedback/Tooltip` (CMP-FBK-04).",
        "whenToUse": [
          "Name or add a shortcut to an icon-only control (\"Add order\", with `N`).",
          "Explain a truncated label, in a few words."
        ],
        "whenNotToUse": [
          "Essential information (an error, a requirement, a price). **A tooltip is never the only source of essential information**: touch users cannot hover, and the text is not shown until the trigger is focused or hovered.",
          "Interactive content (links, buttons, forms): use `Popover`.",
          "A long explanation: put it in the page, or use a `Popover`."
        ],
        "keyboard": [
          "Opens on **keyboard focus and hover**, closes on blur, pointer leave or `Escape` (WCAG 1.4.13: dismissible). The pointer can move onto the tooltip without closing it (hoverable), and it stays until dismissed (persistent)."
        ],
        "examples": [
          "<Tooltip content=\"Add order\" shortcut=\"N\">\n  <IconButton label=\"Add order\" icon={PlusIcon} />\n</Tooltip>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Name or add a shortcut to an icon-only control (\"Add order\", with `N`).\n- Explain a truncated label, in a few words."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- Essential information (an error, a requirement, a price). **A tooltip is never the only source of essential information**: touch users cannot hover, and the text is not shown until the trigger is focused or hovered.\n- Interactive content (links, buttons, forms): use `Popover`.\n- A long explanation: put it in the page, or use a `Popover`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nTooltipTrigger\n  trigger            Button | IconButton | Link   (focusable, React Aria based)\n  Tooltip            role=tooltip, primary background, label type, radius sm\n    content          text\n    shortcut         optional, mono caption, --ds-primary-fg-muted\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                     | Type                                    | Default | Notes                                                           |\n| ------------------------ | --------------------------------------- | ------- | --------------------------------------------------------------- |\n| `children`               | `ReactElement`                          | none    | The trigger. Must be focusable and React Aria based.            |\n| `content`                | `ReactNode`                             | none    | A few words.                                                    |\n| `shortcut`               | `string`                                |         | Mono text after the content (\"Ctrl+K\").                         |\n| `placement`              | `'top' \\| 'bottom' \\| 'start' \\| 'end'` | `top`   | `start`/`end` follow the writing direction; collisions flip it. |\n| `delay`                  | `number`                                | `500`   | ms before a hover opens it. Focus opens it immediately.         |\n| `closeDelay`             | `number`                                | `0`     | ms before it closes after the pointer leaves.                   |\n| `isDisabled`             | `boolean`                               | `false` | Never opens.                                                    |\n| `isOpen` / `defaultOpen` | `boolean`                               |         | Controlled / uncontrolled.                                      |\n| `onOpenChange`           | `(isOpen: boolean) => void`             |         |                                                                 |\n\n`TOOLTIP_DELAY` (500) is exported. The handoff used no delay; React Aria's own default (1500 ms) feels slow for a dense tool."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Role `tooltip`, linked to the trigger with `aria-describedby` while open. The trigger keeps its own accessible name.\n- Opens on **keyboard focus and hover**, closes on blur, pointer leave or `Escape` (WCAG 1.4.13: dismissible). The pointer can move onto the tooltip without closing it (hoverable), and it stays until dismissed (persistent).\n- Placement is collision-aware.\n- Contrast: `--ds-primary-fg` on `--ds-primary-bg`; the shortcut uses `--ds-primary-fg-muted` (at least 4.5:1 on the ink surface).\n- Reduced motion: the 150 ms fade is disabled. Forced colors: a `CanvasText` border is drawn.\n- Layer: `--ds-z-tooltip`, the top of the stack."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Sentence case, no final period for a fragment, no exclamation marks.\n- Do not repeat a visible label; add the shortcut or the missing detail."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<Tooltip content=\"Add order\" shortcut=\"N\">\n  <IconButton label=\"Add order\" icon={PlusIcon} />\n</Tooltip>\n```\n\n`IconButton` also takes a `tooltip` prop that does this for you."
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Built on React Aria: focus and hover, `Escape`, `aria-describedby`, hoverable content and collision-aware placement came for free; the handoff used a `span` with mouse handlers and absolute positioning.\n- All four sides; delay; controlled state; `isDisabled`.\n- No `forceOpen`: use `defaultOpen` or `isOpen`."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Popover",
            "reason": "Interactive content (links, buttons, forms): use `Popover`."
          }
        ],
        "uses": [],
        "usedBy": ["IconButton", "Toggle"]
      }
    },
    {
      "name": "TopBar",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { TopBar } from 'loui-ds';",
      "folder": "src/components/TopBar",
      "sourceFile": "src/components/TopBar/TopBar.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "The page header: a title with an optional subtitle, or a breadcrumb trail, and an actions\nslot. It is a `header` element, `--ds-topbar-h` (64px) high, with a bottom divider. It does\nnot stick by itself; position it in your layout.",
      "props": [
        {
          "name": "title",
          "type": "ReactNode",
          "required": false,
          "description": "The title of the page or section. Rendered as a heading (see `headingLevel`). Ignored\nwhen `breadcrumb` is set: the trail then names the page and the page supplies its own `h1`."
        },
        {
          "name": "subtitle",
          "type": "ReactNode",
          "required": false,
          "description": "One short line under the title (a count, a date, an environment). Not used with `breadcrumb`."
        },
        {
          "name": "headingLevel",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "1",
          "description": "Heading level of the title, `1` by default (one `h1` per page).",
          "values": ["1", "2", "3", "4", "5", "6"]
        },
        {
          "name": "breadcrumb",
          "type": "readonly BreadcrumbItem[]",
          "required": false,
          "description": "A breadcrumb trail shown instead of the title."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "required": false,
          "description": "Free content next to the title, for example a `Badge` or a `SegmentedControl`."
        },
        {
          "name": "actions",
          "type": "ReactNode",
          "required": false,
          "description": "Actions at the end of the bar. Put the single primary action of the screen last, as a\n`Button variant=\"primary\"` with its shortcut as a `Kbd` in `trailing`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 169
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 54
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [
        "--ds-bg",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-meta",
        "--ds-lh-heading",
        "--ds-lh-meta",
        "--ds-line-subtle",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-4",
        "--ds-text",
        "--ds-text-2",
        "--ds-top-bar-padding-inline",
        "--ds-topbar-h",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/TopBar",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-top-bar--default",
            "hasPlay": true
          },
          {
            "name": "WithSubtitle",
            "id": "components-top-bar--with-subtitle",
            "hasPlay": false
          },
          {
            "name": "WithPrimaryAction",
            "id": "components-top-bar--with-primary-action",
            "hasPlay": true
          },
          {
            "name": "WithContent",
            "id": "components-top-bar--with-content",
            "hasPlay": false
          },
          {
            "name": "WithBreadcrumb",
            "id": "components-top-bar--with-breadcrumb",
            "hasPlay": true
          },
          {
            "name": "HeadingLevel2",
            "id": "components-top-bar--heading-level2",
            "hasPlay": true
          },
          {
            "name": "LongTitle",
            "id": "components-top-bar--long-title",
            "hasPlay": false
          },
          {
            "name": "RightToLeft",
            "id": "components-top-bar--right-to-left",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/TopBar/TopBar.md",
        "summary": "The page header: a title with an optional subtitle, or a breadcrumb trail, and a slot for actions. A `header` element, 64px high (`--ds-topbar-h`), with a bottom divider.\n\n- Handoff source: `components/layout/TopBar.jsx` (TODO `CMP-LAY-02`).",
        "whenToUse": [
          "At the top of the content area of an app shell, next to the `Sidebar`.",
          "To hold the single primary action of the screen, at the top right, with its keyboard shortcut."
        ],
        "whenNotToUse": [
          "For the heading of a card or section: use `Card` or a heading.",
          "For a floating or sticky toolbar: position this component in your layout instead."
        ],
        "keyboard": [],
        "examples": [
          "<TopBar\n  title=\"Orders\"\n  subtitle=\"1,248 orders\"\n  actions={\n    <Button variant=\"primary\" trailing={<Kbd keys={['N']} />}>\n      Create order\n    </Button>\n  }\n/>\n\n<TopBar breadcrumb={[{ label: 'Orders', href: '/orders' }, { label: 'ORD-1048' }]} />"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- At the top of the content area of an app shell, next to the `Sidebar`.\n- To hold the single primary action of the screen, at the top right, with its keyboard shortcut."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- For the heading of a card or section: use `Card` or a heading.\n- For a floating or sticky toolbar: position this component in your layout instead."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop           | Type               | Default | Notes                                                                    |\n| -------------- | ------------------ | ------- | ------------------------------------------------------------------------ |\n| `title`        | `ReactNode`        | none    | Rendered as a heading. Ignored when `breadcrumb` is set.                 |\n| `subtitle`     | `ReactNode`        | none    | One short line under the title.                                          |\n| `headingLevel` | `1`-`6`            | `1`     | One `h1` per page; use another level when the bar sits inside a section. |\n| `breadcrumb`   | `BreadcrumbItem[]` | none    | Shown instead of the title. The page then supplies its own `h1`.         |\n| `children`     | `ReactNode`        | none    | Free content next to the title: a `Badge`, a `SegmentedControl`.         |\n| `actions`      | `ReactNode`        | none    | End of the bar. Put the primary action last.                             |\n\n`ref` points to the `<header>`; `className` and other `header` attributes pass through."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- A `header` that is not inside `main`, `section` or `article` is the page's banner landmark. Use at most one banner per page; inside a section it is a plain group.\n- The title is a real heading. A breadcrumb replaces it, so keep a page-level `h1` elsewhere in that case.\n- The title wins the space: content next to it (`children`, such as badges) wraps to a second line before the title truncates, and the bar grows taller. A title truncates with an ellipsis only when it alone is wider than the bar. The actions never shrink or move out of the bar; on narrow screens keep the primary action and put the others in a `Menu`.\n- The primary action is a `Button variant=\"primary\"` with its shortcut as a `Kbd` in `trailing`. Register the shortcut with `useHotkey`; `Kbd` only displays it.\n- Height is a minimum, so a 200% text zoom grows the bar instead of clipping it."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Titles are nouns in sentence case (\"Orders\"). Subtitles give context: a count, a date, an environment.\n- One primary action per screen, a verb plus object: \"Create order\"."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<TopBar\n  title=\"Orders\"\n  subtitle=\"1,248 orders\"\n  actions={\n    <Button variant=\"primary\" trailing={<Kbd keys={['N']} />}>\n      Create order\n    </Button>\n  }\n/>\n\n<TopBar breadcrumb={[{ label: 'Orders', href: '/orders' }, { label: 'ORD-1048' }]} />\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- Height is `--ds-topbar-h` (64px) as a minimum; the handoff code used 56px while the tokens and the ERP spec said 64px.\n- The title is a heading with a level; `subtitle` is new; `breadcrumb` takes `Breadcrumb` items.\n- No `sticky` prop (the handoff hard-coded `z-index: 20`, which is not a layer token). Make it sticky in your layout with a token layer."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Card",
            "reason": "For the heading of a card or section: use `Card` or a heading."
          }
        ],
        "uses": ["Breadcrumb"],
        "usedBy": []
      }
    },
    {
      "name": "Tour",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { Tour } from 'loui-ds';",
      "folder": "src/components/Tour",
      "sourceFile": "src/components/Tour/Tour.tsx",
      "status": "experimental",
      "statusSource": "doc",
      "description": "A step-by-step onboarding tour. Each step is a non-modal popover (React Aria `Popover` with\n`isNonModal`) anchored to an element, with a title, a short text, a \"Step 2 of 5\" counter and\nSkip, Back and Next or Done buttons. The page stays interactive; there is no scrim.\n\nFocus moves into each step as it appears (the step is a named dialog, so a screen reader\nannounces its title). `Escape` skips the tour. When the tour ends, focus returns to the element\nthat had it before the tour opened, normally the button that started it.\n\nUse a `Tooltip` for a hint on one control, and a `Popover` for interactive content. A tour is\nfor a few steps shown once; do not use it to hold documentation.",
      "props": [
        {
          "name": "steps",
          "type": "readonly TourStep[]",
          "required": true,
          "description": "The steps, in order. Each points at one element through `target`."
        },
        {
          "name": "step",
          "type": "number",
          "required": false,
          "description": "Zero-based index of the current step (controlled). Pair with `onStepChange`."
        },
        {
          "name": "defaultStep",
          "type": "number",
          "required": false,
          "default": "0",
          "description": "Initial step when uncontrolled. Default `0`."
        },
        {
          "name": "onStepChange",
          "type": "(step: number) => void",
          "required": false,
          "description": "Called when the user moves to another step."
        },
        {
          "name": "isOpen",
          "type": "boolean",
          "required": false,
          "description": "Controlled open state. Pair with `onOpenChange`."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "required": false,
          "default": "false",
          "description": "Initial open state when uncontrolled."
        },
        {
          "name": "onOpenChange",
          "type": "(isOpen: boolean) => void",
          "required": false,
          "description": "Called with `false` when the tour is finished or skipped."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "required": false,
          "description": "Called when the user presses Done on the last step. `onOpenChange(false)` follows."
        },
        {
          "name": "onSkip",
          "type": "() => void",
          "required": false,
          "description": "Called when the user skips the tour, with `Skip tour` or `Escape`. `onOpenChange(false)` follows."
        },
        {
          "name": "placement",
          "type": "'bottom' | 'bottom left' | 'bottom right' | 'bottom start' | 'bottom end' | 'top' | 'top left' | 'top right' | 'top start' | 'top end' | 'left' | 'left top' | 'left bottom' | 'start' | 'start top' | 'start bottom' | 'right' | 'right top' | 'right bottom' | 'end' | 'end top' | 'end bottom'",
          "required": false,
          "default": "'bottom start'",
          "description": "Side and alignment relative to the target. Default `bottom start`. Flips on collision.",
          "values": [
            "bottom",
            "bottom left",
            "bottom right",
            "bottom start",
            "bottom end",
            "top",
            "top left",
            "top right",
            "top start",
            "top end",
            "left",
            "left top",
            "left bottom",
            "start",
            "start top",
            "start bottom",
            "right",
            "right top",
            "right bottom",
            "end",
            "end top",
            "end bottom"
          ]
        },
        {
          "name": "messages",
          "type": "Partial<{ progress: (current: number, total: number) => string; back: string; next: string; done: string; skip: string; }>",
          "required": false,
          "description": "Per-instance overrides of the built-in messages."
        },
        {
          "name": "className",
          "type": "string",
          "required": false,
          "description": "Class name for the step surface."
        },
        {
          "name": "ref",
          "type": "Ref<HTMLElement>",
          "required": false,
          "description": "Ref to the step surface."
        }
      ],
      "inheritedProps": [],
      "variants": {},
      "messages": {
        "providerKey": "Tour",
        "locales": ["en", "pt-BR"],
        "keys": [
          {
            "key": "progress",
            "kind": "function",
            "default": "Step 2 of 2",
            "signature": "(current: number, total: number)",
            "description": "Progress text of a step, such as \"Step 2 of 5\"."
          },
          {
            "key": "back",
            "kind": "string",
            "default": "Back",
            "description": "Goes to the previous step."
          },
          {
            "key": "next",
            "kind": "string",
            "default": "Next",
            "description": "Goes to the next step."
          },
          {
            "key": "done",
            "kind": "string",
            "default": "Done",
            "description": "Finishes the tour on the last step."
          },
          {
            "key": "skip",
            "kind": "string",
            "default": "Skip tour",
            "description": "Ends the tour before the last step."
          }
        ]
      },
      "cssVariables": [
        "--ds-duration",
        "--ds-ease",
        "--ds-font-mono",
        "--ds-font-sans",
        "--ds-fs-heading",
        "--ds-fs-label",
        "--ds-fs-small",
        "--ds-lh-heading",
        "--ds-lh-label",
        "--ds-lh-small",
        "--ds-line-strong",
        "--ds-radius-lg",
        "--ds-shadow-overlay",
        "--ds-space-1",
        "--ds-space-2",
        "--ds-space-3",
        "--ds-space-4",
        "--ds-surface-raised",
        "--ds-text",
        "--ds-text-2",
        "--ds-text-3",
        "--ds-tour-arrow-size",
        "--ds-tour-padding",
        "--ds-tour-width",
        "--ds-weight-semibold"
      ],
      "stories": {
        "title": "Components/Tour",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "components-tour--default",
            "hasPlay": true
          },
          {
            "name": "FirstStep",
            "id": "components-tour--first-step",
            "description": "Open on the first step for the visual review.",
            "hasPlay": false
          },
          {
            "name": "MiddleStep",
            "id": "components-tour--middle-step",
            "hasPlay": false
          },
          {
            "name": "LastStep",
            "id": "components-tour--last-step",
            "hasPlay": false
          },
          {
            "name": "AboveTarget",
            "id": "components-tour--above-target",
            "hasPlay": false
          },
          {
            "name": "KeyboardOnly",
            "id": "components-tour--keyboard-only",
            "hasPlay": true
          }
        ]
      },
      "docs": {
        "path": "src/components/Tour/Tour.md",
        "summary": "Step-by-step onboarding hints anchored to elements of the page. Each step is a non-modal popover (React Aria `Popover` with `isNonModal`) with a title, a short text, a \"Step 2 of 5\" counter and Skip, Back and Next or Done buttons. Generic: it lives in the core entry point.",
        "whenToUse": [
          "Guiding new users through the interface with a few onboarding hints (three to six steps) that introduce a screen.",
          "Pointing at controls that are not obvious from their label."
        ],
        "whenNotToUse": [
          "A hint on one control: `Tooltip`.",
          "Interactive content anchored to a button (filters, a small form): `Popover`.",
          "Documentation or long explanations: link to help instead.",
          "A task the user must finish before going on: `Dialog` or `Sheet`."
        ],
        "keyboard": [
          "Focus moves into each step as it appears (the dialog itself, so a screen reader reads its title; the next `Tab` reaches Skip, Back and Next). Pressing Next or Back mounts the next step and moves focus to it, so focus never lands on a removed button.",
          "While a step is open, `Tab` and `Shift+Tab` cycle through the step's buttons (React Aria contains focus in the dialog); `Escape` skips the tour and is the keyboard way out. A pointer press elsewhere on the page does not end it, and the page stays visible and readable (no scrim, not inert)."
        ],
        "examples": [
          "const search = useRef<HTMLButtonElement>(null);\nconst filters = useRef<HTMLButtonElement>(null);\nconst [open, setOpen] = useState(false);\n\n<>\n  <Button onPress={() => setOpen(true)}>Start tour</Button>\n  <Button ref={search}>Search</Button>\n  <Button ref={filters}>Filters</Button>\n  <Tour\n    isOpen={open}\n    onOpenChange={setOpen}\n    steps={[\n      { target: search, title: 'Search', content: 'Find any item by name or code.' },\n      { target: filters, title: 'Filters', content: 'Narrow the list by status or date.' },\n    ]}\n  />\n</>;"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- Guiding new users through the interface with a few onboarding hints (three to six steps) that introduce a screen.\n- Pointing at controls that are not obvious from their label."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- A hint on one control: `Tooltip`.\n- Interactive content anchored to a button (filters, a small form): `Popover`.\n- Documentation or long explanations: link to help instead.\n- A task the user must finish before going on: `Dialog` or `Sheet`."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nPopover (non-modal, anchored to step.target, z popover, overlay shadow)\n  OverlayArrow\n  Dialog (role=dialog, named by the title)\n    Heading        step title\n    content        step text (--ds-text-2)\n    footer         \"Step 2 of 5\" (mono) ... Skip tour (ghost) · Back · Next | Done (primary)\n```\n\nThe primary button is Next, or Done on the last step. Skip is hidden on the last step, Back on the first."
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop                                      | Type                                                                   | Default        | Notes                                                                                        |\n| ----------------------------------------- | ---------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------- |\n| `steps`                                   | `{ target: RefObject<Element \\| null>, title, content, placement? }[]` | required       | A step whose target is not mounted renders nothing.                                          |\n| `step` / `defaultStep` / `onStepChange`   | `number`                                                               | `0`            | Zero-based index; controlled / uncontrolled.                                                 |\n| `isOpen` / `defaultOpen` / `onOpenChange` |                                                                        | closed         | Controlled / uncontrolled. `onOpenChange(false)` fires when the tour is finished or skipped. |\n| `onComplete`                              | `() => void`                                                           |                | Done on the last step.                                                                       |\n| `onSkip`                                  | `() => void`                                                           |                | Skip tour or `Escape`.                                                                       |\n| `placement`                               | popover placement                                                      | `bottom start` | Flips on collision; each step may override it.                                               |\n| `messages`                                | `{ progress, back, next, done, skip }`                                 |                | pt-BR included.                                                                              |\n\nWhen the tour ends (finished or skipped) an uncontrolled step index returns to the first step."
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- Each step is a `dialog` named by its title. It is non-modal: the page is not inert, there is no scrim, nothing is hidden from assistive technology.\n- Focus moves into each step as it appears (the dialog itself, so a screen reader reads its title; the next `Tab` reaches Skip, Back and Next). Pressing Next or Back mounts the next step and moves focus to it, so focus never lands on a removed button.\n- While a step is open, `Tab` and `Shift+Tab` cycle through the step's buttons (React Aria contains focus in the dialog); `Escape` skips the tour and is the keyboard way out. A pointer press elsewhere on the page does not end it, and the page stays visible and readable (no scrim, not inert).\n- When the tour ends, focus returns to the element that had focus when the tour opened (normally the button that started it); for a tour that is open on first render, to the target of the current step if it is focusable. If the consumer closes a controlled tour from outside, focus is left where the user put it.\n- The target is scrolled into view (`block: nearest`) when its step appears. The step is positioned against the target and flips when it does not fit; the arrow points at the target.\n- The fade-in is removed under `prefers-reduced-motion`. Visuals are calm: raised surface, strong line and the overlay shadow only; accent is used only for focus rings.\n- Because the step is rendered in a portal, it comes last in the Tab order of the page: the user reaches it by the focus that moves into it, not by tabbing from the target."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Title: the name of the thing, in sentence case (\"Filters\"). Text: one or two sentences saying what it does, with no exclamation marks or emoji.\n- Button labels come from the message catalog: Back, Next, Done, Skip tour."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\nconst search = useRef<HTMLButtonElement>(null);\nconst filters = useRef<HTMLButtonElement>(null);\nconst [open, setOpen] = useState(false);\n\n<>\n  <Button onPress={() => setOpen(true)}>Start tour</Button>\n  <Button ref={search}>Search</Button>\n  <Button ref={filters}>Filters</Button>\n  <Tour\n    isOpen={open}\n    onOpenChange={setOpen}\n    steps={[\n      { target: search, title: 'Search', content: 'Find any item by name or code.' },\n      { target: filters, title: 'Filters', content: 'Narrow the list by status or date.' },\n    ]}\n  />\n</>;\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "New component (GAP-006): the handoff has no onboarding component."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "Tooltip",
            "reason": "A hint on one control: `Tooltip`."
          },
          {
            "name": "Popover",
            "reason": "Interactive content anchored to a button (filters, a small form): `Popover`."
          },
          {
            "name": "Dialog",
            "reason": "A task the user must finish before going on: `Dialog` or `Sheet`."
          },
          {
            "name": "Sheet",
            "reason": "A task the user must finish before going on: `Dialog` or `Sheet`."
          }
        ],
        "uses": ["Button"],
        "usedBy": []
      }
    },
    {
      "name": "VisuallyHidden",
      "kind": "component",
      "entryPoint": "loui-ds",
      "importStatement": "import { VisuallyHidden } from 'loui-ds';",
      "folder": "src/components/VisuallyHidden",
      "sourceFile": "src/components/VisuallyHidden/VisuallyHidden.tsx",
      "status": "stable",
      "statusSource": "doc",
      "description": "Hides content visually while keeping it available to assistive technology.\nUse it for text alternatives that sighted users do not need.",
      "props": [
        {
          "name": "as",
          "type": "ElementType",
          "required": false,
          "default": "span",
          "description": "Element to render. Defaults to `span`."
        }
      ],
      "inheritedProps": [
        {
          "source": "@types/react",
          "interface": "AriaAttributes",
          "domGeneric": true,
          "count": 53
        },
        {
          "source": "@types/react",
          "interface": "Attributes",
          "domGeneric": true,
          "count": 1
        },
        {
          "source": "@types/react",
          "interface": "DOMAttributes",
          "domGeneric": true,
          "count": 170
        },
        {
          "source": "@types/react",
          "interface": "HTMLAttributes",
          "domGeneric": true,
          "count": 55
        },
        {
          "source": "@types/react",
          "interface": "RefAttributes",
          "domGeneric": true,
          "count": 1
        }
      ],
      "variants": {},
      "cssVariables": [],
      "stories": {
        "title": "Primitives/VisuallyHidden",
        "tags": ["autodocs"],
        "items": [
          {
            "name": "Default",
            "id": "primitives-visually-hidden--default",
            "hasPlay": false
          },
          {
            "name": "AsHeading",
            "id": "primitives-visually-hidden--as-heading",
            "hasPlay": false
          }
        ]
      },
      "docs": {
        "path": "src/components/VisuallyHidden/VisuallyHidden.md",
        "summary": "Hides content visually while keeping it in the accessibility tree. It renders a `span` (or the element given by `as`) with the standard \"screen reader only\" clipping, so the text is read aloud and found by assistive technology but takes no space on screen.",
        "whenToUse": [
          "A text alternative for an icon-only control when a `label` prop is not available (for example the name of a custom control).",
          "A heading or landmark label that sighted users do not need because the layout already implies it.",
          "Extra context for a repeated link or button (\"Edit\" followed by a hidden \"order ORD-1048\")."
        ],
        "whenNotToUse": [
          "To hide content from everyone: use the `hidden` attribute or conditional rendering.",
          "To hide content from assistive technology only: use `aria-hidden=\"true\"`.",
          "For a skip link that appears on focus: use `SkipLink`.",
          "For text that sighted keyboard users need to see: render it visibly."
        ],
        "keyboard": [],
        "examples": [
          "<button type=\"button\">\n  <span aria-hidden=\"true\">x</span>\n  <VisuallyHidden>Close dialog</VisuallyHidden>\n</button>\n\n<VisuallyHidden as=\"h2\">Order summary</VisuallyHidden>"
        ],
        "sections": [
          {
            "id": "when-to-use",
            "heading": "When to use",
            "content": "- A text alternative for an icon-only control when a `label` prop is not available (for example the name of a custom control).\n- A heading or landmark label that sighted users do not need because the layout already implies it.\n- Extra context for a repeated link or button (\"Edit\" followed by a hidden \"order ORD-1048\")."
          },
          {
            "id": "when-not-to-use",
            "heading": "When not to use",
            "content": "- To hide content from everyone: use the `hidden` attribute or conditional rendering.\n- To hide content from assistive technology only: use `aria-hidden=\"true\"`.\n- For a skip link that appears on focus: use `SkipLink`.\n- For text that sighted keyboard users need to see: render it visibly."
          },
          {
            "id": "anatomy",
            "heading": "Anatomy",
            "content": "```\nspan.root   (or as=\"h2\", \"div\", ...)\n  clip: rect(0 0 0 0); position: absolute; 1px box; overflow: hidden\n```"
          },
          {
            "id": "props",
            "heading": "Props",
            "content": "| Prop        | Type          | Default  | Notes                                                      |\n| ----------- | ------------- | -------- | ---------------------------------------------------------- |\n| `as`        | `ElementType` | `'span'` | Element to render (`h2`, `div`, `label`, ...).             |\n| `className` | `string`      | -        | Merged with the internal class.                            |\n| `children`  | `ReactNode`   | -        | The text read by assistive technology.                     |\n| `...rest`   | `span` props  | -        | Spread onto the element (`id`, `data-*`, `aria-*`, `ref`). |"
          },
          {
            "id": "accessibility",
            "heading": "Accessibility",
            "content": "- The content stays in the accessibility tree and in the reading order; place it where it should be read.\n- Do not put interactive elements inside: a focusable element that cannot be seen is a focus-visibility failure (WCAG 2.4.7).\n- Write the text as you would write visible copy: sentence case, no emoji, no decorative punctuation."
          },
          {
            "id": "content-rules",
            "heading": "Content rules",
            "content": "- Keep it short and specific (\"Close dialog\", not \"Click here to close the dialog window\").\n- Do not repeat what the visible label already says."
          },
          {
            "id": "examples",
            "heading": "Examples",
            "content": "```tsx\n<button type=\"button\">\n  <span aria-hidden=\"true\">x</span>\n  <VisuallyHidden>Close dialog</VisuallyHidden>\n</button>\n\n<VisuallyHidden as=\"h2\">Order summary</VisuallyHidden>\n```"
          },
          {
            "id": "differences-from-the-handoff",
            "heading": "Differences from the handoff",
            "content": "- New in Loui DS: the handoff had no equivalent; screen-reader-only text was ad hoc."
          }
        ]
      },
      "related": {
        "alternatives": [
          {
            "name": "SkipLink",
            "reason": "For a skip link that appears on focus: use `SkipLink`."
          }
        ],
        "uses": [],
        "usedBy": [
          "ActionList",
          "AddCard",
          "Alert",
          "ApprovalCard",
          "Banner",
          "CardLink",
          "Chart",
          "ChatPanel",
          "CommandPalette",
          "DataTable",
          "FileUpload",
          "Kbd",
          "Legend",
          "Link",
          "MatrixGrid",
          "NavItem",
          "ResponseFeedback",
          "SegmentedControl",
          "ShortcutsHelp",
          "StackedBar",
          "StatusSteps",
          "Stepper",
          "Tab",
          "TabList",
          "TabPanel",
          "Tabs",
          "Textarea",
          "Timeline",
          "ToastRegion",
          "ToolCallStep"
        ]
      }
    }
  ],
  "recipes": [
    {
      "name": "ApprovalQueue",
      "description": "A queue of replies written by an agent that wait for a person to approve them before anything reaches a customer. Tabs split \"To approve\" from \"Sent\"; Send and Discard act at once and offer Undo.",
      "status": "experimental",
      "doc": "recipes/approval-queue.md"
    },
    {
      "name": "CommandPaletteSetup",
      "description": "Keyboard first, wired end to end: one list of destinations and actions feeds the bare-key shortcuts, the command palette (`Mod+K`) and the shortcuts help (`?`).",
      "status": "experimental",
      "doc": "recipes/command-palette-setup.md"
    },
    {
      "name": "CreateOnTheFly",
      "description": "Pick an item from a list, or create it without leaving the field: typing a name that matches nothing ends the list with a `Create \"name\"` row. Choosing it creates the item, selects it and shows a toast with Undo. Built on `Combobox` (`onCreate`, option `leading`) and the `useUndoable` hook.",
      "status": "experimental",
      "doc": "recipes/create-on-the-fly.md"
    },
    {
      "name": "DestructiveConfirm",
      "description": "Delete a record from a row action: ask first in a dialog that names the object and the consequence, then delete and offer Undo in a toast.",
      "status": "experimental",
      "doc": "recipes/destructive-confirm.md"
    },
    {
      "name": "DetailSheet",
      "description": "Open a record next to its list: a row opens a side `Sheet` with the details in editable fields, a footer with the destructive action at the start and Cancel plus one primary action at the end.",
      "status": "experimental",
      "doc": "recipes/detail-sheet.md"
    },
    {
      "name": "FilterableTable",
      "description": "A list of records with search, status filters that show counts, a sortable table, row selection with a bulk action and an empty state that tells the user what to do.",
      "status": "experimental",
      "doc": "recipes/filterable-table.md"
    },
    {
      "name": "FormWithValidation",
      "description": "A form that fails well: native validation with one message per field, an error summary that takes focus, server errors on the same fields, a Save button that always works, and \"Saved\" next to it.",
      "status": "experimental",
      "doc": "recipes/form-with-validation.md"
    },
    {
      "name": "InlinePriceEdit",
      "description": "A price that reads as text and turns into a field when pressed. Enter confirms, Esc cancels and restores the old value, leaving the field confirms. An empty field means \"use the inherited price\", which the placeholder shows.",
      "status": "experimental",
      "doc": "recipes/inline-price-edit.md"
    },
    {
      "name": "MarkdownMessage",
      "description": "Safe markdown in the replies of a `ChatPanel`, through its `renderContent` prop. A small subset is parsed into React elements: no markdown library, no `dangerouslySetInnerHTML`, no HTML string anywhere.",
      "status": "experimental",
      "doc": "recipes/markdown-message.md"
    },
    {
      "name": "PasswordField",
      "description": "A password field with a show/hide toggle, built from `Input` and a labelled `Button` in its `trailing` slot. It satisfies WCAG 3.3.8 (accessible authentication): pasting and password managers keep working, and nothing asks the user to remember or retype a password.",
      "status": "experimental",
      "doc": "recipes/password-field.md"
    },
    {
      "name": "SettingsPage",
      "description": "A settings screen: app shell, a top bar with the page's one primary action and its keyboard shortcut, and one card per topic with switches, a select and a radio group.",
      "status": "experimental",
      "doc": "recipes/settings-page.md"
    },
    {
      "name": "SheetForm",
      "description": "A form inside a side `Sheet` whose primary button sits in the sheet footer, outside the `<form>`. The button is linked to the form with the `form` attribute, so the browser submits and validates natively.",
      "status": "experimental",
      "doc": "recipes/sheet-form.md"
    },
    {
      "name": "UndoableAction",
      "description": "A reversible action (archive, mark as done, change status) that happens at once, with no confirmation, and an Undo in a toast. Built on the `useUndoable` hook.",
      "status": "experimental",
      "doc": "recipes/undoable-action.md"
    }
  ],
  "providers": [
    {
      "name": "DSProvider",
      "kind": "provider",
      "entryPoint": "loui-ds",
      "importStatement": "import { DSProvider } from 'loui-ds';",
      "sourceFile": "src/providers/DSProvider.tsx",
      "status": "experimental",
      "description": "Root provider of Loui DS. Supplies the locale (also to RAC through its\n`I18nProvider`), message overrides and optional client-side routing.",
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "required": true,
          "description": ""
        },
        {
          "name": "locale",
          "type": "string",
          "required": false,
          "default": "en-US",
          "description": "BCP 47 locale for messages, formatting, sorting and RAC components. Default `en-US`."
        },
        {
          "name": "messages",
          "type": "Readonly<Record<string, Readonly<Partial<Record<string, Message>>>>>",
          "required": false,
          "description": "Message overrides keyed by component, applied over the built-in catalogs."
        },
        {
          "name": "navigate",
          "type": "((path: string, routerOptions: undefined) => void)",
          "required": false,
          "description": "Client-side navigation for link components (passed to RAC `RouterProvider`)."
        },
        {
          "name": "useHref",
          "type": "((href: string) => string)",
          "required": false,
          "description": "Maps a router path to a real `href` (passed to RAC `RouterProvider`)."
        }
      ],
      "docs": "docs/guides/getting-started.md"
    }
  ],
  "hooks": [
    {
      "name": "useControllableState",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useControllableState } from 'loui-ds';",
      "sourceFile": "src/hooks/useControllableState.ts",
      "description": "Controlled/uncontrolled state for the `value` / `defaultValue` / `onValueChange`\nconvention (AGENTS.md section 9). Thin wrapper over react-stately's `useControlledState`,\nwhich also warns in development when a component switches between modes.\n\n```tsx\nconst [value, setValue] = useControllableState({ value: props.value, defaultValue: props.defaultValue ?? '', onChange: props.onValueChange });\n```",
      "signature": "useControllableState<T>({ value, defaultValue, onChange, }: UseControllableStateOptions<T>): [T, (value: T) => void]",
      "params": [
        {
          "name": "__0",
          "type": "UseControllableStateOptions<T>",
          "required": true
        }
      ],
      "returns": "[T, (value: T) => void]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useDSLocale",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useDSLocale } from 'loui-ds';",
      "sourceFile": "src/i18n/useMessages.ts",
      "description": "Returns the active locale (BCP 47 tag) set by `DSProvider`; `en-US` without a provider.",
      "signature": "useDSLocale(): string",
      "params": [],
      "returns": "string",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "useHotkey",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useHotkey } from 'loui-ds';",
      "sourceFile": "src/hooks/useHotkey.ts",
      "description": "Runs `handler` when a single key combo is pressed.\n\n- `combo`: modifiers joined with `+` and one key, e.g. `Mod+K`, `Shift+/`, `Alt+N`, `Escape`.\n  `Mod` is Command on macOS and Control elsewhere.\n- Ignores events from editable fields (unless `allowInEditable`), IME composition,\n  key auto-repeat (unless `allowRepeat`) and events another handler already prevented.\n\nThis is deliberately small. A shortcut registry with conflict detection and a help\noverlay is a separate task (GAP-004).",
      "signature": "useHotkey(combo: string, handler: (event: KeyboardEvent) => void, { enabled, scope, allowInEditable, allowRepeat, preventDefault, platform, }?: UseHotkeyOptions): void",
      "params": [
        {
          "name": "combo",
          "type": "string",
          "required": true
        },
        {
          "name": "handler",
          "type": "(event: KeyboardEvent) => void",
          "required": true
        },
        {
          "name": "__2",
          "type": "UseHotkeyOptions",
          "required": false
        }
      ],
      "returns": "void",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useHotkeys",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useHotkeys } from 'loui-ds';",
      "sourceFile": "src/hooks/useHotkeys.ts",
      "description": "Registers keyboard shortcuts in the registry (see `HotkeyProvider`) for as long as the\ncomponent is mounted.\n\n- `keys`: `Mod+K`, `F2`, `N`, `1`, `Shift+/`, `Escape`. `Mod` is Command on macOS and Control\n  elsewhere.\n- Ignores editable fields (`allowInEditable`), IME composition, auto-repeat (`allowRepeat`),\n  events another handler already prevented and, for page-level shortcuts, focus inside an open\n  dialog or menu (`allowInOverlay`).\n- Only one named scope is active at a time (`useHotkeyScope`); shortcuts of other scopes wait.\n- In development, registering the same combo twice in one scope logs a warning; the most\n  recent registration wins.\n- `description` and `group` make a shortcut appear in `useRegisteredHotkeys`, the\n  command palette and `ShortcutsHelp`.\n\nHandlers may close over props and state: the latest handler always runs. The array can be\ncreated inline on each render.",
      "signature": "useHotkeys(definitions: readonly HotkeyDefinition[]): void",
      "params": [
        {
          "name": "definitions",
          "type": "readonly HotkeyDefinition[]",
          "required": true
        }
      ],
      "returns": "void",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useHotkeyScope",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useHotkeyScope } from 'loui-ds';",
      "sourceFile": "src/hooks/useHotkeys.ts",
      "description": "Makes `name` the active scope while the component is mounted (and `isActive`): shortcuts\nregistered with `scope: name` fire and `global` ones stop, until the component unmounts.\nScopes stack: the most recently mounted one wins. Use it in the body of a panel, a wizard or\nan editor that has its own shortcuts. Dialogs do not need it for page shortcuts: those are\nalready ignored while focus is inside an open dialog.",
      "signature": "useHotkeyScope(name: string, { isActive }?: UseHotkeyScopeOptions): void",
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true
        },
        {
          "name": "__1",
          "type": "UseHotkeyScopeOptions",
          "required": false
        }
      ],
      "returns": "void",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useMediaQuery",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useMediaQuery } from 'loui-ds';",
      "sourceFile": "src/hooks/useMediaQuery.ts",
      "description": "Subscribes to a CSS media query. SSR-safe: on the server (and during hydration)\nit returns `defaultValue` (`false` unless given), then the real value after hydration.\n\nPrefer CSS media queries for styling; use this only when behavior (not appearance) depends on the query.",
      "signature": "useMediaQuery(query: string, defaultValue?: boolean): boolean",
      "params": [
        {
          "name": "query",
          "type": "string",
          "required": true
        },
        {
          "name": "defaultValue",
          "type": "boolean",
          "required": false
        }
      ],
      "returns": "boolean",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useMessages",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useMessages } from 'loui-ds';",
      "sourceFile": "src/i18n/useMessages.ts",
      "description": "Returns a component's messages for the active locale.",
      "signature": "useMessages<M extends MessageSet>(componentKey: string, catalog: MessageCatalog<M>, overrides?: MessageOverrides): M",
      "params": [
        {
          "name": "componentKey",
          "type": "string",
          "required": true
        },
        {
          "name": "catalog",
          "type": "MessageCatalog<M>",
          "required": true
        },
        {
          "name": "overrides",
          "type": "Readonly<Partial<Record<string, Message>>> | undefined",
          "required": false
        }
      ],
      "returns": "M",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "usePrefersReducedMotion",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { usePrefersReducedMotion } from 'loui-ds';",
      "sourceFile": "src/hooks/usePrefersReducedMotion.ts",
      "description": "True when the user asks the OS for reduced motion (`prefers-reduced-motion: reduce`).",
      "signature": "usePrefersReducedMotion(): boolean",
      "params": [],
      "returns": "boolean",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useRegisteredHotkeys",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useRegisteredHotkeys } from 'loui-ds';",
      "sourceFile": "src/hooks/useHotkeys.ts",
      "description": "The shortcuts that have a `description`, in registration order, for a help dialog or a\ncommand list. By default only those that can fire now (active scope). Updates when\nshortcuts mount, unmount or the active scope changes. Empty during server rendering.",
      "signature": "useRegisteredHotkeys({ includeInactive, }?: UseRegisteredHotkeysOptions): readonly RegisteredHotkey[]",
      "params": [
        {
          "name": "__0",
          "type": "UseRegisteredHotkeysOptions",
          "required": false
        }
      ],
      "returns": "readonly RegisteredHotkey[]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useToast",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useToast } from 'loui-ds';",
      "sourceFile": "src/components/Toast/queue.ts",
      "description": "Hook form of `toast`, for components that prefer a hook. It returns the same stable object,\nso it is safe in dependency arrays.",
      "signature": "useToast(): ToastApi",
      "params": [],
      "returns": "ToastApi",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "useUndoable",
      "kind": "hook",
      "entryPoint": "loui-ds",
      "importStatement": "import { useUndoable } from 'loui-ds';",
      "sourceFile": "src/hooks/useUndoable.ts",
      "description": "Undo, don't confirm (AGENTS.md section 5): applies a reversible action immediately, shows a\ntoast with an **Undo** button, then either reverts it (Undo) or commits it (the toast times\nout or is dismissed).\n\n- `apply` runs now; the screen updates at once.\n- Undo calls `revert`. Closing or timing out calls the optional async `commit`; if that\n  fails, the change is reverted and an error toast stays until dismissed.\n- The timer lives in the toast, so it pauses on hover and focus (WCAG 2.2.1) and the commit\n  waits for it. A pending commit still runs if the component that started it unmounts.\n- Mount one `ToastRegion` near the app root.\n\nUse it for reversible actions (archive, mark as done, change status). Deleting still asks\nfor a confirmation first (`ConfirmDialog`); the Undo toast follows it.\n\nDeferring the commit means a closed tab loses a pending change. When that matters, persist in\n`apply` and make `revert` send the inverse change instead (omit `commit`).",
      "signature": "useUndoable({ toast, messages: overrides, }?: UseUndoableOptions): UseUndoableResult",
      "params": [
        {
          "name": "__0",
          "type": "UseUndoableOptions",
          "required": false
        }
      ],
      "returns": "UseUndoableResult",
      "docs": "docs/architecture/utilities.md"
    }
  ],
  "utilities": [
    {
      "name": "chartAverage",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { chartAverage } from 'loui-ds';",
      "sourceFile": "src/components/Chart/chartAverage.ts",
      "description": "The arithmetic mean of chart values, for an `average` reference line:\n`referenceLines={[{ kind: 'average', label: 'Average', value: chartAverage(data) }]}`.\n\nAccepts numbers or chart data (`{ value }`). Non-finite entries are ignored; with nothing\nleft to average (or the sum overflows) the result is `0`. The result is never `NaN`.",
      "signature": "chartAverage(values: readonly (number | { readonly value: number; })[]): number",
      "params": [
        {
          "name": "values",
          "type": "readonly (number | { readonly value: number; })[]",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "compareText",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { compareText } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "Compares two strings alphabetically for `locale`; usable as an `Array.prototype.sort` comparator.",
      "signature": "compareText(a: string, b: string, locale: string): number",
      "params": [
        {
          "name": "a",
          "type": "string",
          "required": true
        },
        {
          "name": "b",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "contrastRatio",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { contrastRatio } from 'loui-ds';",
      "sourceFile": "src/utils/color.ts",
      "description": "WCAG contrast ratio between two hex colors, from `1` (identical) to `21` (black on white). Order does not matter.",
      "signature": "contrastRatio(a: string, b: string): number",
      "params": [
        {
          "name": "a",
          "type": "string",
          "required": true
        },
        {
          "name": "b",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "createTextCollator",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { createTextCollator } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "Collator for alphabetical order as the product requires (AGENTS.md section 5, rule 5b):\naccent- and case-insensitive (`sensitivity: 'base'`) with natural number order\n(`numeric: true`, so `Item 2` sorts before `Item 10`). The instance is shared\nand cached per locale; do not mutate it.",
      "signature": "createTextCollator(locale: string): Intl.Collator",
      "params": [
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "Intl.Collator",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "createToastApi",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { createToastApi } from 'loui-ds';",
      "sourceFile": "src/components/Toast/queue.ts",
      "description": "Builds the `toast()` function for a queue.",
      "signature": "createToastApi(queue: ToastQueue): ToastApi",
      "params": [
        {
          "name": "queue",
          "type": "ToastQueue",
          "required": true
        }
      ],
      "returns": "ToastApi",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "createToastQueue",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { createToastQueue } from 'loui-ds';",
      "sourceFile": "src/components/Toast/queue.ts",
      "description": "Creates an independent queue, for tests or for a second region. Most apps use `toast`.",
      "signature": "createToastQueue(): ToastQueue",
      "params": [],
      "returns": "ToastQueue",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "daysBetween",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { daysBetween } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Whole days from `isoA` to `isoB` (positive when `isoB` is later). Uses UTC\ndate math, so daylight-saving changes cannot produce 23- or 25-hour \"days\".\nInvalid input throws in development and returns `NaN` in production.",
      "signature": "daysBetween(isoA: ISODate, isoB: ISODate): number",
      "params": [
        {
          "name": "isoA",
          "type": "string",
          "required": true
        },
        {
          "name": "isoB",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "DEFAULT_LOCALE",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { DEFAULT_LOCALE } from 'loui-ds';",
      "sourceFile": "src/i18n/locales.ts",
      "description": "Default locale.",
      "signature": "\"en-US\" | \"pt-BR\"",
      "params": [],
      "returns": "\"en-US\" | \"pt-BR\"",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "DEFAULT_TOAST_TIMEOUT",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { DEFAULT_TOAST_TIMEOUT } from 'loui-ds';",
      "sourceFile": "src/components/Toast/queue.ts",
      "description": "Default time a toast stays on screen, in milliseconds. The handoff used 3.6 s, which is too\nshort to read a sentence and press Undo (WCAG 2.2.1 Timing Adjustable); 5 s plus pause on\nhover and focus lets keyboard and slow readers finish.",
      "signature": "5000",
      "params": [],
      "returns": "5000",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "defineMessages",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { defineMessages } from 'loui-ds';",
      "sourceFile": "src/i18n/messages.ts",
      "description": "Declares a component's catalog. Identity function that type-checks that\n`pt-BR` has exactly the same keys and signatures as `en`.",
      "signature": "defineMessages<M extends MessageSet>(catalog: { readonly en: M; readonly \"pt-BR\": NoInfer<M>; readonly [locale: string]: NoInfer<M> | undefined; }): MessageCatalog<M>",
      "params": [
        {
          "name": "catalog",
          "type": "{ readonly [locale: string]: NoInfer<M> | undefined; readonly en: M; readonly 'pt-BR': NoInfer<M>; }",
          "required": true
        }
      ],
      "returns": "MessageCatalog<M>",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "dueStatus",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { dueStatus } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Urgency of a due date: overdue (`< 0` days) is `danger`, due within a week\n(`0` to `7`) is `warning`, later is `neutral`. The visible text always comes\nfrom messages (color is never the only signal, AGENTS.md section 5).",
      "signature": "dueStatus(dueISO: ISODate, todayISO: ISODate): DueStatus",
      "params": [
        {
          "name": "dueISO",
          "type": "string",
          "required": true
        },
        {
          "name": "todayISO",
          "type": "string",
          "required": true
        }
      ],
      "returns": "DueStatus",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "formatCurrency",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { formatCurrency } from 'loui-ds';",
      "sourceFile": "src/utils/currency.ts",
      "description": "Formats money held as an integer in minor units (ADR-0006):\n`formatCurrency(123456, 'USD', 'en-US')` is `$1,234.56`.\n\nThe amount is passed to `Intl` as an exact decimal string, so there is no\nfloating-point drift. Non-integer or unsafe amounts throw in development\n(a sign of a unit bug at the call site) and are rounded in production.",
      "signature": "formatCurrency(minorUnits: number, currency: string, locale: string, options?: FormatCurrencyOptions): string",
      "params": [
        {
          "name": "minorUnits",
          "type": "number",
          "required": true
        },
        {
          "name": "currency",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        },
        {
          "name": "options",
          "type": "FormatCurrencyOptions",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "formatDate",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { formatDate } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Formats an ISO date for `locale`. Formatting happens in UTC, so the day never\nshifts with the viewer's time zone. Invalid input throws in development and\nis returned unchanged in production.\n\n`formatDate('2026-10-31', 'en-US', 'medium')` is `Oct 31, 2026`.",
      "signature": "formatDate(iso: ISODate, locale: string, style?: DateStyle): string",
      "params": [
        {
          "name": "iso",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        },
        {
          "name": "style",
          "type": "DateStyle",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "formatNumber",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { formatNumber } from 'loui-ds';",
      "sourceFile": "src/utils/number.ts",
      "description": "Formats a decimal number for `locale` (`1234.5` becomes `1,234.5` in `en-US`).",
      "signature": "formatNumber(value: number, locale: string, options?: FormatNumberOptions): string",
      "params": [
        {
          "name": "value",
          "type": "number",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        },
        {
          "name": "options",
          "type": "FormatNumberOptions | undefined",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "formatPercent",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { formatPercent } from 'loui-ds';",
      "sourceFile": "src/utils/number.ts",
      "description": "Formats a ratio as a percentage: `0.256` becomes `26%` in `en-US`.\nPass `maximumFractionDigits` for more precision.",
      "signature": "formatPercent(ratio: number, locale: string, options?: Omit<FormatNumberOptions, \"unit\" | \"unitDisplay\">): string",
      "params": [
        {
          "name": "ratio",
          "type": "number",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        },
        {
          "name": "options",
          "type": "Omit<FormatNumberOptions, \"unit\" | \"unitDisplay\"> | undefined",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "formatRelativeDays",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { formatRelativeDays } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Relative day label from `Intl.RelativeTimeFormat` with `numeric: 'auto'`:\n`-1` is `yesterday`, `0` is `today`, `1` is `tomorrow`, `3` is `in 3 days`.\n`days` must be an integer (throws in development, rounded in production).",
      "signature": "formatRelativeDays(days: number, locale: string): string",
      "params": [
        {
          "name": "days",
          "type": "number",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getCurrencyDigits",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { getCurrencyDigits } from 'loui-ds';",
      "sourceFile": "src/utils/currency.ts",
      "description": "Number of minor-unit digits of a currency (2 for USD/BRL, 0 for JPY, 3 for KWD),\nread from `Intl` so we never keep our own table.",
      "signature": "getCurrencyDigits(currency: string, locale: string): number",
      "params": [
        {
          "name": "currency",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getInitials",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { getInitials } from 'loui-ds';",
      "sourceFile": "src/components/Avatar/getInitials.ts",
      "description": "Initials of a person's name: the first letter of the first and of the last word\n(`Ana Maria da Silva` gives `AS`), or one letter for a single word. Uses `Intl.Segmenter`,\nso combined characters stay whole, and upper-cases with the locale rules (`i` in `tr`).",
      "signature": "getInitials(name: string, locale: string): string",
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getItemRange",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { getItemRange } from 'loui-ds';",
      "sourceFile": "src/components/Pagination/pageItems.ts",
      "description": "The 1-based range of items on `page`, or `null` when there are no items.",
      "signature": "getItemRange(page: number, pageSize: number, total: number): { from: number; to: number; } | null",
      "params": [
        {
          "name": "page",
          "type": "number",
          "required": true
        },
        {
          "name": "pageSize",
          "type": "number",
          "required": true
        },
        {
          "name": "total",
          "type": "number",
          "required": true
        }
      ],
      "returns": "{ from: number; to: number; } | null",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getPageCount",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { getPageCount } from 'loui-ds';",
      "sourceFile": "src/components/Pagination/pageItems.ts",
      "description": "Number of pages for `total` items shown `pageSize` at a time (at least 1).",
      "signature": "getPageCount(total: number, pageSize: number): number",
      "params": [
        {
          "name": "total",
          "type": "number",
          "required": true
        },
        {
          "name": "pageSize",
          "type": "number",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getPageItems",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { getPageItems } from 'loui-ds';",
      "sourceFile": "src/components/Pagination/pageItems.ts",
      "description": "Which page buttons to show, with ellipses for the pages that are left out.\n\nUp to 7 pages all of them are listed. Beyond that the first page, the last page, the current\npage and its two neighbours are listed, and a gap between two of them becomes an ellipsis.\nA gap that hides exactly one page shows that page instead (an ellipsis for a single page\nwould be wider than the page it hides). `current` is clamped into `1..pageCount`; a\n`pageCount` below 1 is treated as 1.\n\n```\ngetPageItems(1, 10)  // 1 2 3 4 5 6 7 8 9 10 collapses to: 1 2 … 10\ngetPageItems(5, 10)  // 1 … 4 5 6 … 10\n```",
      "signature": "getPageItems(current: number, pageCount: number): PageItem[]",
      "params": [
        {
          "name": "current",
          "type": "number",
          "required": true
        },
        {
          "name": "pageCount",
          "type": "number",
          "required": true
        }
      ],
      "returns": "PageItem[]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "getSourceElementId",
      "kind": "utility",
      "entryPoint": "loui-ds/patterns",
      "importStatement": "import { getSourceElementId } from 'loui-ds/patterns';",
      "sourceFile": "src/patterns/SourceList/SourceList.tsx",
      "description": "The DOM id of one source of a list. `Citation` builds the same id from `listId` and `sourceId`.",
      "signature": "getSourceElementId(listId: string, sourceId: string): string",
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true
        },
        {
          "name": "sourceId",
          "type": "string",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "highlightMatch",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { highlightMatch } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "Splits `text` into matched and unmatched segments for highlighting, ignoring case\nand accents. The segments always concatenate back to `text` exactly, and a match\nkeeps any combining marks attached to its last letter. All occurrences are returned.\n\n`highlightMatch('Crème brûlée', 'creme')` gives `[{ text: 'Crème', match: true }, { text: ' brûlée', match: false }]`.",
      "signature": "highlightMatch(text: string, query: string): HighlightSegment[]",
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true
        },
        {
          "name": "query",
          "type": "string",
          "required": true
        }
      ],
      "returns": "HighlightSegment[]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "hotkeyKeys",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { hotkeyKeys } from 'loui-ds';",
      "sourceFile": "src/hooks/hotkey.ts",
      "description": "The parts of a combo in display order, ready for `Kbd`'s `keys`: `'Mod+K'` gives\n`['Mod', 'K']`, `'Shift+/'` gives `['Shift', '/']`. Returns `[]` for a malformed combo (never throws).",
      "signature": "hotkeyKeys(combo: string): string[]",
      "params": [
        {
          "name": "combo",
          "type": "string",
          "required": true
        }
      ],
      "returns": "string[]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "HotkeyProvider",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { HotkeyProvider } from 'loui-ds';",
      "sourceFile": "src/hooks/HotkeyProvider.tsx",
      "description": "Optional root provider for the shortcut registry. Without it `useHotkeys` uses one shared\nregistry for the whole page, which is enough for most apps; add the provider to isolate the\nshortcuts of a subtree (micro frontends, tests, Storybook) or to pin the platform.\n\nPlace it inside `DSProvider` and above the `CommandPalette` and `ShortcutsHelp` it feeds.",
      "signature": "HotkeyProvider({ children, platform }: HotkeyProviderProps): React.JSX.Element",
      "params": [
        {
          "name": "__0",
          "type": "HotkeyProviderProps",
          "required": true
        }
      ],
      "returns": "React.JSX.Element",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "HotkeyRegistry",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { HotkeyRegistry } from 'loui-ds';",
      "sourceFile": "src/hooks/hotkeyRegistry.ts",
      "description": "The store behind `HotkeyProvider`, `useHotkeys` and `useRegisteredHotkeys`. One `keydown`\nlistener on `document` is attached while at least one shortcut is registered. Framework\nindependent so it can be tested without React.",
      "signature": "typeof HotkeyRegistry",
      "params": [],
      "returns": "typeof HotkeyRegistry",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "iconNames",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { iconNames } from 'loui-ds';",
      "sourceFile": "src/components/Icon/icons.ts",
      "description": "Kebab-case names of the curated set with the use each icon is meant for. Source of the\nallow-list in the component manifest; keep it in sync with the exports above (tested).",
      "signature": "{ readonly 'arrow-down': \"Descending sort, move down\"; readonly 'arrow-left': \"Back, previous page\"; readonly 'arrow-right': \"Forward, next step\"; readonly 'arrow-up': \"Ascending sort, move up\"; readonly archive: \"Archive an item (reversible, with Undo); not for delete\"; readonly bell: \"Notifications and alerts entry point\"; readonly bot: \"An AI agent as an actor, for example an agent avatar or a bot-authored message\"; readonly calendar: \"Date fields and date pickers\"; readonly check: \"Done, selected, checkbox mark\"; readonly 'chevron-down': \"Collapsed disclosure, select trigger, menu button\"; readonly 'chevron-left': \"Previous month or page (use the directional prop so it mirrors in RTL)\"; readonly 'chevron-right': \"Next month or page, submenu, breadcrumb separator (directional)\"; readonly 'chevron-up': \"Expanded disclosure\"; readonly 'chevrons-up-down': \"Sortable column, select trigger with many options\"; readonly 'circle-alert': \"Error and danger status\"; readonly 'circle-check': \"Success status\"; readonly 'circle-x': \"Failed status, clear input\"; readonly ellipsis: \"More actions (overflow menu)\"; readonly 'external-link': \"Link that opens in a new tab\"; readonly filter: \"Filter a list or table (opens filter controls)\"; readonly info: \"Neutral information status\"; readonly link: \"Copy or insert a link, linked records\"; readonly loader: \"Loading glyph (static); animated loading uses Spinner\"; readonly message: \"Messages, chat and comments, for example a conversational agent\"; readonly menu: \"Opens the navigation menu (hamburger), for example the app shell nav toggle\"; readonly minus: \"Remove one, indeterminate checkbox\"; readonly pencil: \"Edit an item or a field in place\"; readonly plus: \"Add, create\"; readonly printer: \"Print a document or a receipt\"; readonly search: \"Search fields\"; readonly sparkles: \"AI-generated content or an AI-assisted action (suggest, generate)\"; readonly star: \"Rating, favorite\"; readonly 'triangle-alert': \"Warning status\"; readonly copy: \"Copy to the clipboard\"; readonly refresh: \"Regenerate, reload, sync again\"; readonly 'thumbs-down': \"Negative feedback on a response\"; readonly 'thumbs-up': \"Positive feedback on a response\"; readonly trash: \"Delete an item; always paired with a confirmation dialog\"; readonly users: \"A group of people, for example team, members or assignees\"; readonly x: \"Close, dismiss, remove\"; }",
      "params": [],
      "returns": "{ readonly 'arrow-down': \"Descending sort, move down\"; readonly 'arrow-left': \"Back, previous page\"; readonly 'arrow-right': \"Forward, next step\"; readonly 'arrow-up': \"Ascending sort, move up\"; readonly archive: \"Archive an item (reversible, with Undo); not for delete\"; readonly bell: \"Notifications and alerts entry point\"; readonly bot: \"An AI agent as an actor, for example an agent avatar or a bot-authored message\"; readonly calendar: \"Date fields and date pickers\"; readonly check: \"Done, selected, checkbox mark\"; readonly 'chevron-down': \"Collapsed disclosure, select trigger, menu button\"; readonly 'chevron-left': \"Previous month or page (use the directional prop so it mirrors in RTL)\"; readonly 'chevron-right': \"Next month or page, submenu, breadcrumb separator (directional)\"; readonly 'chevron-up': \"Expanded disclosure\"; readonly 'chevrons-up-down': \"Sortable column, select trigger with many options\"; readonly 'circle-alert': \"Error and danger status\"; readonly 'circle-check': \"Success status\"; readonly 'circle-x': \"Failed status, clear input\"; readonly ellipsis: \"More actions (overflow menu)\"; readonly 'external-link': \"Link that opens in a new tab\"; readonly filter: \"Filter a list or table (opens filter controls)\"; readonly info: \"Neutral information status\"; readonly link: \"Copy or insert a link, linked records\"; readonly loader: \"Loading glyph (static); animated loading uses Spinner\"; readonly message: \"Messages, chat and comments, for example a conversational agent\"; readonly menu: \"Opens the navigation menu (hamburger), for example the app shell nav toggle\"; readonly minus: \"Remove one, indeterminate checkbox\"; readonly pencil: \"Edit an item or a field in place\"; readonly plus: \"Add, create\"; readonly printer: \"Print a document or a receipt\"; readonly search: \"Search fields\"; readonly sparkles: \"AI-generated content or an AI-assisted action (suggest, generate)\"; readonly star: \"Rating, favorite\"; readonly 'triangle-alert': \"Warning status\"; readonly copy: \"Copy to the clipboard\"; readonly refresh: \"Regenerate, reload, sync again\"; readonly 'thumbs-down': \"Negative feedback on a response\"; readonly 'thumbs-up': \"Positive feedback on a response\"; readonly trash: \"Delete an item; always paired with a confirmation dialog\"; readonly users: \"A group of people, for example team, members or assignees\"; readonly x: \"Close, dismiss, remove\"; }",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "iconSet",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { iconSet } from 'loui-ds';",
      "sourceFile": "src/components/Icon/icons.ts",
      "description": "The curated set by kebab-case name, for tooling and name-driven rendering.",
      "signature": "{ readonly 'arrow-down': LucideIcon; readonly 'arrow-left': LucideIcon; readonly 'arrow-right': LucideIcon; readonly 'arrow-up': LucideIcon; readonly archive: LucideIcon; readonly bell: LucideIcon; readonly bot: LucideIcon; readonly calendar: LucideIcon; readonly check: LucideIcon; readonly 'chevron-down': LucideIcon; readonly 'chevron-left': LucideIcon; readonly 'chevron-right': LucideIcon; readonly 'chevron-up': LucideIcon; readonly 'chevrons-up-down': LucideIcon; readonly 'circle-alert': LucideIcon; readonly 'circle-check': LucideIcon; readonly 'circle-x': LucideIcon; readonly ellipsis: LucideIcon; readonly 'external-link': LucideIcon; readonly filter: LucideIcon; readonly info: LucideIcon; readonly link: LucideIcon; readonly loader: LucideIcon; readonly menu: LucideIcon; readonly message: LucideIcon; readonly minus: LucideIcon; readonly pencil: LucideIcon; readonly plus: LucideIcon; readonly printer: LucideIcon; readonly search: LucideIcon; readonly sparkles: LucideIcon; readonly star: LucideIcon; readonly 'triangle-alert': LucideIcon; readonly copy: LucideIcon; readonly refresh: LucideIcon; readonly 'thumbs-down': LucideIcon; readonly 'thumbs-up': LucideIcon; readonly trash: LucideIcon; readonly users: LucideIcon; readonly x: LucideIcon; }",
      "params": [],
      "returns": "{ readonly 'arrow-down': LucideIcon; readonly 'arrow-left': LucideIcon; readonly 'arrow-right': LucideIcon; readonly 'arrow-up': LucideIcon; readonly archive: LucideIcon; readonly bell: LucideIcon; readonly bot: LucideIcon; readonly calendar: LucideIcon; readonly check: LucideIcon; readonly 'chevron-down': LucideIcon; readonly 'chevron-left': LucideIcon; readonly 'chevron-right': LucideIcon; readonly 'chevron-up': LucideIcon; readonly 'chevrons-up-down': LucideIcon; readonly 'circle-alert': LucideIcon; readonly 'circle-check': LucideIcon; readonly 'circle-x': LucideIcon; readonly ellipsis: LucideIcon; readonly 'external-link': LucideIcon; readonly filter: LucideIcon; readonly info: LucideIcon; readonly link: LucideIcon; readonly loader: LucideIcon; readonly menu: LucideIcon; readonly message: LucideIcon; readonly minus: LucideIcon; readonly pencil: LucideIcon; readonly plus: LucideIcon; readonly printer: LucideIcon; readonly search: LucideIcon; readonly sparkles: LucideIcon; readonly star: LucideIcon; readonly 'triangle-alert': LucideIcon; readonly copy: LucideIcon; readonly refresh: LucideIcon; readonly 'thumbs-down': LucideIcon; readonly 'thumbs-up': LucideIcon; readonly trash: LucideIcon; readonly users: LucideIcon; readonly x: LucideIcon; }",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "isHexColor",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { isHexColor } from 'loui-ds';",
      "sourceFile": "src/components/Swatch/hex.ts",
      "description": "True for `#rgb` and `#rrggbb` (the `#` is required). Accepts model data typed\n`string | null | undefined`; `null`, `undefined`, `transparent`, a CSS name and an empty string are\nfalse. Deliberately a plain boolean, not a type predicate: a string that fails the check is still a\nstring.",
      "signature": "isHexColor(value: string | null | undefined): boolean",
      "params": [
        {
          "name": "value",
          "type": "string | null | undefined",
          "required": true
        }
      ],
      "returns": "boolean",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "isISODate",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { isISODate } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "True if `value` is a real calendar date written as `YYYY-MM-DD`.",
      "signature": "isISODate(value: string): value is ISODate",
      "params": [
        {
          "name": "value",
          "type": "string",
          "required": true
        }
      ],
      "returns": "boolean",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "matchesSearch",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { matchesSearch } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "True when `text` contains `query`, ignoring case and accents. An empty query matches everything.",
      "signature": "matchesSearch(text: string, query: string): boolean",
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true
        },
        {
          "name": "query",
          "type": "string",
          "required": true
        }
      ],
      "returns": "boolean",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "niceScale",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { niceScale } from 'loui-ds';",
      "sourceFile": "src/components/Chart/niceScale.ts",
      "description": "Computes a \"nice\" value axis for data between `minValue` and `maxValue`.\n\nThe axis always includes zero (bars grow from it), so an all-positive data set starts at 0,\nan all-negative one ends at 0, and mixed data spans both sides. The step is a round number\n(1, 2, 2.5, 5 or 10 times a power of ten) and the ends are multiples of it, aiming for about\n`tickCount` ticks. It handles zero data (axis 0 to 1), tiny values (`0.00003`), huge values\nand non-finite input (treated as 0).\n\n```ts\nniceScale(0, 42).ticks; // [0, 10, 20, 30, 40, 50]\nniceScale(-3, 8).ticks; // [-5, -2.5, 0, 2.5, 5, 7.5, 10]\n```",
      "signature": "niceScale(minValue: number, maxValue: number, tickCount?: number): NiceScale",
      "params": [
        {
          "name": "minValue",
          "type": "number",
          "required": true
        },
        {
          "name": "maxValue",
          "type": "number",
          "required": true
        },
        {
          "name": "tickCount",
          "type": "number",
          "required": false
        }
      ],
      "returns": "NiceScale",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "normalizeSearch",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { normalizeSearch } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "Normalizes text for accent- and case-insensitive search: `Café` and `cafe` both\nbecome `cafe`. Works per code point so it stays index-aligned with `highlightMatch`.",
      "signature": "normalizeSearch(text: string): string",
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "parseCurrencyAmount",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { parseCurrencyAmount } from 'loui-ds';",
      "sourceFile": "src/utils/currency.ts",
      "description": "Parses what the user typed in **whole units** into integer minor units (the `amount` typing\nmode of `CurrencyInput`): `200` is 200.00, so `20000` in BRL; `12,5` is `1250`.\n\nRules, applied with string manipulation only (no floats):\n- Digits are whole units until a decimal mark; the locale's own digits and full-width digits\n  count. Other characters (symbols, spaces, signs, letters) are ignored.\n- The **last** `.` or `,` (or the locale's decimal mark) starts the fraction when it is the\n  only one of its kind and either it is the locale's decimal mark, or it is followed by at\n  most as many digits as the currency has minor digits (and is not the locale's grouping\n  mark followed by exactly three digits, as in `1.234` for KWD in pt-BR). Otherwise it is\n  grouping and ignored: `1.234,56` and `1,234.56` are both 1234.56; `1,234` is 1234 in USD.\n- Fraction digits beyond the currency's minor digits are cut (not rounded). Currencies with\n  0 minor digits (JPY) have no fraction: every mark is ignored.\n- A missing fraction is padded with zeros. Text without digits is `0`. More than 15 digits\n  (leading zeros excluded) returns `NaN`, never a shortened number; any other result is a safe\n  integer. The sign is not handled here.",
      "signature": "parseCurrencyAmount(text: string, currency: string, locale: string): number",
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true
        },
        {
          "name": "currency",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "parseCurrencyInput",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { parseCurrencyInput } from 'loui-ds';",
      "sourceFile": "src/utils/currency.ts",
      "description": "Parses what the user typed in a currency field into integer minor units, using\nthe digits-only typing model: every digit shifts in from the right, and any\nother character (separators, symbols, spaces, a sign) is ignored. Typing\n`1`, `2`, `3` into a USD field yields `1`, `12`, `123`, shown as `$0.01`,\n`$0.12`, `$1.23`. Digits of the locale's numbering system (e.g. Arabic-Indic)\nand full-width digits count as digits. Empty input is `0`. Input longer than\n15 digits (leading zeros do not count) returns `NaN` rather than a silently shorter number,\nso a result that is not `NaN` is always a safe integer.\n\n`currency` is validated through `Intl` (throws `RangeError` for an invalid code).\nPasting `12` into a USD field means `$0.12` by this model; callers that want\nto accept pasted decimals must handle paste before calling this function.",
      "signature": "parseCurrencyInput(text: string, currency: string, locale: string): number",
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true
        },
        {
          "name": "currency",
          "type": "string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "plural",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { plural } from 'loui-ds';",
      "sourceFile": "src/i18n/plural.ts",
      "description": "Picks a plural form for `count` using `Intl.PluralRules`.\nCall it from catalog functions, passing that catalog's own locale:\n`selected: (n: number) => plural('en', n, { one: '1 selected', other: `${n} selected` })`.",
      "signature": "plural(locale: string, count: number, forms: PluralForms): string",
      "params": [
        {
          "name": "locale",
          "type": "string",
          "required": true
        },
        {
          "name": "count",
          "type": "number",
          "required": true
        },
        {
          "name": "forms",
          "type": "PluralForms",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "readableTextOn",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { readableTextOn } from 'loui-ds';",
      "sourceFile": "src/utils/color.ts",
      "description": "Which text color is more readable on a background color: `'light'` (white-ish\ntext, for dark backgrounds) or `'dark'`. Picks whichever of white or black has\nthe higher WCAG contrast, replacing the handoff's 0.299/0.587/0.114 shortcut.",
      "signature": "readableTextOn(backgroundHex: string): \"light\" | \"dark\"",
      "params": [
        {
          "name": "backgroundHex",
          "type": "string",
          "required": true
        }
      ],
      "returns": "\"light\" | \"dark\"",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "relativeLuminance",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { relativeLuminance } from 'loui-ds';",
      "sourceFile": "src/utils/color.ts",
      "description": "WCAG 2.x relative luminance of a hex color: `0` (black) to `1` (white).\nAccepts `#rgb` and `#rrggbb`. Invalid colors throw in development and count as black in production.",
      "signature": "relativeLuminance(hex: string): number",
      "params": [
        {
          "name": "hex",
          "type": "string",
          "required": true
        }
      ],
      "returns": "number",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "sortByText",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { sortByText } from 'loui-ds';",
      "sourceFile": "src/utils/text.ts",
      "description": "Returns a new array sorted alphabetically by `getText(item)`. Stable: items whose\ntext compares equal (for example `Ana` and `ana`, or `é` and `e`) keep their input order.",
      "signature": "sortByText<T>(items: readonly T[], getText: (item: T) => string, locale: string): T[]",
      "params": [
        {
          "name": "items",
          "type": "readonly T[]",
          "required": true
        },
        {
          "name": "getText",
          "type": "(item: T) => string",
          "required": true
        },
        {
          "name": "locale",
          "type": "string",
          "required": true
        }
      ],
      "returns": "T[]",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "SUPPORTED_LOCALES",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { SUPPORTED_LOCALES } from 'loui-ds';",
      "sourceFile": "src/i18n/locales.ts",
      "description": "Locales that ship with built-in message catalogs (ADR-0005).",
      "signature": "readonly [\"en-US\", \"pt-BR\"]",
      "params": [],
      "returns": "readonly [\"en-US\", \"pt-BR\"]",
      "docs": "docs/architecture/i18n.md"
    },
    {
      "name": "toast",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { toast } from 'loui-ds';",
      "sourceFile": "src/components/Toast/queue.ts",
      "description": "Shows a toast in the default queue. Mount one `ToastRegion` near the app root.",
      "signature": "toast(message: ReactNode, options?: ToastOptions): string",
      "params": [
        {
          "name": "message",
          "type": "ReactNode",
          "required": true
        },
        {
          "name": "options",
          "type": "ToastOptions | undefined",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "toastWithUndo",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { toastWithUndo } from 'loui-ds';",
      "sourceFile": "src/components/Toast/undoToast.ts",
      "description": "Non-hook form of the undo toast, for plain modules (stores, services, event handlers outside\nReact). Shows `message` with an **Undo** action whose label comes from the message catalogue\nand the active locale, so callers never repeat the word by hand. Returns the toast key, like\n`toast()`.\n\nUnlike `useUndoable` it does not defer anything: do the change first, then call this with a\nfunction that reverses it. Use `useUndoable` inside components when you also need a deferred\n`commit` or failure handling.\n\nOutside React there is no context, so the locale is the one of the mounted `ToastRegion`\n(inside `DSProvider`); pass `locale` or `messages` to decide it yourself.",
      "signature": "toastWithUndo(message: ReactNode, options: ToastWithUndoOptions): string",
      "params": [
        {
          "name": "message",
          "type": "ReactNode",
          "required": true
        },
        {
          "name": "options",
          "type": "ToastWithUndoOptions",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "toCalendarDay",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { toCalendarDay } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Normalizes a calendar-day input to `YYYY-MM-DD`. Accepts an `ISODate` (returned unchanged, so a\nplain date is never shifted by a time zone), a `Date` (its **local** day) or an ISO date-time\nstring such as `2026-10-31T23:30:00-03:00` (parsed with `new Date()`, then its **local** day).\nAny other string is returned unchanged for the caller's own validation. An unparsable `Date`\nor date-time throws in development and gives `''` in production.",
      "signature": "toCalendarDay(value: Date | string): ISODate",
      "params": [
        {
          "name": "value",
          "type": "string | Date",
          "required": true
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "toISODate",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { toISODate } from 'loui-ds';",
      "sourceFile": "src/utils/date.ts",
      "description": "Converts a `Date` to `YYYY-MM-DD` using its **local** calendar day (what the\nuser sees on their clock). Pass `{ utc: true }` to use the UTC day instead.\nInvalid dates and years outside 0000-9999 throw in development and give `''` in production.",
      "signature": "toISODate(date: Date, options?: { utc?: boolean; }): ISODate",
      "params": [
        {
          "name": "date",
          "type": "Date",
          "required": true
        },
        {
          "name": "options",
          "type": "{ utc?: boolean; }",
          "required": false
        }
      ],
      "returns": "string",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "TOOLTIP_DELAY",
      "kind": "constant",
      "entryPoint": "loui-ds",
      "importStatement": "import { TOOLTIP_DELAY } from 'loui-ds';",
      "sourceFile": "src/components/Tooltip/Tooltip.tsx",
      "description": "Default time in milliseconds the pointer must rest on the trigger before the tooltip opens.",
      "signature": "500",
      "params": [],
      "returns": "500",
      "docs": "docs/architecture/utilities.md"
    },
    {
      "name": "warnDeprecated",
      "kind": "utility",
      "entryPoint": "loui-ds",
      "importStatement": "import { warnDeprecated } from 'loui-ds';",
      "sourceFile": "src/utils/meta.ts",
      "description": "Logs a deprecation warning once per `name`. Development only: a no-op in\nproduction builds and on the server-side of production. Call it from the\ndeprecated component's body or module, passing what to use instead.\n\n`warnDeprecated('OldButton', 'Use Button with variant=\"ghost\" instead.')`",
      "signature": "warnDeprecated(name: string, message: string): void",
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true
        },
        {
          "name": "message",
          "type": "string",
          "required": true
        }
      ],
      "returns": "void",
      "docs": "docs/architecture/utilities.md"
    }
  ],
  "icons": {
    "entryPoint": "loui-ds",
    "source": "lucide-react",
    "description": "Bundled Lucide icons, exported with an `Icon` suffix. Pass one to `Icon`, `IconButton` or a `leading`/`trailing` slot. Decorative unless labelled.",
    "names": [
      "ArchiveIcon",
      "ArrowDownIcon",
      "ArrowLeftIcon",
      "ArrowRightIcon",
      "ArrowUpIcon",
      "BellIcon",
      "BotIcon",
      "CalendarIcon",
      "CheckIcon",
      "ChevronDownIcon",
      "ChevronLeftIcon",
      "ChevronRightIcon",
      "ChevronsUpDownIcon",
      "ChevronUpIcon",
      "CircleAlertIcon",
      "CircleCheckIcon",
      "CircleXIcon",
      "CopyIcon",
      "EllipsisIcon",
      "ExternalLinkIcon",
      "FilterIcon",
      "InfoIcon",
      "LinkIcon",
      "LoaderIcon",
      "MenuIcon",
      "MessageIcon",
      "MinusIcon",
      "PencilIcon",
      "PlusIcon",
      "PrinterIcon",
      "RefreshIcon",
      "SearchIcon",
      "SparklesIcon",
      "StarIcon",
      "ThumbsDownIcon",
      "ThumbsUpIcon",
      "TrashIcon",
      "TriangleAlertIcon",
      "UsersIcon",
      "XIcon"
    ]
  },
  "reexports": [
    {
      "name": "isRTL",
      "from": "react-aria-components"
    },
    {
      "name": "useLocale",
      "from": "react-aria-components"
    }
  ]
}
