@ggui-ai/ui-gen 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +2 -2
  2. package/dist/{adapter-Du7P_5Fx.d.ts → adapter-BJwn74rx.d.ts} +49 -1
  3. package/dist/adapters/generation-dispatch.d.ts +32 -7
  4. package/dist/adapters/generation-dispatch.js +1997 -216
  5. package/dist/adapters/generation-dispatch.js.map +1 -1
  6. package/dist/adapters/index.js +29 -6
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/advanced/index.d.ts +15 -12
  9. package/dist/advanced/index.js +2013 -270
  10. package/dist/advanced/index.js.map +1 -1
  11. package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
  12. package/dist/bank-Ctdp3FUA.d.ts +118 -0
  13. package/dist/blueprint-validator.js +416 -92
  14. package/dist/blueprint-validator.js.map +1 -1
  15. package/dist/boilerplate.d.ts +2 -2
  16. package/dist/boilerplate.js +34 -15
  17. package/dist/boilerplate.js.map +1 -1
  18. package/dist/check/index.d.ts +13 -5
  19. package/dist/check/index.js +56 -3
  20. package/dist/check/index.js.map +1 -1
  21. package/dist/classifier/index.d.ts +2 -2
  22. package/dist/compose.d.ts +2 -2
  23. package/dist/{create-ui-generator-GIhzx08x.d.ts → create-ui-generator-N4gRXpcL.d.ts} +11 -4
  24. package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
  25. package/dist/{dispatch-BnAH56p4.d.ts → dispatch-BUoXRj_r.d.ts} +6 -4
  26. package/dist/evaluation/axis-checks/index.d.ts +7 -6
  27. package/dist/evaluation/axis-checks/index.js +90 -2
  28. package/dist/evaluation/axis-checks/index.js.map +1 -1
  29. package/dist/evaluation/axis-checks/registry.d.ts +6 -5
  30. package/dist/evaluation/axis-checks/registry.js +88 -1
  31. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  32. package/dist/evaluation/index.d.ts +189 -344
  33. package/dist/evaluation/index.js +1272 -59
  34. package/dist/evaluation/index.js.map +1 -1
  35. package/dist/evaluation/types.d.ts +26 -1
  36. package/dist/evaluation/types.js.map +1 -1
  37. package/dist/fragments/index.d.ts +3 -3
  38. package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
  39. package/dist/harness/check/runtime-render/index.d.ts +7 -7
  40. package/dist/harness/check/runtime-render/index.js +416 -92
  41. package/dist/harness/check/runtime-render/index.js.map +1 -1
  42. package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
  43. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  44. package/dist/harness/index.d.ts +5 -6
  45. package/dist/harness/index.js +69 -20
  46. package/dist/harness/index.js.map +1 -1
  47. package/dist/harness/prompts.d.ts +2 -2
  48. package/dist/harness/prompts.js +10 -1
  49. package/dist/harness/prompts.js.map +1 -1
  50. package/dist/harness/result-types.d.ts +19 -5
  51. package/dist/harness/result-types.js.map +1 -1
  52. package/dist/harness/runtime.d.ts +21 -7
  53. package/dist/harness/runtime.js +36 -17
  54. package/dist/harness/runtime.js.map +1 -1
  55. package/dist/harness/types-public.d.ts +6 -4
  56. package/dist/harness/types-public.js +43 -18
  57. package/dist/harness/types-public.js.map +1 -1
  58. package/dist/harness/validator-trace-sink.d.ts +6 -5
  59. package/dist/hash.d.ts +1 -1
  60. package/dist/index.d.ts +64 -13
  61. package/dist/index.js +2023 -270
  62. package/dist/index.js.map +1 -1
  63. package/dist/patch.d.ts +5 -5
  64. package/dist/policy.d.ts +1 -1
  65. package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
  66. package/dist/tools/get-primitives-ts.d.ts +1 -1
  67. package/dist/tools/get-primitives-ts.js +1 -1
  68. package/dist/tools/get-primitives-ts.js.map +1 -1
  69. package/dist/tools/render-check-worker.js +8 -1
  70. package/dist/tools/render-check-worker.js.map +1 -1
  71. package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
  72. package/dist/types-public-B7wn4ce5.d.ts +959 -0
  73. package/dist/validation/index.d.ts +1 -1
  74. package/dist/validation/index.js +1 -1
  75. package/dist/validation/index.js.map +1 -1
  76. package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
  77. package/dist/workflows.d.ts +5 -5
  78. package/package.json +12 -12
  79. package/src/adapters/generation-dispatch.ts +38 -2
  80. package/src/adapters/tools.ts +2 -1
  81. package/src/boilerplate/free-design-prompt.ts +4 -4
  82. package/src/boilerplate/generate.ts +26 -1
  83. package/src/boilerplate/hard-sections.ts +14 -14
  84. package/src/boilerplate/system-prompt.ts +1 -1
  85. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
  86. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
  87. package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
  88. package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
  89. package/src/check/index.ts +1 -0
  90. package/src/check/run-tier0.ts +6 -3
  91. package/src/check/sample-props.ts +63 -0
  92. package/src/coding-agent/tools.ts +4 -1
  93. package/src/contract-context.ts +1 -1
  94. package/src/create-harness.ts +3 -3
  95. package/src/create-ui-generator.ts +20 -1
  96. package/src/evaluation/__fixtures__/png.ts +100 -0
  97. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  98. package/src/evaluation/axis-checks/checks/universal.ts +111 -0
  99. package/src/evaluation/axis-checks/dispatch.ts +4 -1
  100. package/src/evaluation/cost-tracker.ts +54 -6
  101. package/src/evaluation/criteria/bank.ts +85 -0
  102. package/src/evaluation/criteria/context.ts +52 -0
  103. package/src/evaluation/criteria/edge-probe.ts +286 -0
  104. package/src/evaluation/criteria/edge.ts +202 -0
  105. package/src/evaluation/criteria/resolve.ts +207 -0
  106. package/src/evaluation/criteria/select.ts +79 -0
  107. package/src/evaluation/index.ts +27 -0
  108. package/src/evaluation/ink-extent.ts +200 -0
  109. package/src/evaluation/judge-spend.ts +48 -0
  110. package/src/evaluation/theme-css.ts +12 -5
  111. package/src/evaluation/types-public.ts +221 -3
  112. package/src/evaluation/types.ts +19 -0
  113. package/src/evaluation/visual-evaluator.ts +853 -51
  114. package/src/harness/check/runtime-render/adapter.ts +18 -0
  115. package/src/harness/check/runtime-render/probe-pending.ts +111 -0
  116. package/src/harness/check/runtime-render/render-check-host.ts +10 -6
  117. package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
  118. package/src/harness/check/runtime-render/render-check.ts +417 -133
  119. package/src/harness/check/runtime-render/walk.ts +200 -0
  120. package/src/harness/coding/assemble-result.ts +2 -0
  121. package/src/harness/coding/generate-task-runner.ts +30 -0
  122. package/src/harness/coding/init-session.ts +5 -3
  123. package/src/harness/coding/run-coding-turn.ts +12 -4
  124. package/src/harness/coding/run-eval-round.ts +82 -3
  125. package/src/harness/in-loop-evaluation.ts +39 -0
  126. package/src/harness/llm-router.ts +58 -1
  127. package/src/harness/result-types.ts +9 -0
  128. package/src/harness/runtime.ts +14 -0
  129. package/src/harness/types-public.ts +10 -1
  130. package/src/harness-tool-names.ts +38 -0
  131. package/src/index.ts +17 -0
  132. package/src/internal/verdict-line.ts +33 -0
  133. package/src/run-check.ts +2 -0
  134. package/src/taught-tokens.ts +32 -0
  135. package/src/tools/get-primitives-ts.ts +1 -1
  136. package/src/tools/get-wire.ts +1 -1
  137. package/src/tools/render-check-worker.ts +2 -1
  138. package/src/tools/render-check.ts +8 -67
  139. package/src/tools.ts +11 -1
  140. package/src/validation/primitives.ts +1 -1
  141. package/dist/llm-router-DwwUK1bz.d.ts +0 -265
  142. package/dist/types-public-Bk6d1WLN.d.ts +0 -451
@@ -1,8 +1,8 @@
1
- export { S as ScreenSize, a as ShellType, g as generateBoilerplate } from './generate-9IeNT4ZY.js';
1
+ export { S as ScreenSize, a as ShellType, g as generateBoilerplate } from './generate-tSO_shQ0.js';
2
2
  export { j as jsonSchemaTypeToTs } from './json-schema-ts-CbB78b3n.js';
3
3
  import { G as GenerationProfileInput } from './styling-profile-CxfvV48g.js';
4
4
  import { GadgetDescriptor } from '@ggui-ai/protocol';
5
- import { D as DesignMode, C as CanvasClass } from './design-mode-DLufhrNp.js';
5
+ import { D as DesignMode, C as CanvasClass } from './design-mode-DGWN_Gkm.js';
6
6
 
7
7
  interface SystemPromptInputs {
8
8
  /** The user's original request. */
@@ -315,6 +315,7 @@ function generateBoilerplate(_userPrompt, contract, shellType, screen, composedS
315
315
  }
316
316
  const actionTypeAliases = [];
317
317
  const actionHookCalls = [];
318
+ let hasOneShotAction = false;
318
319
  const actionsMap = contract?.actionSpec ?? {};
319
320
  for (const [key, entry] of Object.entries(actionsMap)) {
320
321
  const label = entry.label ?? key;
@@ -335,7 +336,20 @@ type ${typeName} = ${tsType};`
335
336
  const callSig = tsType === "void" ? "() => void \u2014 fire and forget" : `(data: ${tsType}) => void`;
336
337
  const toolHint = tool ? ` \u2192 nextStep: ${tool}` : "";
337
338
  const onceHint = " \u2014 if this is meant ONCE, disable its control after it fires";
338
- actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
339
+ if (entry.oneShot === true) {
340
+ hasOneShotAction = true;
341
+ actionHookCalls.push(
342
+ ` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint} \u2014 declared oneShot: the runtime drops a second dispatch`
343
+ );
344
+ actionHookCalls.push(
345
+ ` const ${key}Spent = useActionSpent('${key}'); // true once fired, also after a reload \u2014 render the control from THIS: disabled={${key}Spent} and a spent label`
346
+ );
347
+ } else {
348
+ actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
349
+ }
350
+ actionHookCalls.push(
351
+ ` const ${key}Pending = useActionPending('${key}'); // true from the tap until the agent's answer repaints the card \u2014 render EVERY control that dispatches '${key}' (a Button, a Select's onChange, an Input's Enter) from THIS: disabled={${key}Pending} aria-busy={${key}Pending}, and a working label on a button; no other control carries its aria-busy`
352
+ );
339
353
  }
340
354
  const streamChannels = contract?.streamSpec ?? {};
341
355
  const streamChannelEntries = Object.entries(streamChannels);
@@ -433,6 +447,8 @@ ${hookLines.join("\n")}
433
447
  const hasAnyWireFromWire = hasActions || hasStream || hasContext;
434
448
  const wireHooks = [];
435
449
  if (hasActions) wireHooks.push("useAction");
450
+ if (hasOneShotAction) wireHooks.push("useActionSpent");
451
+ if (hasActions) wireHooks.push("useActionPending");
436
452
  if (hasStream) wireHooks.push("useStream");
437
453
  if (hasContext) wireHooks.push("useGguiContext");
438
454
  const wireImport = hasAnyWireFromWire ? `import { ${wireHooks.join(", ")} } from '@ggui-ai/wire';
@@ -776,6 +792,13 @@ function buildCodingCriteriaSummary(designMode = "constrained") {
776
792
  }
777
793
  return lines.join("\n");
778
794
  }
795
+ var NOT_YET_TAUGHT_TOKENS = /* @__PURE__ */ new Set([
796
+ "--ggui-color-controlAccentOutline",
797
+ "--ggui-color-controlOutline"
798
+ ]);
799
+ var taughtTokenManifest = consumedTokenManifest.filter(
800
+ (token) => !NOT_YET_TAUGHT_TOKENS.has(token)
801
+ );
779
802
 
780
803
  // src/boilerplate/hard-sections.ts
781
804
  var PROTOCOL_NOTES = `## Protocol Notes
@@ -962,21 +985,17 @@ Rules:
962
985
  2. Layout and styling are universal. Colors, spacing, structure \u2014 these are the reusable part.
963
986
  3. Default values come from the current request \u2014 so the component renders correctly standalone.
964
987
  4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** \u2014 not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` \u2014 UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it. An action's control shows the contract's \`label\` VERBATIM \u2014 every character, including \`&\`, \`/\`, \`\u2013\` and quotes \u2014 never paraphrased, shortened or re-cased (\`"Confirm & Schedule"\` is not \`"Confirm Schedule"\`).
965
- 5. Props interface must be typed and exported.`;
988
+ 5. Props interface must be typed and exported.
989
+ 6. Keys on a mapped list come from the item's own identity: \`key={item.id}\` when the contract gives an id; when the items carry none (KPI tiles \`{label, value}\`, forecast days \`{day, high}\`), key by the field that names each one \u2014 \`key={item.label}\`, \`key={day.day}\` \u2014 never by the map index: a reorder or a stream merge re-keys by index and re-mounts the wrong rows, and the self-check refuses an index key.`;
966
990
  var TERMINAL_ACTIONS = `## Actions the user means once
967
991
 
968
992
  A second press is a mistake only when it would make a SECOND THING HAPPEN IN THE WORLD \u2014 a second booking, a second charge, a second order, a second application sent. If pressing again would only write the SAME STATE AGAIN \u2014 saving, updating, renaming, setting a preference, toggling \u2014 nothing is duplicated and the control stays armed.
969
993
 
970
- Guard a control only where you have evidence, not a feeling about the verb: the request itself names a one-time commitment \u2014 "submit the application", "place the order", "confirm the booking".
971
-
972
- **When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked \u2014 save, edit, save again is ordinary. A control left armed is only what every interface does by default.
994
+ **The contract says which actions are once.** An action the contract declares \`oneShot: true\` (in the Contract block above) is guarded by the runtime \u2014 a second dispatch is dropped \u2014 and its spent state is DATA you read, never state you hold: render its control from \`const spent = useActionSpent('<name>')\`, \`disabled={spent}\`, and say so in the label or beside it ("Submitted", "Request sent") \u2014 a control that is disabled without explanation reads as broken. The hook's answer survives a reload and a second tab; \`useState(false)\` does not, so a reloaded card built on local state re-arms a control that already fired.
973
995
 
974
- When you do guard one:
975
- - Hold a small piece of state for it and set it in the handler: \`const [submitted, setSubmitted] = useState(false)\`.
976
- - \`disabled={submitted}\` on the control that fires it, and say so in the label or beside it ("Submitted", "Request sent") \u2014 a control that is disabled without explanation reads as broken.
977
- - A card re-rendered with new props starts fresh; the state above lives with the component, not with the request.
996
+ **An action the contract does not declare \`oneShot\` stays armed.** Never guard one from a feeling about the verb \u2014 "confirm", "submit", "place" alone are not evidence. Guard it only where the request itself names a one-time commitment the contract failed to declare ("submit the application", "place the order"), and then with local state as the fallback: \`const [submitted, setSubmitted] = useState(false)\` set in the handler, \`disabled={submitted}\` with a label \u2014 knowing that this state lives with the component, not with the request, and starts fresh on a re-render with new props.
978
997
 
979
- Nothing in the wire DECLARES one-shot-ness yet, which is why the test is what a second press would DO \u2014 not how final the word sounds.`;
998
+ **When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked \u2014 save, edit, save again is ordinary. A control left armed is only what every interface does by default.`;
980
999
  var FRAME_SIZING = `## Sizing \u2014 the frame owns the height and the width
981
1000
 
982
1001
  The host sizes the iframe; the component never sizes itself to the viewport. NEVER write \`100vh\` / \`100dvh\` / \`100svh\` (or an \`h-screen\` / \`min-h-screen\` class) as a height on any element:
@@ -989,7 +1008,7 @@ The width works the same way, and the mistake is the mirror image: **your outerm
989
1008
 
990
1009
  - Never put \`maxWidth\` (or a \`max-w-*\` class) on the element you return at the top. On a wide canvas that turns your card into a narrow strip with the frame's ground on either side of it, and on a narrow one it does nothing \u2014 the same composition reading differently at two sizes.
991
1010
  - DO cap the reading measure inside: a column of prose is easiest to read at 60\u201370 characters, so wrap the TEXT in a \`<Container maxWidth="sm">\` (or your own \`max-width\`) and leave the root filling. A hero band, a toolbar, a table or an image then still uses the whole width, which is what makes a wide card look composed rather than padded.
992
- - Centre that inner column with the container's own centring, not by shrinking the root.
1011
+ - Centre that inner column with the container's own centring, not by shrinking the root \u2014 and give the card ONE column: every block of the reading flow (a heading, its text, and the chips or buttons that answer it) sits in a \`<Container>\` of the same \`maxWidth\`, so their left edges line up even where a full-width band runs between them.
993
1012
  - **They are two different elements.** Even when the whole card is one column of text, the root still fills and the column sits INSIDE it \u2014 \`<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>\`, never a root that is itself the container. A centred card is centring inside a filled frame, not a shrunken frame.`;
994
1013
  var COMPONENT_STRUCTURE = `## Component Structure
995
1014
 
@@ -1066,7 +1085,7 @@ function colorVocabularyFromManifest(manifest) {
1066
1085
  function renderRamp(r) {
1067
1086
  return `\`${r.family}-${r.steps.join("/")}\`${r.bare ? ` (plus bare \`${r.family}\`)` : ""}`;
1068
1087
  }
1069
- function renderFreeColorRule(manifest = consumedTokenManifest) {
1088
+ function renderFreeColorRule(manifest = taughtTokenManifest) {
1070
1089
  const { ramps, roles } = colorVocabularyFromManifest(manifest);
1071
1090
  const brand = ramps.filter((r) => r.family === "primary" || r.family === "neutral");
1072
1091
  const state = ramps.filter((r) => r.family !== "primary" && r.family !== "neutral");
@@ -1132,7 +1151,7 @@ With raw elements YOU own the semantics \u2014 nothing is accessible by construc
1132
1151
  3. **Images have \`alt\`** (\`alt=""\` for decorative). Icon-only buttons need \`aria-label\`; decorative icons and emoji are \`aria-hidden="true"\`.
1133
1152
  4. **Live & streaming data** (a \`useStream\` \`.latest\` value, counters, clocks, flashing prices) sits in an \`aria-live="polite"\` (or \`role="status"\`) region so screen readers announce the change.
1134
1153
  5. **Headings nest**: one \`<h1>\` per screen, \`<h2>\` for sections, \`<h3>\` below \u2014 never skipped or inverted.
1135
- 6. **Busy state is announced**: \`<button disabled={isLoading} aria-busy={isLoading}>{isLoading ? 'Submitting\u2026' : 'Submit'}</button>\`.
1154
+ 6. **Busy state is announced**: every control that dispatches an action (a button, a select whose change dispatches, an input that sends on Enter) stays pending from \`useActionPending('<action>')\` until the agent's answer repaints the card \u2014 \`<button disabled={sendPending} aria-busy={sendPending}>{sendPending ? 'Sending\u2026' : 'Send'}</button>\`. Only a control that dispatches the action carries its busy look; while it is pending, other controls of the same record may be disabled \u2014 nothing more (controls of other records, creation controls and the action's own cancel stay enabled); busy state the card owns (\`isLoading\`) the same way.
1136
1155
  7. **Stateful controls announce state**: \`role="checkbox"\` + \`aria-checked\` on a toggleable row, \`aria-pressed\` on a toggle button, \`aria-selected\` on the chosen item, \`aria-expanded\` on an open/close affordance. Styling (strikethrough, color, a check mark) is never the only carrier of state.
1137
1156
  8. **Focus is visible** \u2014 style \`:focus-visible\` with a token color; never \`outline: none\` without a replacement.`;
1138
1157
  var FREE_QUALITY_CHECKLIST = `## Quality Checklist (verify before returning)
@@ -1156,7 +1175,7 @@ var TOKEN_GROUPS = [
1156
1175
  { label: "Radius (optional)", prefixes: ["--ggui-shape-radius-"], note: "" },
1157
1176
  { label: "Shadow (optional)", prefixes: ["--ggui-shape-shadow-"], note: "" }
1158
1177
  ];
1159
- function renderTokenVocabulary(manifest = consumedTokenManifest) {
1178
+ function renderTokenVocabulary(manifest = taughtTokenManifest) {
1160
1179
  const sorted = [...manifest].sort();
1161
1180
  const claimed = /* @__PURE__ */ new Set();
1162
1181
  const lines = [
@@ -1676,7 +1695,7 @@ The design-system primitives are accessible by construction \u2014 they emit the
1676
1695
  3. **Icons are decorative by default** \u2014 \`<Icon name="check" />\` is hidden from screen readers, which is correct for an icon sitting next to text. Add \`aria-label\` ONLY for a standalone, meaning-bearing icon with no adjacent text. Icon-only \`Button\`s still need \`aria-label\` on the **Button** itself.
1677
1696
  4. **Live & streaming data** \u2014 wrap any region whose content updates on its own (a \`useStream\` \`.latest\` value, a live clock, an "N new" counter, a flashing price) in an element with \`aria-live="polite"\` so screen readers announce the change.
1678
1697
  5. **Headings nest** \u2014 one \`<Heading level={1}>\` per screen, \`level={2}\` for sections, \`level={3}\` for subsections. Never skip or invert levels.
1679
- 6. **Buttons** \u2014 descriptive text content; icon-only buttons need \`aria-label\`. Announce busy state: \`<Button disabled={isLoading} aria-busy={isLoading}>{isLoading ? 'Submitting\u2026' : 'Submit'}</Button>\`.
1698
+ 6. **Buttons** \u2014 descriptive text content; icon-only buttons need \`aria-label\`. Announce busy state. EVERY control that dispatches an action \u2014 a Button, a Select whose change dispatches, an Input that sends on Enter \u2014 stays pending from that action's \`useActionPending\` until the agent's answer repaints the card: with \`const sendPending = useActionPending('send');\`, write \`<Button disabled={sendPending} aria-busy={sendPending} onClick={() => send({ text })}>{sendPending ? 'Sending\u2026' : 'Send'}</Button>\`, and give a dispatching Select or Input the same \`disabled={sendPending} aria-busy={sendPending}\`. Only a control that dispatches the action carries its \`aria-busy\` and a working label. While it is pending, other controls of the SAME record may be \`disabled\` \u2014 nothing more; controls of other records, creation controls and the action's own cancel stay enabled. Busy state the card owns (a local \`isLoading\`) is announced the same way.
1680
1699
  7. **Stateful controls announce their state.** Toggleable / selectable / expandable things use the primitive that carries the semantics (\`Checkbox\`, \`Toggle\`, \`Tabs\`, \`Accordion\`) \u2014 never a styled \`Box\`/\`Icon\` that only *looks* checked. When you hand-roll one with \`as={Clickable}\`, \`Clickable\` adds button + keyboard semantics but NOT state \u2014 add it yourself: \`role="checkbox"\` + \`aria-checked={done}\` on a toggleable row, \`aria-pressed\` on a toggle button, \`aria-selected\` on the chosen item, \`aria-expanded\` on an open/close affordance. Styling (strikethrough, color, a check icon) must never be the only carrier of state.
1681
1700
 
1682
1701
  ## Design System Usage (CRITICAL)