@grampro/kit 2.6.2 → 2.7.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 (94) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +5 -4
  2. package/.gbs/skills/gbs-components/references/install.md +17 -14
  3. package/CHANGELOG.md +201 -18
  4. package/README.md +24 -6
  5. package/index.cjs +59 -59
  6. package/package.json +2 -2
  7. package/schema/passport-overlay-v1.schema.json +67 -0
  8. package/source/components/accordion/passport.json +45 -8
  9. package/source/components/accordion/passport.manual.json +46 -0
  10. package/source/components/alert/passport.json +41 -8
  11. package/source/components/alert/passport.manual.json +42 -0
  12. package/source/components/avatar/passport.json +40 -7
  13. package/source/components/avatar/passport.manual.json +43 -0
  14. package/source/components/badge/passport.json +43 -7
  15. package/source/components/badge/passport.manual.json +46 -0
  16. package/source/components/breadcrumb/passport.json +36 -7
  17. package/source/components/breadcrumb/passport.manual.json +39 -0
  18. package/source/components/button/passport.json +1 -1
  19. package/source/components/button/passport.manual.json +1 -1
  20. package/source/components/card/passport.json +45 -8
  21. package/source/components/card/passport.manual.json +47 -0
  22. package/source/components/checkbox/core/agent.ts +33 -0
  23. package/source/components/checkbox/core/index.ts +2 -0
  24. package/source/components/checkbox/passport.json +83 -10
  25. package/source/components/checkbox/passport.manual.json +82 -0
  26. package/source/components/combobox/core/agent.ts +38 -0
  27. package/source/components/combobox/core/index.ts +2 -0
  28. package/source/components/combobox/passport.json +103 -11
  29. package/source/components/combobox/passport.manual.json +101 -0
  30. package/source/components/combobox/react/props.ts +14 -0
  31. package/source/components/combobox/react/useCombobox.ts +7 -1
  32. package/source/components/data-grid/export/xlsx.ts +9 -2
  33. package/source/components/data-grid/passport.json +2 -2
  34. package/source/components/data-grid/passport.manual.json +1 -1
  35. package/source/components/date-picker/core/agent.ts +42 -0
  36. package/source/components/date-picker/core/index.ts +2 -0
  37. package/source/components/date-picker/passport.json +102 -11
  38. package/source/components/date-picker/passport.manual.json +100 -0
  39. package/source/components/date-picker/react/DatePicker.tsx +6 -0
  40. package/source/components/date-picker/react/DateRangePicker.tsx +4 -0
  41. package/source/components/date-picker/react/props.ts +17 -0
  42. package/source/components/dialog/passport.json +54 -9
  43. package/source/components/dialog/passport.manual.json +54 -0
  44. package/source/components/file-uploader/passport.json +65 -10
  45. package/source/components/file-uploader/passport.manual.json +64 -0
  46. package/source/components/input/core/agent.ts +36 -0
  47. package/source/components/input/core/index.ts +2 -0
  48. package/source/components/input/passport.json +89 -12
  49. package/source/components/input/passport.manual.json +86 -0
  50. package/source/components/menu/passport.json +64 -9
  51. package/source/components/menu/passport.manual.json +64 -0
  52. package/source/components/modal/passport.json +52 -9
  53. package/source/components/modal/passport.manual.json +52 -0
  54. package/source/components/number-input/core/agent.ts +38 -0
  55. package/source/components/number-input/core/index.ts +2 -0
  56. package/source/components/number-input/passport.json +92 -11
  57. package/source/components/number-input/passport.manual.json +90 -0
  58. package/source/components/popover/passport.json +47 -8
  59. package/source/components/popover/passport.manual.json +48 -0
  60. package/source/components/progress/passport.json +37 -8
  61. package/source/components/progress/passport.manual.json +39 -0
  62. package/source/components/radio-group/core/agent.ts +34 -0
  63. package/source/components/radio-group/core/index.ts +2 -0
  64. package/source/components/radio-group/passport.json +77 -10
  65. package/source/components/radio-group/passport.manual.json +76 -0
  66. package/source/components/shared/core/agent/field-agent.ts +101 -0
  67. package/source/components/shared/core/agent/field-executors.ts +137 -0
  68. package/source/components/shared/core/agent/field.ts +649 -0
  69. package/source/components/shared/core/agent/index.ts +30 -0
  70. package/source/components/shared/core/agent/write.ts +58 -0
  71. package/source/components/shared/react/useFieldAgent.ts +88 -0
  72. package/source/components/skeleton/passport.json +37 -6
  73. package/source/components/skeleton/passport.manual.json +39 -0
  74. package/source/components/spinner/passport.json +34 -7
  75. package/source/components/spinner/passport.manual.json +37 -0
  76. package/source/components/switch/core/agent.ts +31 -0
  77. package/source/components/switch/core/index.ts +2 -0
  78. package/source/components/switch/passport.json +68 -10
  79. package/source/components/switch/passport.manual.json +67 -0
  80. package/source/components/tabs/passport.json +50 -9
  81. package/source/components/tabs/passport.manual.json +50 -0
  82. package/source/components/textarea/core/agent.ts +34 -0
  83. package/source/components/textarea/core/index.ts +2 -0
  84. package/source/components/textarea/passport.json +83 -10
  85. package/source/components/textarea/passport.manual.json +82 -0
  86. package/source/components/toaster/passport.json +41 -8
  87. package/source/components/toaster/passport.manual.json +42 -0
  88. package/source/components/tooltip/passport.json +39 -6
  89. package/source/components/tooltip/passport.manual.json +41 -0
  90. package/tools/mcp/README.md +9 -1
  91. package/tools/mcp/design.cjs +6 -1
  92. package/tools/mcp/figma.cjs +49 -9
  93. package/tools/mcp/install.cjs +6 -1
  94. package/tools/mcp/server.cjs +14 -2
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Writing to a field the way a person does.
3
+ *
4
+ * This is the one file under `agent/` that needs a browser. Everything else
5
+ * here validates, and validation runs anywhere; executing has to touch
6
+ * something real. It is kept separate so the split is visible rather than
7
+ * discovered when a server build fails.
8
+ *
9
+ * Why not simply assign `input.value = x`: React tracks the last value it
10
+ * wrote on the DOM node, and an assignment that bypasses the prototype setter
11
+ * leaves that tracker in step with the new value. React then concludes nothing
12
+ * changed and never fires `onChange`, so a controlled field snaps back on the
13
+ * next render. Going through the prototype setter and dispatching a real
14
+ * bubbling event is what makes the change indistinguishable from typing — to
15
+ * React, to form libraries, and to any plain listener.
16
+ *
17
+ * `Input.clear()` has worked this way since before there was an agent runtime.
18
+ * This is the same technique, generalised to textareas and to checkedness.
19
+ */
20
+
21
+ type Writable = HTMLInputElement | HTMLTextAreaElement;
22
+
23
+ /** The prototype that owns the real setter for this element. */
24
+ const prototypeOf = (element: Writable) =>
25
+ element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
26
+
27
+ function nativeSetter(element: Writable, property: "value" | "checked") {
28
+ return Object.getOwnPropertyDescriptor(prototypeOf(element), property)?.set;
29
+ }
30
+
31
+ /**
32
+ * Set a field's text through the prototype setter, then fire the events a
33
+ * person typing would have fired.
34
+ */
35
+ export function writeValue(element: Writable, value: string): void {
36
+ nativeSetter(element, "value")?.call(element, value);
37
+ element.dispatchEvent(new Event("input", { bubbles: true }));
38
+ element.dispatchEvent(new Event("change", { bubbles: true }));
39
+ }
40
+
41
+ /**
42
+ * Set a checkbox or radio, as a click would.
43
+ *
44
+ * `checked` rather than `value`, and a `click` is not used: clicking a radio
45
+ * that is already selected does nothing, and clicking a disabled one is
46
+ * silently dropped. Setting the property and announcing it is both more
47
+ * predictable and easier to reason about from a validator's point of view —
48
+ * by the time this runs, the policy layer has already decided it is allowed.
49
+ */
50
+ export function writeChecked(element: HTMLInputElement, checked: boolean): void {
51
+ nativeSetter(element, "checked")?.call(element, checked);
52
+ element.dispatchEvent(new Event("input", { bubbles: true }));
53
+ element.dispatchEvent(new Event("change", { bubbles: true }));
54
+ }
55
+
56
+ /** True when the element can still be written to at all. */
57
+ export const writable = (element: Writable | null | undefined): element is Writable =>
58
+ Boolean(element) && !(element as Writable).disabled && !(element as Writable).readOnly;
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ /*
4
+ * Make a field agent-operable without changing the field.
5
+ *
6
+ * The DataGrid takes an `agent` prop because a grid's runtime needs to see the
7
+ * data, the columns and the live state to say anything useful. A field needs
8
+ * none of that: its contract is its own props, and its executor is the ref the
9
+ * caller already holds. So this is a hook rather than a prop, and no component
10
+ * had to grow an API to support it.
11
+ *
12
+ * const ref = useRef<HTMLInputElement>(null);
13
+ * const agent = useFieldAgent({
14
+ * contract: { component: "Input", kind: "text", operations: ["setValue", "clear", "focus"], maxLength: 80 },
15
+ * target: ref,
16
+ * });
17
+ *
18
+ * await agent.run({ action: "setValue", value: "Ada Lovelace" });
19
+ *
20
+ * `target` is whatever that component's ref gives you. Input, Textarea,
21
+ * NumberInput, Checkbox, Switch and Radio hand back the DOM element; Combobox
22
+ * and DatePicker hand back a handle. The hook tells them apart and nothing
23
+ * above it has to care.
24
+ */
25
+
26
+ import { useMemo, useRef } from "react";
27
+ import {
28
+ createFieldAgent,
29
+ domFieldExecutors,
30
+ handleFieldExecutors,
31
+ type FieldAgent,
32
+ type FieldContract,
33
+ type FieldHandleTarget,
34
+ type RegisteredExecutor,
35
+ } from "../core/agent";
36
+
37
+ type Target =
38
+ | { readonly current: HTMLInputElement | HTMLTextAreaElement | null }
39
+ | { readonly current: FieldHandleTarget | null };
40
+
41
+ export interface UseFieldAgentOptions {
42
+ /** What this instance will accept. Rebuild it from props as they change. */
43
+ readonly contract: FieldContract;
44
+ /** The component's ref: a DOM element for most fields, a handle for some. */
45
+ readonly target: Target;
46
+ }
47
+
48
+ /** A handle, as opposed to a DOM node. Checked by capability, not by instanceof. */
49
+ const isHandle = (value: unknown): value is FieldHandleTarget =>
50
+ Boolean(value) && typeof (value as FieldHandleTarget).setValue === "function";
51
+
52
+ export function useFieldAgent({ contract, target }: UseFieldAgentOptions): FieldAgent {
53
+ // The ref object is stable; its contents are not, and the executors read
54
+ // them at call time rather than closing over whatever was there on mount.
55
+ const latest = useRef(target);
56
+ latest.current = target;
57
+
58
+ return useMemo(() => {
59
+ const read = () => latest.current.current as unknown;
60
+
61
+ /*
62
+ * Which kind of target this is cannot be decided here: a ref is null on
63
+ * the first render, and deciding then would type every component as a DOM
64
+ * field and never revisit it. Both registries are built, and the choice is
65
+ * made inside `execute`, by which point the ref is populated.
66
+ */
67
+ const dom = domFieldExecutors(() => read() as HTMLInputElement | HTMLTextAreaElement | null, {
68
+ checkable: contract.kind === "boolean",
69
+ });
70
+ const handle = handleFieldExecutors(() => read() as FieldHandleTarget | null);
71
+
72
+ const executors = new Map<string, RegisteredExecutor<FieldContract>>();
73
+ for (const name of new Set([...dom.keys(), ...handle.keys()])) {
74
+ executors.set(name, {
75
+ operation: name,
76
+ execute: (input, context) => {
77
+ const chosen = isHandle(read()) ? handle.get(name) : dom.get(name);
78
+ if (!chosen) throw new Error(`${contract.component} cannot ${name}.`);
79
+ return chosen.execute(input, context);
80
+ },
81
+ } as RegisteredExecutor<FieldContract>);
82
+ }
83
+
84
+ return createFieldAgent({ contract, executors });
85
+ // The contract is the dependency that matters: a field that has just
86
+ // become read-only must stop accepting writes on the very next render.
87
+ }, [contract]);
88
+ }
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.2",
12
+ "libraryVersion": "2.7.0",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/lines.ts",
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "sourceHash": "sha256:c43f854a600cef0e2704513508c60910dbe3938df4b33ec6f2f614b8743055b3"
23
23
  },
24
+ "purpose": "Placeholders in the shape of the content that is loading, plus an Empty state for when there turns out to be nothing.",
24
25
  "props": [
25
26
  {
26
27
  "name": "actions",
@@ -234,20 +235,50 @@
234
235
  }
235
236
  ],
236
237
  "accessibility": {
237
- "supplied": [],
238
- "required": []
238
+ "supplied": [
239
+ "placeholders are hidden from assistive technology, so a screen reader is not read a wall of meaningless boxes",
240
+ "the loading condition is announced once by the region that owns it, rather than by every bar",
241
+ "Empty renders a heading and an action, not just an illustration"
242
+ ],
243
+ "required": [
244
+ "announce loading on the container, since the skeletons themselves are silent by design",
245
+ "an Empty state that says what to do next, not only that there is nothing"
246
+ ]
247
+ },
248
+ "composition": {
249
+ "notes": "Skeletons should match the shape of what replaces them. A layout that jumps when the content arrives is worse than a spinner."
239
250
  },
240
- "composition": {},
241
251
  "operations": {},
242
252
  "intentDomains": [],
243
- "safeMutations": {},
253
+ "safeMutations": {
254
+ "allowed": [
255
+ "variant",
256
+ "animation",
257
+ "width",
258
+ "height",
259
+ "radius",
260
+ "lines",
261
+ "lastLineWidth",
262
+ "size",
263
+ "icon"
264
+ ],
265
+ "forbidden": [
266
+ "label",
267
+ "description",
268
+ "actions"
269
+ ],
270
+ "rationale": "Shape and animation are entirely presentational, which is unusual here and is why almost everything is allowed. The Empty state's words are content."
271
+ },
244
272
  "examples": [],
245
273
  "coverage": {
246
274
  "propsTotal": 13,
247
275
  "propsDescribed": 13,
248
276
  "describedPct": 100,
249
277
  "authoredSections": [
250
- "accessibility"
278
+ "accessibility",
279
+ "composition",
280
+ "purpose",
281
+ "safeMutations"
251
282
  ]
252
283
  }
253
284
  }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Skeleton",
4
+ "purpose": "Placeholders in the shape of the content that is loading, plus an Empty state for when there turns out to be nothing.",
5
+ "intentDomains": [],
6
+ "accessibility": {
7
+ "supplied": [
8
+ "placeholders are hidden from assistive technology, so a screen reader is not read a wall of meaningless boxes",
9
+ "the loading condition is announced once by the region that owns it, rather than by every bar",
10
+ "Empty renders a heading and an action, not just an illustration"
11
+ ],
12
+ "required": [
13
+ "announce loading on the container, since the skeletons themselves are silent by design",
14
+ "an Empty state that says what to do next, not only that there is nothing"
15
+ ]
16
+ },
17
+ "composition": {
18
+ "notes": "Skeletons should match the shape of what replaces them. A layout that jumps when the content arrives is worse than a spinner."
19
+ },
20
+ "safeMutations": {
21
+ "allowed": [
22
+ "variant",
23
+ "animation",
24
+ "width",
25
+ "height",
26
+ "radius",
27
+ "lines",
28
+ "lastLineWidth",
29
+ "size",
30
+ "icon"
31
+ ],
32
+ "forbidden": [
33
+ "label",
34
+ "description",
35
+ "actions"
36
+ ],
37
+ "rationale": "Shape and animation are entirely presentational, which is unusual here and is why almost everything is allowed. The Empty state's words are content."
38
+ }
39
+ }
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.2",
11
+ "libraryVersion": "2.7.0",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "sourceHash": "sha256:35c2de8415540772f688151782dff3c84c49b7a3d03fd7e575b3d4d4d4e93d85"
23
23
  },
24
+ "purpose": "A loading indicator on its own, beside a label or covering content, with a delay before it appears and a minimum time on screen.",
24
25
  "props": [
25
26
  {
26
27
  "name": "aria-describedby",
@@ -74,7 +75,8 @@
74
75
  "default": 0,
75
76
  "origin": "derived",
76
77
  "description": "Milliseconds to wait before showing, so fast work doesn't flash. Default 0.",
77
- "descriptionOrigin": "jsdoc"
78
+ "descriptionOrigin": "jsdoc",
79
+ "note": "Nothing renders before this elapses, which is what stops a spinner flashing on fast connections."
78
80
  },
79
81
  {
80
82
  "name": "hidden",
@@ -226,20 +228,45 @@
226
228
  }
227
229
  ],
228
230
  "accessibility": {
229
- "supplied": [],
230
- "required": []
231
+ "supplied": [
232
+ "the spinner graphic is aria-hidden and the label carries the announcement",
233
+ "a delay before appearing, so fast work never flashes a spinner",
234
+ "a minimum duration once shown, so it does not flicker out instantly"
235
+ ],
236
+ "required": [
237
+ "a `label` saying what is loading, or `showLabel` where it should be visible too",
238
+ "use Progress instead when the length is known"
239
+ ]
240
+ },
241
+ "composition": {
242
+ "notes": "Covering content is a variant of this component rather than a separate overlay."
231
243
  },
232
- "composition": {},
233
244
  "operations": {},
234
245
  "intentDomains": [],
235
- "safeMutations": {},
246
+ "safeMutations": {
247
+ "allowed": [
248
+ "size",
249
+ "variant",
250
+ "delay",
251
+ "minDuration",
252
+ "showLabel",
253
+ "label"
254
+ ],
255
+ "forbidden": [
256
+ "loading"
257
+ ],
258
+ "rationale": "Everything here is presentation except whether the work is happening."
259
+ },
236
260
  "examples": [],
237
261
  "coverage": {
238
262
  "propsTotal": 8,
239
263
  "propsDescribed": 8,
240
264
  "describedPct": 100,
241
265
  "authoredSections": [
242
- "accessibility"
266
+ "accessibility",
267
+ "composition",
268
+ "purpose",
269
+ "safeMutations"
243
270
  ]
244
271
  }
245
272
  }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Spinner",
4
+ "purpose": "A loading indicator on its own, beside a label or covering content, with a delay before it appears and a minimum time on screen.",
5
+ "intentDomains": [],
6
+ "accessibility": {
7
+ "supplied": [
8
+ "the spinner graphic is aria-hidden and the label carries the announcement",
9
+ "a delay before appearing, so fast work never flashes a spinner",
10
+ "a minimum duration once shown, so it does not flicker out instantly"
11
+ ],
12
+ "required": [
13
+ "a `label` saying what is loading, or `showLabel` where it should be visible too",
14
+ "use Progress instead when the length is known"
15
+ ]
16
+ },
17
+ "composition": {
18
+ "notes": "Covering content is a variant of this component rather than a separate overlay."
19
+ },
20
+ "safeMutations": {
21
+ "allowed": [
22
+ "size",
23
+ "variant",
24
+ "delay",
25
+ "minDuration",
26
+ "showLabel",
27
+ "label"
28
+ ],
29
+ "forbidden": [
30
+ "loading"
31
+ ],
32
+ "rationale": "Everything here is presentation except whether the work is happening."
33
+ },
34
+ "propNotes": {
35
+ "delay": "Nothing renders before this elapses, which is what stops a spinner flashing on fast connections."
36
+ }
37
+ }
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Switch's agent contract.
3
+ *
4
+ * A Switch applies its change as it is flipped, so a write here is the
5
+ * setting taking effect — not a draft the user will later submit.
6
+ *
7
+ * Pure: no React, no DOM. The contract is what this instance will accept, and
8
+ * the validator in shared/core/agent decides everything from it. Build it from
9
+ * props on every render so that a field which has just become read-only stops
10
+ * accepting writes immediately.
11
+ */
12
+
13
+ import type { FieldContract } from "../../shared/core/agent";
14
+
15
+ export interface SwitchAgentProps {
16
+ label?: string;
17
+ disabled?: boolean;
18
+ readOnly?: boolean;
19
+ }
20
+
21
+ export function switchContract(props: SwitchAgentProps = {}): FieldContract {
22
+ return {
23
+ component: "Switch",
24
+ kind: "boolean",
25
+ // No clear: a switch is on or off, and "neither" is not one of its states.
26
+ operations: ["setValue", "focus"],
27
+ label: props.label,
28
+ disabled: props.disabled,
29
+ readOnly: props.readOnly,
30
+ };
31
+ }
@@ -1,2 +1,4 @@
1
1
  export { adopt, idleToggle, request, settle, type ToggleState } from "./toggle";
2
2
  export type * from "./types";
3
+ // The agent contract: what a model may ask this field to do.
4
+ export { switchContract, type SwitchAgentProps } from "./agent";
@@ -8,8 +8,9 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.2",
11
+ "libraryVersion": "2.7.0",
12
12
  "files": [
13
+ "core/agent.ts",
13
14
  "core/index.ts",
14
15
  "core/toggle.ts",
15
16
  "core/types.ts",
@@ -18,8 +19,9 @@
18
19
  "react/locale.ts",
19
20
  "react/props.ts"
20
21
  ],
21
- "sourceHash": "sha256:b2007da7ec91cbad6bd7697048cc205d22e8b94035482a9f631a75e9dabfd0e4"
22
+ "sourceHash": "sha256:4e168aeb5ca0aef334176cc4edb1b15c63546bf415df867085dadf871f9e24af"
22
23
  },
24
+ "purpose": "A setting that takes effect as it is flipped, on a native checkbox with role=switch, with an optimistic pending state while the change is saved.",
23
25
  "props": [
24
26
  {
25
27
  "name": "aria-describedby",
@@ -190,7 +192,8 @@
190
192
  "default": false,
191
193
  "origin": "derived",
192
194
  "description": "Show the busy state and block presses from outside, e.g. while a form saves.",
193
- "descriptionOrigin": "jsdoc"
195
+ "descriptionOrigin": "jsdoc",
196
+ "note": "Managed when the change handler returns a promise. Setting it by hand opts out of that."
194
197
  },
195
198
  {
196
199
  "name": "localeText",
@@ -414,20 +417,75 @@
414
417
  }
415
418
  ],
416
419
  "accessibility": {
417
- "supplied": [],
418
- "required": []
420
+ "supplied": [
421
+ "role=switch on a native checkbox, with aria-checked tracking the state",
422
+ "Space toggles",
423
+ "the loading state is announced rather than silently blocking the control"
424
+ ],
425
+ "required": [
426
+ "a `label` saying what the setting is, phrased so that on and off are both obvious",
427
+ "do not use it where the change needs a Save button — that is a Checkbox"
428
+ ]
429
+ },
430
+ "composition": {
431
+ "mustNotContain": [
432
+ "label"
433
+ ],
434
+ "notes": "Renders its own label element."
435
+ },
436
+ "operations": {
437
+ "setValue": {
438
+ "summary": "Write a value into the field, as though a person had entered it.",
439
+ "effects": [
440
+ "replaces the field's current value",
441
+ "fires the host's change handler"
442
+ ],
443
+ "reversible": true
444
+ },
445
+ "focus": {
446
+ "summary": "Move keyboard focus to the field.",
447
+ "effects": [
448
+ "moves focus"
449
+ ],
450
+ "reversible": false
451
+ }
452
+ },
453
+ "intentDomains": [
454
+ "ActionIntent"
455
+ ],
456
+ "safeMutations": {
457
+ "allowed": [
458
+ "size",
459
+ "labelPosition",
460
+ "description"
461
+ ],
462
+ "forbidden": [
463
+ "checked",
464
+ "defaultChecked",
465
+ "value",
466
+ "name",
467
+ "required",
468
+ "disabled",
469
+ "readOnly",
470
+ "loading",
471
+ "onChange",
472
+ "onCheckedChange",
473
+ "error"
474
+ ],
475
+ "rationale": "A Switch applies its change immediately, so writing `checked` is performing the setting, not describing it. `loading` is owned by the component's own async handling."
419
476
  },
420
- "composition": {},
421
- "operations": {},
422
- "intentDomains": [],
423
- "safeMutations": {},
424
477
  "examples": [],
425
478
  "coverage": {
426
479
  "propsTotal": 11,
427
480
  "propsDescribed": 11,
428
481
  "describedPct": 100,
429
482
  "authoredSections": [
430
- "accessibility"
483
+ "accessibility",
484
+ "composition",
485
+ "intentDomains",
486
+ "operations",
487
+ "purpose",
488
+ "safeMutations"
431
489
  ]
432
490
  }
433
491
  }
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Switch",
4
+ "purpose": "A setting that takes effect as it is flipped, on a native checkbox with role=switch, with an optimistic pending state while the change is saved.",
5
+ "intentDomains": [
6
+ "ActionIntent"
7
+ ],
8
+ "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
9
+ "operations": {
10
+ "setValue": {
11
+ "summary": "Write a value into the field, as though a person had entered it.",
12
+ "effects": [
13
+ "replaces the field's current value",
14
+ "fires the host's change handler"
15
+ ],
16
+ "reversible": true
17
+ },
18
+ "focus": {
19
+ "summary": "Move keyboard focus to the field.",
20
+ "effects": [
21
+ "moves focus"
22
+ ],
23
+ "reversible": false
24
+ }
25
+ },
26
+ "accessibility": {
27
+ "supplied": [
28
+ "role=switch on a native checkbox, with aria-checked tracking the state",
29
+ "Space toggles",
30
+ "the loading state is announced rather than silently blocking the control"
31
+ ],
32
+ "required": [
33
+ "a `label` saying what the setting is, phrased so that on and off are both obvious",
34
+ "do not use it where the change needs a Save button — that is a Checkbox"
35
+ ]
36
+ },
37
+ "composition": {
38
+ "mustNotContain": [
39
+ "label"
40
+ ],
41
+ "notes": "Renders its own label element."
42
+ },
43
+ "safeMutations": {
44
+ "allowed": [
45
+ "size",
46
+ "labelPosition",
47
+ "description"
48
+ ],
49
+ "forbidden": [
50
+ "checked",
51
+ "defaultChecked",
52
+ "value",
53
+ "name",
54
+ "required",
55
+ "disabled",
56
+ "readOnly",
57
+ "loading",
58
+ "onChange",
59
+ "onCheckedChange",
60
+ "error"
61
+ ],
62
+ "rationale": "A Switch applies its change immediately, so writing `checked` is performing the setting, not describing it. `loading` is owned by the component's own async handling."
63
+ },
64
+ "propNotes": {
65
+ "loading": "Managed when the change handler returns a promise. Setting it by hand opts out of that."
66
+ }
67
+ }