@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.
- package/README.md +2 -2
- package/dist/{adapter-Du7P_5Fx.d.ts → adapter-BJwn74rx.d.ts} +49 -1
- package/dist/adapters/generation-dispatch.d.ts +32 -7
- package/dist/adapters/generation-dispatch.js +1997 -216
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +29 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +15 -12
- package/dist/advanced/index.js +2013 -270
- package/dist/advanced/index.js.map +1 -1
- package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
- package/dist/bank-Ctdp3FUA.d.ts +118 -0
- package/dist/blueprint-validator.js +416 -92
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +2 -2
- package/dist/boilerplate.js +34 -15
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +13 -5
- package/dist/check/index.js +56 -3
- package/dist/check/index.js.map +1 -1
- package/dist/classifier/index.d.ts +2 -2
- package/dist/compose.d.ts +2 -2
- package/dist/{create-ui-generator-GIhzx08x.d.ts → create-ui-generator-N4gRXpcL.d.ts} +11 -4
- package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
- package/dist/{dispatch-BnAH56p4.d.ts → dispatch-BUoXRj_r.d.ts} +6 -4
- package/dist/evaluation/axis-checks/index.d.ts +7 -6
- package/dist/evaluation/axis-checks/index.js +90 -2
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +6 -5
- package/dist/evaluation/axis-checks/registry.js +88 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +189 -344
- package/dist/evaluation/index.js +1272 -59
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +26 -1
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.d.ts +3 -3
- package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +7 -7
- package/dist/harness/check/runtime-render/index.js +416 -92
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +5 -6
- package/dist/harness/index.js +69 -20
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +10 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +19 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +21 -7
- package/dist/harness/runtime.js +36 -17
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +6 -4
- package/dist/harness/types-public.js +43 -18
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +6 -5
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +64 -13
- package/dist/index.js +2023 -270
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +5 -5
- package/dist/policy.d.ts +1 -1
- package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.js +8 -1
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
- package/dist/types-public-B7wn4ce5.d.ts +959 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
- package/dist/workflows.d.ts +5 -5
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +38 -2
- package/src/adapters/tools.ts +2 -1
- package/src/boilerplate/free-design-prompt.ts +4 -4
- package/src/boilerplate/generate.ts +26 -1
- package/src/boilerplate/hard-sections.ts +14 -14
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
- package/src/check/index.ts +1 -0
- package/src/check/run-tier0.ts +6 -3
- package/src/check/sample-props.ts +63 -0
- package/src/coding-agent/tools.ts +4 -1
- package/src/contract-context.ts +1 -1
- package/src/create-harness.ts +3 -3
- package/src/create-ui-generator.ts +20 -1
- package/src/evaluation/__fixtures__/png.ts +100 -0
- package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
- package/src/evaluation/axis-checks/checks/universal.ts +111 -0
- package/src/evaluation/axis-checks/dispatch.ts +4 -1
- package/src/evaluation/cost-tracker.ts +54 -6
- package/src/evaluation/criteria/bank.ts +85 -0
- package/src/evaluation/criteria/context.ts +52 -0
- package/src/evaluation/criteria/edge-probe.ts +286 -0
- package/src/evaluation/criteria/edge.ts +202 -0
- package/src/evaluation/criteria/resolve.ts +207 -0
- package/src/evaluation/criteria/select.ts +79 -0
- package/src/evaluation/index.ts +27 -0
- package/src/evaluation/ink-extent.ts +200 -0
- package/src/evaluation/judge-spend.ts +48 -0
- package/src/evaluation/theme-css.ts +12 -5
- package/src/evaluation/types-public.ts +221 -3
- package/src/evaluation/types.ts +19 -0
- package/src/evaluation/visual-evaluator.ts +853 -51
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/probe-pending.ts +111 -0
- package/src/harness/check/runtime-render/render-check-host.ts +10 -6
- package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
- package/src/harness/check/runtime-render/render-check.ts +417 -133
- package/src/harness/check/runtime-render/walk.ts +200 -0
- package/src/harness/coding/assemble-result.ts +2 -0
- package/src/harness/coding/generate-task-runner.ts +30 -0
- package/src/harness/coding/init-session.ts +5 -3
- package/src/harness/coding/run-coding-turn.ts +12 -4
- package/src/harness/coding/run-eval-round.ts +82 -3
- package/src/harness/in-loop-evaluation.ts +39 -0
- package/src/harness/llm-router.ts +58 -1
- package/src/harness/result-types.ts +9 -0
- package/src/harness/runtime.ts +14 -0
- package/src/harness/types-public.ts +10 -1
- package/src/harness-tool-names.ts +38 -0
- package/src/index.ts +17 -0
- package/src/internal/verdict-line.ts +33 -0
- package/src/run-check.ts +2 -0
- package/src/taught-tokens.ts +32 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/get-wire.ts +1 -1
- package/src/tools/render-check-worker.ts +2 -1
- package/src/tools/render-check.ts +8 -67
- package/src/tools.ts +11 -1
- package/src/validation/primitives.ts +1 -1
- package/dist/llm-router-DwwUK1bz.d.ts +0 -265
- package/dist/types-public-Bk6d1WLN.d.ts +0 -451
package/dist/boilerplate.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { S as ScreenSize, a as ShellType, g as generateBoilerplate } from './generate-
|
|
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-
|
|
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. */
|
package/dist/boilerplate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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={
|
|
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 =
|
|
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={
|
|
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)
|