{
  "schemaVersion": 1,
  "documentationVersion": "2.1.0 (planned; decision 397)",
  "sourceVerified": "2026-11-13",
  "maturity": "documented-reference",
  "owner": "Space Works design-system owner (Shaan Shoukath); product maintainers own app integration",
  "nativeSupport": "Platform starters only; no shipped cross-platform component package asserted",
  "components": [
    {
      "slug": "button",
      "name": "Button",
      "category": "Actions",
      "ids": [
        "P11",
        "P30",
        "S9",
        "L5",
        "B"
      ],
      "purpose": "Run one clearly named action in the current task.",
      "use": "Use for Save, Create, Retry and confirmation. Use Link for a destination and IconButton only for a familiar compact action.",
      "anatomy": [
        "Native button: required action name and click target.",
        "Text label: verb plus object; owns the accessible name.",
        "Optional leading icon: decorative; pending artwork replaces it.",
        "Adjacent reason or outcome: visible text plus description/status relationship."
      ],
      "variants": "Primary: one filled action per region. Neutral: secondary action. Ghost: low emphasis. Danger outline: opens a consequential confirmation. Filled danger: final confirmation only. Brand: public/member primary with validated tenant pairs.",
      "spec": "44 px minimum control height and target width; 50–52 px full-width auth action only where P11 calls for it. Control radius 10 px, 4 px spacing grid, Outfit 600. B fill/on-fill pairs cover rest/hover/pressed. Cards have no elevation; overlay shadows do not belong on buttons.",
      "file": "ActionControl.tsx",
      "api": "`ActionControl({action, primary=false, className=''})`. `Action`: label, href, onClick, icon, pending, pendingLabel, disabled, disabledReason, tone ('default'|'danger'), external, confirms and confirm {title,message,confirmLabel?}. No arbitrary variant prop, submit type or forwarded ref is supplied. Use a native submit button for a form.",
      "app": "<ActionControl primary action={{ label: 'Save changes', pending: saving,\n  pendingLabel: 'Saving…', disabled: !dirty, disabledReason: !dirty ? 'No changes to save.' : undefined,\n  onClick: save }} />",
      "html": "<button type=\"button\" class=\"primary\" data-action=\"save\">Save changes</button>",
      "keyboard": "Tab / Shift+Tab: visit enabled buttons. Enter / Space: activate once. Native disabled buttons leave the tab order. A confirmation moves focus into Dialog; closing restores its opener.",
      "content": "Use “Save changes”, “Saving…”, “Saved.” and “Couldn’t save. Try again.” Explain blocked prerequisites visibly; the ActionControl reason is screen-reader-only unless the product also renders it. Append … for a confirmation, never for a completed action.",
      "gap": "ActionControl does not await onClick, own fetched state, show success/error or prevent double mutation by itself. Callers must set pending synchronously, await the result, retain entered data and announce outcomes. Its disabledReason is not a visible explanation.",
      "kind": "action",
      "related": [
        "link",
        "icon-button",
        "dialog",
        "toast"
      ],
      "do": "Pair Save changes with Discard for a dirty settings group; disable only the pending action.",
      "dont": "Do not use a filled danger control before confirmation or opacity to communicate disabled state.",
      "stableId": "SWC-button",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/ActionControl.tsx"
    },
    {
      "slug": "link",
      "name": "Link",
      "category": "Actions",
      "ids": [
        "P11",
        "P3",
        "S6",
        "L4",
        "B"
      ],
      "purpose": "Navigate to a named destination or resource.",
      "use": "Use for a route, Back link, download or in-page destination. Use Button for save, reveal or mutation; a visual text action can still be a button.",
      "anatomy": [
        "Anchor with real href: required destination.",
        "Visible descriptive text: owns its name.",
        "Optional external indicator: supplementary to a clear destination.",
        "Current-page marker: aria-current when used in navigation."
      ],
      "variants": "Inline text link; detail Back link; navigation current link; external/new-tab link; download. RouteLink chooses router navigation only for eligible internal URLs. It is not a disabled-link widget.",
      "spec": "Use accent-ink for text, an underline in prose, and a focus ring with offset. Inline links can use the inline target-size exception; standalone navigation targets follow the 44 px house rule. Allow long labels and URLs to wrap.",
      "file": "RouteLink.tsx",
      "api": "`RouteLink` accepts `AnchorHTMLAttributes<HTMLAnchorElement>`. Inside a router, a single-leading-slash href without target='_blank' uses TransitionLink; other hrefs use an anchor. React Router and the app motion module are dependencies. No loading, disabled or readOnly prop is defined.",
      "app": "<RouteLink href='/member/account' aria-current='page'>Account</RouteLink>",
      "html": "<a href=\"#main-content\">View the first-screen example</a>",
      "keyboard": "Tab / Shift+Tab: visit href anchors. Enter: follow. Space scrolls, as for a native link. Preserve open-in-new-tab, copy-link and modified-click behaviour. Route changes require product title and focus handling.",
      "content": "Name the destination (“Member account”), rather than “Click here”. Explain a new tab when it matters, with an external indicator as required by the UX quality bar. Downloads identify format and size when known.",
      "gap": "RouteLink does not implement route titles, focus restoration or status feedback. An anchor without href is not a functional link. ActionControl deliberately removes href for blocked actions; products must explain the reason and expose an accessible alternative.",
      "kind": "link",
      "related": [
        "button",
        "tabs",
        "menu"
      ],
      "do": "Use a real href and retain the browser context menu.",
      "dont": "Do not put a save mutation in href='#' or treat aria-disabled alone as click prevention.",
      "stableId": "SWC-link",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/RouteLink.tsx"
    },
    {
      "slug": "icon-button",
      "name": "IconButton",
      "category": "Actions",
      "ids": [
        "P11",
        "P14",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Offer a compact, named action with a familiar icon.",
      "use": "Use for Close, Search or a repeated row action when the icon is understood. Use a labelled Button for an unfamiliar or consequential action and Menu for rare row operations.",
      "anatomy": [
        "Button: 44×44 px minimum target.",
        "Decorative icon child: required visual symbol.",
        "Required label prop: accessible name and title.",
        "Optional adjacent explanation: required for blocked actions in context."
      ],
      "variants": "IconButton runs a button action. IconLink navigates using React Router Link. The wrapper uses the neutral desk-button family; primary, toggle, loading and destructive variants are not separate API values.",
      "spec": "Actual wrapper: min-h-11 min-w-11, centred, px-2. Preserve 44 px hit area when an icon is only 16–20 px. Keep icon and ring ≥3:1 against their actual surrounding surfaces; inherit explicit disabled colours.",
      "file": "IconButton.tsx",
      "api": "`IconButton({label, children, className, ...buttonProps})`: native button attributes except aria-label, children, title and type, supplied by the wrapper. `IconLink` has label/children plus React Router LinkProps. IconButton fixes type='button'. Native aria-pressed is available for a genuine toggle; no pending helper is supplied.",
      "app": "<IconButton label='Close details' onClick={close}><Icon name='X' /></IconButton>",
      "html": "<button type=\"button\" aria-label=\"Close details\"><span aria-hidden=\"true\">×</span></button>",
      "keyboard": "Tab / Shift+Tab: visit the button. Enter / Space: activate. For IconLink use Enter. For an actual toggle set aria-pressed and keep its name stable; title alone is not a reliable touch explanation.",
      "content": "Use “Close details” or “More actions for Drill press”, not “X” or “Ellipsis”. The label must describe the result; a title is supplementary. Never use the icon name as error feedback.",
      "gap": "The component cannot infer an icon’s meaning, pending state or disabled reason. It accepts native disabled and ARIA attributes, but the caller must prevent conflicting selected/disabled behaviour and hide decorative icon content.",
      "kind": "action",
      "related": [
        "button",
        "link",
        "menu"
      ],
      "do": "Give repeated row controls object-specific names and keep the icon aria-hidden.",
      "dont": "Do not shrink the button to the SVG bounds or rely on hover tooltips for the name.",
      "stableId": "SWC-icon-button",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/IconButton.tsx"
    },
    {
      "slug": "text-field",
      "name": "TextField and SearchField",
      "category": "Forms",
      "ids": [
        "P15",
        "P18",
        "S9",
        "L5",
        "B"
      ],
      "purpose": "Collect editable text or a search query with a persistent label.",
      "use": "Use a text input for a short value, textarea for long text, and SearchField for filtering/search. Use Select or Radio for a closed set. Keep read-only values selectable and explain why editing is unavailable.",
      "anatomy": [
        "Field label: required, visible, associated with one control.",
        "Input: value, appropriate type and autocomplete.",
        "Optional hint and prefix: read before acting; prefix is not the name.",
        "Error text: connected by aria-describedby with aria-invalid.",
        "Search only: magnifier, optional loading status, clear button and shortcut hint."
      ],
      "variants": "Native input/textarea inside Field; native readOnly value; SearchField controlled or defaultValue. Search options: expand, bare, shortcut, fixedIcon, adornmentClassName, loading/loadingLabel, clearLabel, clearOnEscape. A shortcut hint does not register a shortcut.",
      "spec": "44 px minimum input height, outline boundary on panel, muted placeholder, ink value; invalid boundary/message use danger. Search can sit on the Citron sidebar in light and neutral surface in dark. Field is grid gap-2. Use min-inline-size:0 and full-width wrapping at narrow sizes; never use a placeholder instead of a label.",
      "file": "Field.tsx",
      "extra": [
        "SearchField.tsx",
        "SearchField.css"
      ],
      "api": "`Field({label,hint?,error?,className?,children})` clones one valid non-Fragment child to supply label/error/hint relationships; custom children must forward them to the input. `SearchField` forwards a ref and native input attributes except type/size, fixes type='search', supports value/defaultValue/onChange, disabled/readOnly and the variants above. No standalone TextField export exists.",
      "app": "<Field label='Search inventory' hint='Search by name or code' error={error}>\n  <SearchField value={query} onChange={e => setQuery(e.target.value)} loading={loading} />\n</Field>",
      "html": "<label for=\"query\">Search inventory</label><input id=\"query\" type=\"search\" aria-describedby=\"query-hint\"><p id=\"query-hint\">Search by name or code.</p>",
      "keyboard": "Tab: input; normal text editing is native. Search Escape clears nonempty editable text, prevents propagation and keeps input focus. Set clearOnEscape=false when Escape closes an enclosing popup. The clear button has tabIndex=-1; keyboard users clear with Escape or text editing. Shortcut registration belongs to the product.",
      "content": "Use visible labels, optional markers and consequence-first helpers. “Search by name or code”, “Searching…”, “No items match ‘saw’. Clear filters.” and “Couldn’t search. Try again.” Preserve the query after errors and label read-only reasons explicitly.",
      "gap": "SearchField is not a combobox and does not supply suggestions, debounce, API cancellation, results announcements or autocomplete semantics. Field cannot associate a Fragment of multiple controls. Use a fieldset for a group.",
      "kind": "field",
      "related": [
        "select",
        "checkbox",
        "empty-state",
        "skeleton-loader"
      ],
      "do": "Keep the last query and results during a refresh and announce the new count once.",
      "dont": "Do not clear a user’s draft after a request failure or disable all fields while one save is pending.",
      "stableId": "SWC-text-field",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/Field.tsx"
    },
    {
      "slug": "checkbox",
      "name": "Checkbox",
      "category": "Forms",
      "ids": [
        "P19",
        "P14",
        "S8",
        "L6",
        "B"
      ],
      "purpose": "Choose independent options or select rows.",
      "use": "Use for independent yes/no choices, required consent and row selection. Radio chooses one option; Switch describes an on/off setting. Keep group Save/Discard separate from immediate updates.",
      "anatomy": [
        "Native input type=checkbox: required state and semantics.",
        "Visible label with a 44 px activation area.",
        "Optional fieldset/legend for related options.",
        "Optional description/error; select-all can expose native indeterminate state."
      ],
      "variants": "Unchecked, checked and mixed/select-all. P19 card choice expands the label surface and strengthens selected boundary. DataTable selects only visible IDs without discarding selections on other pages. No shared Checkbox React export exists.",
      "spec": "Use 16–20 px native mark inside a ≥44×44 px label target. Necessary unchecked outline and checked mark need 3:1; use brand-boundary/ink where outline on hover would fail. P19’s three columns collapse to a single column; labels and scopes wrap.",
      "file": "DataTable.tsx",
      "extra": [
        "ListColumns.tsx"
      ],
      "api": "Native input `checked`, `defaultChecked`, `onChange(event.target.checked)`, disabled, required, name/value and aria-describedby. Set the DOM `indeterminate` property with a ref for mixed state, as DataTable does. Native readOnly does not lock a checkbox. DataTable selectedIds/onSelectionChange are controlled.",
      "app": "<label className='flex min-h-11 items-center gap-2'>\n  <input type='checkbox' checked={selected} onChange={e => setSelected(e.target.checked)} />\n  Include archived items\n</label>",
      "html": "<label class=\"choice\"><input type=\"checkbox\">Include archived items</label>",
      "keyboard": "Tab: each enabled checkbox. Space: toggle. A native mixed checkbox is announced as mixed; keep select-all’s state aligned with visible selected IDs. Clicking the label toggles once. Do not replace normal checkbox keyboard behaviour with radio arrows.",
      "content": "Use positive labels (“Include archived items”). Explain why a final visible column cannot be hidden. “3 items selected” belongs near bulk actions, with a polite status update after user changes.",
      "gap": "The primitives use native inputs in DataTable/ListColumns, rather than an exported checkbox component. ListColumns disables the final selected column; products must make that prerequisite understandable. Selection is not permission to mutate fetched records.",
      "kind": "choice",
      "related": [
        "radio",
        "switch",
        "table"
      ],
      "do": "Use a fieldset legend and a visible selected count for bulk operations.",
      "dont": "Do not simulate read-only using readOnly on input type=checkbox or make all cards respond as one radio group.",
      "stableId": "SWC-checkbox",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/DataTable.tsx"
    },
    {
      "slug": "radio",
      "name": "Radio",
      "category": "Forms",
      "ids": [
        "P19",
        "P22",
        "S20",
        "L8",
        "B"
      ],
      "purpose": "Choose exactly one value from a visible set.",
      "use": "Use for one-of-many form choices where seeing alternatives matters. Checkbox supports multiple choices; SegmentedControl switches two or three views of the same object; Select suits a longer list.",
      "anatomy": [
        "Fieldset and legend: name the decision.",
        "Native radios sharing one group name.",
        "Visible label for every option.",
        "Optional hint/error linked to the group and affected inputs."
      ],
      "variants": "Unselected and selected; default optional group or required explicit choice. Reference WordFeedback renders one radio per word. No generic Radio export or asynchronous radio group is supplied.",
      "spec": "Keep a 44 px minimum label hit area; allow options to stack at 320 px and enlarged text. Use ink/brand-boundary for the necessary ring and selected dot. Never communicate selection through hue alone.",
      "file": "../delight/WordFeedback.tsx",
      "api": "Native `type='radio'`, shared name, distinct value, checked/defaultChecked, required, disabled and onChange. WordFeedback is a task-specific implementation, not a reusable Radio API; use native controls for a new product.",
      "app": "<fieldset><legend>View</legend>\n  {['List', 'Cards'].map(view => <label key={view}>\n    <input type='radio' name='view' value={view} checked={value === view}\n      onChange={() => setValue(view)} />{view}</label>)}\n</fieldset>",
      "html": "<fieldset><legend>Layout</legend><label class=\"choice\"><input type=\"radio\" name=\"layout\" value=\"list\" checked>List</label><label class=\"choice\"><input type=\"radio\" name=\"layout\" value=\"cards\">Cards</label></fieldset>",
      "keyboard": "Tab enters/leaves the native group; arrows move/select among enabled choices; Space chooses the focused option. Browser entry for an entirely unchecked group can vary by direction. Preserve one shared name; do not require a separate Tab stop for every custom option.",
      "content": "The legend asks a decision (“Layout”); labels are parallel (“List”, “Cards”). State “Choose one” if the task is unfamiliar. Never use a radio to submit or navigate on focus.",
      "gap": "Read-only is not supported by native radio. Show a labelled static value for locked choices. A product must own draft value, save validation and server errors; no source claims a universal Radio component.",
      "kind": "choice",
      "related": [
        "checkbox",
        "select",
        "segmented-control"
      ],
      "do": "Group related choices with a legend and preserve the chosen value after a save error.",
      "dont": "Do not use separate names for mutually exclusive options or auto-submit when focus enters.",
      "stableId": "SWC-radio",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/delight/WordFeedback.tsx"
    },
    {
      "slug": "switch",
      "name": "Switch",
      "category": "Forms",
      "ids": [
        "P15",
        "S9",
        "L5",
        "B"
      ],
      "purpose": "Represent a setting that is on or off.",
      "use": "Use for a binary setting with a stable name. Checkbox is clearer for consent or an item in a multi-choice list. Choose one persistence model for the section: explicit group Save or clearly immediate per-control save.",
      "anatomy": [
        "Native checkbox with role=switch: checked exposes on/off.",
        "Stable visible label in its activation area.",
        "Optional helper describing the effect.",
        "Save/outcome region owned by the surrounding settings form."
      ],
      "variants": "On and off; a draft switch in CommunityPolicySettings is saved with the group. An immediate switch must handle pending, rollback and outcome at the product layer. No standalone Switch component is exported.",
      "spec": "Reference uses a native 20 px checkbox visual with role=switch inside a min-h-11 labelled row. A custom track must preserve 44 px target and a non-colour state cue. Do not imply a sliding track exists in the source.",
      "file": "../../../features/community/settings/CommunityPolicySettings.tsx",
      "api": "Native `checked`, `onChange(event.target.checked)`, disabled, id and aria-describedby; role='switch'. CommunityPolicySettings holds a draft, derives changed fields and submits them through a mutation. Native readOnly has no effect. No indeterminate state belongs on a switch.",
      "app": "<label className='flex min-h-11 items-center gap-3'>\n  <input type='checkbox' role='switch' checked={enabled}\n    onChange={e => setEnabled(e.target.checked)} />Send the follow-up\n</label>",
      "html": "<label class=\"choice\"><input type=\"checkbox\" role=\"switch\">Send the follow-up</label>",
      "keyboard": "Tab visits enabled switches. Space toggles. Keep the name unchanged between on and off. For an immediate server save, prevent duplicate requests and announce completion or rollback without moving focus.",
      "content": "Use “Send the follow-up” with a helper explaining who receives it and when. Say “Saving…” and “Couldn’t save. The previous setting is restored.” only when that matches actual data handling.",
      "gap": "The app example is a group-save draft, not proof of instant persistence. Do not claim a shared Switch API or read-only HTML switch. Source does not supply a visible custom sliding track.",
      "kind": "choice",
      "related": [
        "checkbox",
        "button",
        "toast"
      ],
      "do": "Explain the setting’s consequence and show one Save/Discard model per section.",
      "dont": "Do not combine an instant save and a page-level Save for the same setting.",
      "stableId": "SWC-switch",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/features/community/settings/CommunityPolicySettings.tsx"
    },
    {
      "slug": "select",
      "name": "Select",
      "category": "Forms",
      "ids": [
        "P15",
        "P18",
        "S9",
        "L5",
        "B"
      ],
      "purpose": "Choose one value from a compact list of known options.",
      "use": "Use native select for a known list. Radio exposes a short choice set; TextField accepts arbitrary text. A searchable remote picker needs a separately reviewed combobox contract.",
      "anatomy": [
        "Visible label: required and associated.",
        "Native select and option children.",
        "Optional empty prompt and group labels.",
        "Hint/error text connected to the control."
      ],
      "variants": "Required or optional single selection; native optgroup for categories. ListColumns uses Comfortable/Compact density; LanguageSwitcher variant='select' supports explicit language and an optional space default. No generic Select export exists.",
      "spec": "44 px minimum control, panel fill and outline boundary, ink value. Width follows its slot; long labels must not widen the page. The operating system owns the opened native option popup. Preserve platform high-contrast and touch behaviour.",
      "file": "../shell/LanguageSwitcher.tsx",
      "extra": [
        "ListColumns.tsx"
      ],
      "api": "Native `value/defaultValue`, `onChange(event.target.value)`, name, required and disabled. LanguageSwitcher takes variant ('menu'|'select') and optional onPersist(code|null):Promise; it persists browser language first and reports account-persistence failure. Native select has no readOnly.",
      "app": "<Field label='Density'><select className='desk-input' value={density}\n  onChange={e => setDensity(e.target.value)}>\n  <option value='comfortable'>Comfortable</option><option value='compact'>Compact</option>\n</select></Field>",
      "html": "<label for=\"density\">Density</label><select id=\"density\"><option value=\"comfortable\">Comfortable</option><option value=\"compact\">Compact</option></select>",
      "keyboard": "Tab enters/leaves. Native arrows/type-ahead choose options; opening/commit shortcuts follow the OS and browser. Escape dismisses the native popup where supported. Do not intercept its keys with Menu handlers.",
      "content": "Use a task label and meaningful options. An empty prompt says “Choose a makerspace”; it is not a real value for a required selection. Distinguish “No options available” from “Couldn’t load options. Retry.”",
      "gap": "The system does not ship an asynchronous Select/combobox. Loading options, stale-value handling and errors belong to the product. Select-only language changes apply locally even if account persistence fails; report that precisely.",
      "kind": "select",
      "related": [
        "radio",
        "text-field",
        "menu"
      ],
      "do": "Keep a still-valid choice when reloading options and explain locked choices as static values.",
      "dont": "Do not add role=menu to a select or assume readOnly prevents selection.",
      "stableId": "SWC-select",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/shell/LanguageSwitcher.tsx"
    },
    {
      "slug": "tabs",
      "name": "Tabs",
      "category": "Navigation",
      "ids": [
        "P8",
        "P3",
        "S6",
        "L4",
        "B"
      ],
      "purpose": "Switch between related sections while exposing which section is current.",
      "use": "Use route tabs for URL-addressable detail sections and list-filter views. Use panel tabs for immediate local content; SegmentedControl is for two or three views of the same object. Omit gated tabs rather than disable them.",
      "anatomy": [
        "Named nav or tablist: required.",
        "Labels with optional inline counts: accessible names include counts.",
        "Current underline and font weight: supplementary to semantics.",
        "Route content or one labelled panel: owned by the active key."
      ],
      "variants": "Without onChange: RouteLink children in nav with aria-current='page'. With onChange: buttons role=tab, roving tabIndex and one tabpanel containing children. One active key; invalid key falls back to first tab. Counts are plain muted text, never badges.",
      "spec": "44 px targets, 2 px underline, normal/semibold labels, border-line baseline. Narrow bars scroll internally and keep the active tab visible; do not widen the page. Necessary dark-panel underline uses accent-ink/brand-boundary; raw accent is 2.99:1 there.",
      "file": "Tabs.tsx",
      "extra": [
        "useRovingFocus.ts"
      ],
      "api": "`Tabs({tabs,activeKey,label='Page sections',onChange?,children?})`. Tab = {key,label,href?,count?}; no disabled prop. Panel mode creates IDs/controls/label relationships. The onChange callback must update activeKey and children; route mode needs a valid href for every tab.",
      "app": "<Tabs tabs={[{ key: 'details', label: 'Details', href: '/items/1/details' },\n  { key: 'history', label: 'History', href: '/items/1/history' }]} activeKey='details' />",
      "html": "<nav aria-label=\"Item sections\"><a href=\"#tabs\" aria-current=\"page\">Details</a> <a href=\"#table\">History</a></nav>",
      "keyboard": "Panel mode: Tab enters selected tab, arrows wrap and activate, Home/End select first/last; Tab goes to the focusable panel. Route mode: native link activation with Enter; the app also adds arrow/Home/End roving without activation. Do not describe route links as ARIA tabs.",
      "content": "Use short parallel section labels, actionable inline counts and no badge fill. Keep the route in the URL for detail tabs. Announce loading within the panel without moving keyboard focus back to the bar.",
      "gap": "Panel mode auto-activates and renders one shared panel; it is unsuitable for slow remote switching without a product loading strategy. The component does not fetch data or own title/focus after navigation. Dark underline contrast needs consumption-time verification.",
      "kind": "tabs",
      "related": [
        "segmented-control",
        "link",
        "table"
      ],
      "do": "Keep stable keys and explicit route hrefs; omit unavailable sections before passing tabs.",
      "dont": "Do not apply role=tab to navigation anchors without implementing panel semantics.",
      "stableId": "SWC-tabs",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/Tabs.tsx"
    },
    {
      "slug": "segmented-control",
      "name": "SegmentedControl",
      "category": "Navigation",
      "ids": [
        "P9",
        "P22",
        "S20",
        "L8",
        "B"
      ],
      "purpose": "Choose two or three mutually exclusive views of the same object.",
      "use": "Use for List/Cards or Sign in/Create account when they are local views. Use Tabs for page sections and Radio/Select for a longer form decision. It never replaces navigation tabs.",
      "anatomy": [
        "Radiogroup with a required label.",
        "Two or three button options with role=radio.",
        "Selected button: checked and only initial tab stop.",
        "Controlled view content: external to the component."
      ],
      "variants": "Intrinsic inline width or fullWidth equal option widths. Invalid value falls back to the first option. No disabled option, read-only or asynchronous mode is exposed.",
      "spec": "Actual min-h-11 min-w-11 options, px-4 py-2, 4 px track gap/padding, border-line surface track, selected panel with outline. Brand geometry is 10 px control radius, no control shadow; P9’s observed pill/shadow is historical reference.",
      "file": "SegmentedControl.tsx",
      "extra": [
        "useRovingFocus.ts"
      ],
      "api": "`SegmentedControl({label,value,options:[{value,label}],onChange,fullWidth=false})`. Caller controls value and renders the corresponding content. It does not assign a panel role, URL or fetch state.",
      "app": "<SegmentedControl label='Inventory view' value={view}\n  options={[{ value: 'list', label: 'List' }, { value: 'cards', label: 'Cards' }]}\n  onChange={setView} fullWidth />",
      "html": "<fieldset class=\"segments\"><legend>Inventory view</legend><label class=\"choice\"><input type=\"radio\" name=\"segment\" value=\"list\" checked>List</label><label class=\"choice\"><input type=\"radio\" name=\"segment\" value=\"cards\">Cards</label></fieldset>",
      "keyboard": "Tab enters the checked choice. Arrow keys wrap, focus and activate; Home/End choose first/last. Space/Enter activate button-backed choices. Tab leaves the group. Keep focus on the chosen option when the content changes.",
      "content": "Use short parallel names (“List”, “Cards”) and a distinct group label (“Inventory view”). Do not include mutable counts if they cause controls to jump in width.",
      "gap": "The fallback is visual/semantic only; it does not call onChange to normalize an invalid value. Callers must keep value valid, handle no options before rendering, and provide accessible content and fetched-state handling.",
      "kind": "tabs",
      "related": [
        "tabs",
        "radio",
        "select"
      ],
      "do": "Use two or three views and let labels wrap or the group wrap at narrow widths.",
      "dont": "Do not invent disabled/readOnly props or copy P9’s reference shadow over brand B/E.",
      "stableId": "SWC-segmented-control",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/SegmentedControl.tsx"
    },
    {
      "slug": "dialog",
      "name": "Dialog",
      "category": "Layers",
      "ids": [
        "P16",
        "P30",
        "S9",
        "L5",
        "B"
      ],
      "purpose": "Focus attention on a bounded task or consequential confirmation.",
      "use": "Use for a short modal task, irreversible confirmation or critical choice. Use Drawer for contextual detail and an inline error for a failed field. Reversible routine actions use immediate effect plus Undo where allowed.",
      "anatomy": [
        "Opener: remembered focus target.",
        "Modal layer/scrim: background becomes inert.",
        "Panel with role=dialog and visible h2 name.",
        "Scrollable body: optional description and fields.",
        "Optional footer actions and explicit Close: remain reachable."
      ],
      "variants": "Modal size='md' (max-w-lg) or 'xl' (max-w-4xl), backdrop='plain' or 'blur'. Phones render as a bottom sheet. ConfirmDialog composes Modal with message, pending and optional safePrimary to emphasise Cancel.",
      "spec": "Viewport-capped panel, independently scrolling body, safe-area padding and keyboard-safe field reveal. Layer z-50, shadow-overlay only. Phone footer stacks/wraps; desktop footer wraps at the end. Never crop task controls at 320 px or enlarged text.",
      "file": "Modal.tsx",
      "extra": [
        "ConfirmDialog.tsx",
        "dialogFocus.ts",
        "OverlayPortal.tsx"
      ],
      "api": "`Modal({open,onClose,title,children,footer?,size='md',backdrop='plain'})`. `ConfirmDialog({open,title,message,confirmLabel?,cancelLabel?,tone='default',onConfirm,onCancel,pending=false,safePrimary=false})`. The parent owns open/pending and callback completion. ConfirmDialog pending disables action buttons; onClose still calls onCancel.",
      "app": "<ConfirmDialog open={open} title='Delete draft?' message='This permanently deletes the draft.'\n  confirmLabel='Delete draft' tone='danger' pending={pending}\n  onCancel={() => setOpen(false)} onConfirm={deleteDraft} />",
      "html": "<details><summary>Review deletion consequences</summary><p>This permanently deletes the draft.</p><button type=\"button\" data-open=\"dialog-demo\">Open confirmation</button></details><dialog id=\"dialog-demo\" aria-labelledby=\"dialog-title\"><h3 id=\"dialog-title\">Delete draft?</h3><p>This permanently deletes the draft.</p><form method=\"dialog\"><button value=\"cancel\">Cancel</button><button value=\"confirm\">Delete draft</button></form></dialog>",
      "keyboard": "Open: app focuses first rendered focusable descendant, or panel if none. Tab/Shift+Tab wrap in the top layer. Escape closes only the top layer unless already prevented. Closing restores the connected opener; missing opener falls back to body in the source. Products must supply a meaningful fallback when deleting the opener.",
      "content": "Name the object and actual consequence; use “Delete draft” instead of “OK”. A safe alternative is explicit Cancel. Explain pending cancellation honestly; do not close before an unresolved irreversible operation is known.",
      "gap": "Initial focus is DOM-first, not automatically the safest control or heading. Modal has no aria-describedby prop; give long content its own structure. ConfirmDialog does not await onConfirm or change pending text. Parent must preserve data and choose cancellation policy; backdrop/Escape can still invoke onClose while pending.",
      "kind": "layer",
      "related": [
        "drawer-sheet",
        "button",
        "toast"
      ],
      "do": "Keep Cancel first where it is safest, explain permanent consequences and test nested focus restoration.",
      "dont": "Do not show unrelated modal tasks simultaneously or treat disabling confirm as preventing Escape.",
      "stableId": "SWC-dialog",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/Modal.tsx"
    },
    {
      "slug": "drawer-sheet",
      "name": "Drawer / Sheet",
      "category": "Layers",
      "ids": [
        "P3",
        "P30",
        "S6",
        "L4",
        "B"
      ],
      "purpose": "Show contextual detail or a focused task above the current screen.",
      "use": "Use a detail drawer for inspection without losing list context and a phone sheet for a short task. Use a routed DetailLayout for a shareable full page or a Dialog for a concise confirmation.",
      "anatomy": [
        "Trigger/context row: return destination.",
        "Scrim and modal dialog panel.",
        "Title and explicit Close action.",
        "Scrollable content with safe-area padding.",
        "Optional task actions inside content: caller owns them."
      ],
      "variants": "DetailDrawer edge='right' default or 'left' for phone navigation. Full-width phone detail; md+ caps to min(48rem,100vw). Modal supplies the bottom-sheet phone variant. There is no generic Sheet export or drag-handle API.",
      "spec": "Drawer h-full w-full with safe-area header/body; md max-width 48rem. Body min-h-0 min-w-0 and independent vertical scroll; overlay elevation only. Modal sheet has viewport-capped body and safe-area-aware footer. A handle is decorative; closing must have a button.",
      "file": "DetailDrawer.tsx",
      "extra": [
        "Modal.tsx",
        "dialogFocus.ts",
        "keyboardSafe.ts"
      ],
      "api": "`DetailDrawer({open,title,onClose,children,edge='right'})`. `Modal` supplies responsive sheet behaviour, not a separate Drawer.sheet prop. Uses OverlayPortal, app presence, dialogFocus and keyboardSafe. No footer, fetch, snap-point, swipe or loading prop exists on DetailDrawer.",
      "app": "<DetailDrawer open={open} title='Drill press' edge='right' onClose={() => setOpen(false)}>\n  <p>Available for supervised use.</p>\n</DetailDrawer>",
      "html": "<button type=\"button\" data-open=\"drawer-demo\">Open item details</button><dialog id=\"drawer-demo\" class=\"drawer\" aria-labelledby=\"drawer-title\"><h3 id=\"drawer-title\">Drill press</h3><p>Available for supervised use.</p><form method=\"dialog\"><button>Close</button></form></dialog><noscript><p>Drill press: available for supervised use.</p></noscript>",
      "keyboard": "Same stack-aware entry, Tab trap, Escape and opener restoration as Dialog. Close button is always available. Background must not receive focus while modal. When content includes search Escape, honour defaultPrevented before closing. No drag gesture is required.",
      "content": "Use the inspected object as title. State “Couldn’t load this item. Retry.” inside the body, retain the Close action, and explain an interrupted draft before discarding it.",
      "gap": "The source title truncates with a title attribute; products should ensure an accessible full title and useful long-text presentation. Edge placement depends on the app motion CSS. The sample uses native dialog for portability and does not reproduce the app animation or claim a reusable native sheet.",
      "kind": "layer",
      "related": [
        "dialog",
        "menu",
        "table"
      ],
      "do": "Restore focus to the selected row or a nearby list heading if that row has disappeared.",
      "dont": "Do not make swipe the sole way to close or place unscrollable fields under a phone keyboard.",
      "stableId": "SWC-drawer-sheet",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/DetailDrawer.tsx"
    },
    {
      "slug": "menu",
      "name": "Menu",
      "category": "Layers",
      "ids": [
        "P14",
        "P16",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Reveal a small set of secondary actions for the current context.",
      "use": "Use OverflowMenu for uncommon row/header actions. Keep the next step inline. Use Select for choosing a field value, and ordinary links for primary navigation.",
      "anatomy": [
        "Named trigger with haspopup, expanded and controls.",
        "Anchored role=menu popup.",
        "Menuitem link/button with action label and optional icon/reason.",
        "Optional confirmation layer after the menu closes."
      ],
      "variants": "Overflow ellipsis by default; showLabel adds label plus ChevronDown. mobileActions are shown below md; actions are always shown. Popup alignment adapts near the left viewport edge. No submenus or checked-menu-item API exists.",
      "spec": "44 px item targets, p-1 popup, w-64 capped to viewport minus 2rem, max-height min(60dvh,28rem) with internal scroll. Panel fill and overlay shadow; wrap long labels. Do not use decorative line as the only boundary for an essential state.",
      "file": "OverflowMenu.tsx",
      "extra": [
        "useRovingFocus.ts"
      ],
      "api": "`OverflowMenu({actions,label='More actions',className='',mobileActions=[],showLabel=false})`, using shared Action fields. Empty action arrays render null. Disabled/pending items expose aria-disabled/aria-busy, remain focusable during menu traversal, and guard click. Menu closes before action/confirmation.",
      "app": "<OverflowMenu label='More actions for Drill press' actions={[\n  { label: 'View history', href: '/items/1/history' },\n  { label: 'Archive', onClick: archive }\n]} />",
      "html": "<details><summary>More actions for Drill press</summary><a href=\"#table\">View history</a><button type=\"button\" data-action=\"archive\">Archive</button></details>",
      "keyboard": "Trigger Enter/Space or ArrowDown opens at first item; ArrowUp opens at last. Popup arrows/Home/End wrap; printable characters perform single-character prefix search; Enter/Space activates. Escape closes/restores trigger. Tab closes (source restores trigger before default Tab); verify forward/backward destination in the composed page. Outside pointer and blur close.",
      "content": "Use verb-plus-object labels. Context belongs in the trigger’s accessible label; a disabled reason belongs with its item and should also be available visibly where necessary. Confirmation labels end in ….",
      "gap": "Source type-ahead is one character, not buffered search. aria-disabled items are included by rovingItems and announce the reason; do not describe them as native-disabled. The standalone details demo deliberately uses ordinary links/buttons and does not claim role=menu keyboard semantics.",
      "kind": "menu",
      "related": [
        "button",
        "dialog",
        "select"
      ],
      "do": "Keep one common action inline and place rare alternatives here.",
      "dont": "Do not fill menus with forms, navigation trees or arbitrary disabled controls.",
      "stableId": "SWC-menu",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/OverflowMenu.tsx"
    },
    {
      "slug": "table",
      "name": "Table",
      "category": "Data and display",
      "ids": [
        "P14",
        "S5",
        "S15",
        "L3",
        "B"
      ],
      "purpose": "Compare structured records while retaining headers, sorting and selection context.",
      "use": "Use DataTable for comparable rows/columns. Use ListRow/card patterns when a record’s primary action matters more than comparison. Keep dense operational adaptations from UX §5.1.",
      "anatomy": [
        "Labelled scroll region with a native semantic table.",
        "Optional screen-reader caption and column headers.",
        "Optional select-all checkbox and row checkboxes.",
        "Cell content with explicit phone roles.",
        "Optional sort buttons plus separate Pagination/status region."
      ],
      "variants": "Comfortable or compact density through listState. Local sort or controlled ordering. Optional controlled selection. Phone roles lead/title/status/detail/hidden; actions placed last. Loading and EmptyState branches are built in; error/offline are product compositions.",
      "spec": "Desktop/tablet table minimum 640 px inside a keyboard-focusable horizontal region. Below md, rows become stacked cards with generated visual detail labels and retained table roles. No page overflow; never hide an essential field via phone='hidden'. Header sort/select controls retain 44 px targets even in compact mode.",
      "file": "DataTable.tsx",
      "extra": [
        "ListColumns.tsx",
        "types.ts"
      ],
      "api": "`DataTable<T>`: columns[{key,header,render?,sortable?,className?,phone?}], data, getRowId?, selectedIds?, onSelectionChange?, loading?, emptyTitle?, emptyDescription?, emptyAction?, skeletonCols? (compatibility only), ordering?, onOrderingChange?, listState?, caption?. Default row ID is row.id. Passing ordering disables local sort; caller supplies correctly ordered fetched rows.",
      "app": "<DataTable caption='Inventory' columns={[{ key: 'name', header: 'Item', sortable: true },\n  { key: 'status', header: 'Status' }]} data={[{ id: 1, name: 'Drill press', status: 'Available' }]} />",
      "html": "<div class=\"table-scroll\" role=\"region\" aria-label=\"Inventory\" tabindex=\"0\"><table><caption>Inventory</caption><thead><tr><th scope=\"col\">Item</th><th scope=\"col\">Status</th></tr></thead><tbody><tr><td>Drill press</td><td>Available</td></tr></tbody></table></div>",
      "keyboard": "Tab visits the scroll region and interactive header/cell controls; arrows scroll the region where supported. Sort uses native button Enter/Space; selection uses native checkbox Space. This is a table, not an ARIA grid: no invented arrow-cell model. Screen-reader table navigation must still associate headers after mobile reflow.",
      "content": "Sentence-case column names, units/currency in context, zero as 0 and unknown as a distinct state. “1–24 of 248” is pagination context. Empty and filtered-empty have different copy and recovery. Identify row checkbox names with meaningful object context where numeric IDs are insufficient.",
      "gap": "Current loading branch renders LogoLoader, although P14/quality bar describes final-geometry skeletons. Empty branch uses legacy titled EmptyState; P17 calls for one sentence. Preserve these as observed differences rather than asserting the app is compliant. Errors, offline, total count and caption remain caller responsibilities.",
      "kind": "data",
      "related": [
        "pagination",
        "checkbox",
        "empty-state",
        "status-pill",
        "skeleton-loader"
      ],
      "do": "Pass caption, stable IDs and a usable phone role for every essential column.",
      "dont": "Do not treat supplied ordering as a request for DataTable to sort fetched data or claim selection reaches unseen rows automatically.",
      "stableId": "SWC-table",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/DataTable.tsx"
    },
    {
      "slug": "pagination",
      "name": "Pagination",
      "category": "Navigation",
      "ids": [
        "P14",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Move through a bounded result set with a clear position and range.",
      "use": "Use for paginated lists/tables. Use a named Load more action when totals are unknown; it needs a separate accumulation contract. Do not show a pager for one page.",
      "anatomy": [
        "Navigation landmark named Pagination.",
        "Optional range text: first–last of count.",
        "Previous and Next native buttons.",
        "Current page of total pages text."
      ],
      "variants": "Page-only label or count+pageSize range context. Whole pager disabled during loading; first/last buttons separately disabled at boundaries. It returns null when totalPages<=1.",
      "spec": "Wrap the action row at 320 px, preserve 44 px targets and explicit disabled colours. Range moves to the start with room for translation. A single-page range must be supplied by the parent because this component renders nothing.",
      "file": "Pagination.tsx",
      "api": "`Pagination({page,totalPages,onChange,count?,pageSize?,disabled?})`. page is 1-based; count+pageSize are both needed for range. Previous/Next clamp callbacks within bounds. Caller owns fetching, valid page/total, URL synchronization and status announcements.",
      "app": "<Pagination page={page} totalPages={11} count={248} pageSize={24}\n  disabled={fetching} onChange={setPage} />",
      "html": "<nav aria-label=\"Pagination\"><span>1–24 of 248</span><button type=\"button\" disabled>Previous</button><span>Page 1 of 11</span><button type=\"button\" data-page-next>Next</button></nav>",
      "keyboard": "Tab reaches enabled Previous/Next; Enter/Space requests that page. Keep focus on the initiating control when data refreshes; if it becomes disabled or the pager disappears, move to a meaningful results heading after the request, not to body. Announce the loaded range once.",
      "content": "Use “Previous”, “Next”, “Page 2 of 11” and “25–48 of 248”. Failure says “Couldn’t load page 2. Try again.” Retain the last successful rows and truthful range until replacement succeeds.",
      "gap": "No pending label, live region, retry, URL or focus fallback is built in. Invalid page, negative counts and stale totals are not normalized beyond callback clamping; validate them in the product. No server or infinite-scroll behaviour is implied.",
      "kind": "pager",
      "related": [
        "table",
        "button",
        "empty-state"
      ],
      "do": "Keep filter/search state while paging and reset to page 1 when filters change.",
      "dont": "Do not announce the requested range as loaded before the server succeeds.",
      "stableId": "SWC-pagination",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/Pagination.tsx"
    },
    {
      "slug": "status-pill",
      "name": "StatusPill",
      "category": "Data and display",
      "ids": [
        "P10",
        "S1",
        "S5",
        "L4",
        "C"
      ],
      "purpose": "Name a record’s status with a compact, supplementary colour cue.",
      "use": "Use for status metadata such as Available, Unpaid or Returned. Use Checkbox/choice chips for selection and Button for an action. A status word is mandatory; colour alone never carries the meaning.",
      "anatomy": [
        "Noninteractive span container.",
        "Optional/default decorative tone icon.",
        "Mandatory visible status word.",
        "Optional children override: must still contain a meaningful word."
      ],
      "variants": "Tone: success, warn, danger, accent, emphasis, info, neutral. Success/warn/info use solid brand fills in both themes; emphasis is solid orange, accent is soft with accent-ink, danger is panel with danger text/line, neutral is surface/ink. Domain labels and backend mappings remain in C.",
      "spec": "Actual min-height 24 px, 12 px semibold, full radius, gap-1, px-2 and py-0.5; public spec allows 32 px. Noninteractive pills do not need a 44 px target. Long labels may wrap without splitting individual letters; keep max-width bounded.",
      "file": "StatusPill.tsx",
      "extra": [
        "tones.ts"
      ],
      "api": "`StatusPill({label,tone='neutral',icon?,children?})`. Icon defaults by tone and is always rendered through Icon; blank trimmed label falls back to translated Unknown, unless truthy children override. No onClick, selected, disabled or pending props exist.",
      "app": "<StatusPill label='Available' tone='success' />",
      "html": "<span class=\"pill\">Available</span>",
      "keyboard": "Not focusable and has no activation keys. Reading order follows the related object. For a live status change, announce a meaningful sentence in a separate status region; do not turn every static pill into a live region.",
      "content": "Use the vocabulary from C and the backend’s actual state. Overdue includes duration (“Overdue 3 days”). Do not present a stale cached value as current availability while offline.",
      "gap": "C still describes soft dark success/warn/info while later lessons and real tones.ts use solid marks in both themes. This page follows the later rule; a dated authority note below records the discrepancy. The legacy label/children combination can bypass the fallback, so callers must supply a real word.",
      "kind": "display",
      "related": [
        "table",
        "empty-state",
        "toast"
      ],
      "do": "Show a readable word and keep the same tone meaning across products.",
      "dont": "Do not turn a status pill into a filter control or reuse pastel fill tokens for standalone text.",
      "stableId": "SWC-status-pill",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/StatusPill.tsx"
    },
    {
      "slug": "empty-state",
      "name": "EmptyState",
      "category": "Feedback",
      "ids": [
        "P17",
        "S2",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Explain why expected content is absent and point to the next useful step.",
      "use": "Use only after a successful load with no records, or no filtered matches. Use Loader for unresolved data and inline error/Retry for failure. Do not disguise a permission denial or offline fetch as an empty list.",
      "anatomy": [
        "Dashed noninteractive container.",
        "One centred situation-and-next-step sentence (P17).",
        "Existing visible control named in the sentence.",
        "Optional action slot when a matching action is needed locally."
      ],
      "variants": "Current message+Action branch; legacy title/description/ReactNode-action branch. Truly empty versus filtered-empty versus a deliberately empty preview are different content cases. P17’s current recipe has no heading or illustration.",
      "spec": "Actual min-h-32, dashed line border, rounded-2xl, p-6 and gap-3, 15 px muted sentence max-w-md. Brand geometry uses card radius 16 px; do not copy reference radius 12 in P17 as a universal token. Let text wrap at 320 px and 200% text.",
      "file": "EmptyState.tsx",
      "api": "`EmptyStateProps = {message:string,action?:Action} | {title:string,description?:string,action?:ReactNode}`. Presence of message chooses the current branch: paragraph role=status plus optional ActionControl. Legacy branch renders an h3 and description. No loading/error/offline props exist.",
      "app": "<EmptyState message='No items match your filters. Clear filters above to see all inventory.'\n  action={{ label: 'Clear filters', onClick: clearFilters }} />",
      "html": "<div class=\"empty\"><p>No items match your filters. Clear filters above to see all inventory.</p><button type=\"button\" data-clear-filters>Clear filters</button></div>",
      "keyboard": "The explanatory block is not a Tab stop. Its optional action follows normal Button behaviour. When results become empty, retain filter focus and announce the change once; do not force focus onto the paragraph.",
      "content": "“No items yet. Use Add item above to start your inventory.” differs from “No items match your filters. Clear filters above.” Mention only controls actually visible and allowed. Avoid blame and decorative imagery.",
      "gap": "Legacy callers, including DataTable, still use a heading branch that differs from P17. Current message branch uses role=status; products must avoid duplicate result announcements and ensure status is inserted after a relevant update. No empty illustration should be inferred from the app.",
      "kind": "empty",
      "related": [
        "table",
        "text-field",
        "skeleton-loader"
      ],
      "do": "State the situation plus the next step, with permission-aware controls.",
      "dont": "Do not show “No records” after a request failed or offer Create when the user lacks that capability.",
      "stableId": "SWC-empty-state",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/EmptyState.tsx"
    },
    {
      "slug": "toast",
      "name": "Toast",
      "category": "Feedback",
      "ids": [
        "P30",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Confirm an off-screen effect or offer compensation for a reversible change.",
      "use": "Use when an effect is off-screen or a row leaves view. Confirm in place when the result remains visible. Never offer Undo for evidence, handovers, audit, payments or settlements; use an explicit consequential confirmation when required.",
      "anatomy": [
        "One ToastHost mounted for the session.",
        "Visible message with optional action, Undo and Dismiss.",
        "Separate polite status and assertive error announcement nodes.",
        "Session-scoped store with timers and awaited compensation."
      ],
      "variants": "Store tones success (default), error and info; optional action or async undo/undoKey. Non-error default duration is 8000 ms; errors persist. Busy Undo disables its own controls and changes the label to Undoing…. No arbitrary children API.",
      "spec": "P30 specifies bottom-centre mobile/bottom-left main desktop; observed ToastHost is right-aligned, safe-area-aware, bottom 6rem and max width min(24rem,100vw−2rem), max-height 60dvh. This discrepancy is recorded, not silently normalized. Keep toast content out of focused controls’ way.",
      "file": "ToastHost.tsx",
      "extra": [
        "toastStore.ts"
      ],
      "api": "`toast(message,{tone?,undo?:()=>Promise<unknown>,undoKey?,duration?,action?:{label,onClick}})` returns id. `dismissToast(id)`, `undoToast(id)`, `invalidateToastUndo(key)`, `clearToasts(context?)`, toastSession and store subscribe/snapshot. ToastHost takes no props. SessionContext and motion modules make this reference app code, not a portable package.",
      "app": "// Mount <ToastHost /> once, within the app session context.\ntoast('Item archived.', { undoKey: `item:${id}`, undo: () => restoreItem(id) });",
      "html": "<button type=\"button\" data-toast>Archive sample item</button><aside class=\"toast-list\" aria-label=\"Notifications\"></aside>",
      "keyboard": "Toasts never take focus merely by appearing. Actions/Dismiss are ordinary buttons in Tab order. Await Undo; failed compensation becomes a persistent error. A timed toast must not remove a focused action; pause expiry on hover/focus and during a pending action, and restore a meaningful focus target after explicit dismissal.",
      "content": "Use precise outcomes: “Item archived.”, “Undoing…”, “Action undone.”, “Couldn’t undo. Refresh and try again.” Do not imply Undo erases an audit trail or restores an irreversible effect.",
      "gap": "The observed store has no hover/focus pause and the host has no expiry-focus fallback. Default 8-second timers can remove focused controls; this is a product implementation gap. The standalone gallery demonstrates paused timing and focus recovery. Placement also differs from P30. No compliance claim is made for the existing host.",
      "kind": "toast",
      "related": [
        "button",
        "dialog",
        "empty-state"
      ],
      "do": "Await compensation and keep failure visible with a recovery route.",
      "dont": "Do not auto-dismiss an error or promise Undo for money, evidence or handover records.",
      "stableId": "SWC-toast",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/ToastHost.tsx"
    },
    {
      "slug": "skeleton-loader",
      "name": "Skeleton / Loader",
      "category": "Feedback",
      "ids": [
        "P14",
        "P17",
        "S5",
        "L3",
        "B"
      ],
      "purpose": "Show that requested content is pending without inventing data or completion.",
      "use": "Use a layout-preserving placeholder for an initial result load and a named inline loader for a pending action. Use a determinate ProgressBar only when an actual fraction is known. EmptyState is for a completed empty result.",
      "anatomy": [
        "Busy region: product owns aria-busy.",
        "One named status: Loading inventory… rather than repeated per row.",
        "Decorative box artwork: hidden from assistive technology.",
        "Optional real progress value and label, or a static final-geometry placeholder."
      ],
      "variants": "Skeleton({className}) decorative box; SkeletonRows({rows=5,cols}) table rows; Spinner({label?,size=24}) delegates to BoxSpinner. BoxSpinner({label?,size=20,className?,mono=false}) named status. ProgressBar({value,label,tone?}) clamps a finite fraction to 0–100 percent. These are separate APIs.",
      "spec": "Artwork is generally 16–24 px inline; loader size/space follows the occupied region. Motion durations/distances come from shipped tokens; reduced motion is a still open logo. Real ProgressBar is h-1 with line track; ensure necessary fill/track boundaries pass for actual tone and background. Reserve space to avoid layout shift.",
      "file": "Skeleton.tsx",
      "extra": [
        "Spinner.tsx",
        "ProgressBar.tsx",
        "../BoxSpinner.tsx"
      ],
      "api": "Skeleton and SkeletonRows are aria-hidden artwork wrappers/cells; callers supply the single loading announcement. ProgressBar requires a value fraction and label; no indeterminate flag exists. Use native progress without value or a named status for unknown progress. Do not pass fabricated percentages.",
      "app": "<section aria-busy={loading} aria-label='Inventory'>\n  {loading ? <><p role='status'>Loading inventory…</p><Skeleton className='min-h-32' /></> : content}\n</section>\n<ProgressBar value={completed / total} label='Uploading evidence' />",
      "html": "<section aria-label=\"Inventory loading\" aria-busy=\"true\"><p role=\"status\">Loading inventory…</p><div class=\"placeholder\" aria-hidden=\"true\"></div><label for=\"upload-progress\">Uploading evidence</label><progress id=\"upload-progress\" max=\"100\" value=\"40\">40%</progress></section>",
      "keyboard": "Decorative loader and progress are not Tab stops. Preserve existing focus during refresh. Completion clears aria-busy and announces a meaningful outcome once; error replaces the pending message with a persistent explanation and Retry. Never loop forever without a recovery path.",
      "content": "Use task-specific “Loading inventory…” or “Uploading 2 of 5 files…”. For unknown progress omit the number. On interruption say whether the upload was saved or must be retried; do not announce every animation frame.",
      "gap": "Despite its name Skeleton renders a box, not final-geometry bars; DataTable renders LogoLoader. The quality bar’s final-geometry guidance remains normative. Static placeholders in the starter are reference compositions, not claimed existing app exports. Dynamic progress contrast still requires validation.",
      "kind": "loader",
      "related": [
        "empty-state",
        "table",
        "button"
      ],
      "do": "Keep one loading status, hide repeated artwork and use a real fraction for determinate progress.",
      "dont": "Do not expose decorative skeleton rows as loaded records or use an unlabeled progressbar.",
      "stableId": "SWC-skeleton-loader",
      "maturity": "proposed",
      "availability": "documented-reference",
      "owner": "Space Works design-system owner (Shaan Shoukath)",
      "firstSupportedVersion": "2.1.0 (planned reference documentation only)",
      "implementationIntroduced": "Not established here; inspected in SpaceWorks 0.12.0 worktree",
      "lastVerified": "2026-11-13",
      "supportedImplementations": [
        "standalone HTML reference",
        "read-only SpaceWorks React source mapping"
      ],
      "nativeStatus": "starter-only; not compiled",
      "replacement": null,
      "deprecatedIn": null,
      "noticeDate": null,
      "removeNotBeforeVersion": null,
      "removeNotBeforeDate": null,
      "migration": "../releases/migration.md",
      "sourceFile": "frontend/src/components/ui/Skeleton.tsx"
    }
  ],
  "lifecycle": "proposed",
  "availability": "documented-reference"
}
