@grampro/kit 2.6.3 → 2.7.1

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 +188 -0
  4. package/README.md +26 -7
  5. package/index.cjs +59 -59
  6. package/package.json +3 -3
  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 +23 -0
  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 +30 -2
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Input's agent contract.
3
+ *
4
+ * Its ref is the <input> element, so the write goes through the element
5
+ * itself, exactly as `clear()` has always done.
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 InputAgentProps {
16
+ label?: string;
17
+ disabled?: boolean;
18
+ readOnly?: boolean;
19
+ required?: boolean;
20
+ maxLength?: number;
21
+ pattern?: string;
22
+ }
23
+
24
+ export function inputContract(props: InputAgentProps = {}): FieldContract {
25
+ return {
26
+ component: "Input",
27
+ kind: "text",
28
+ operations: ["setValue", "clear", "focus"],
29
+ label: props.label,
30
+ disabled: props.disabled,
31
+ readOnly: props.readOnly,
32
+ required: props.required,
33
+ maxLength: props.maxLength,
34
+ pattern: props.pattern,
35
+ };
36
+ }
@@ -9,3 +9,5 @@ export {
9
9
  separatorsAfter,
10
10
  } from "./otp";
11
11
  export type * from "./types";
12
+ // The agent contract: what a model may ask this field to do.
13
+ export { inputContract, type InputAgentProps } from "./agent";
@@ -9,8 +9,9 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.3",
12
+ "libraryVersion": "2.7.1",
13
13
  "files": [
14
+ "core/agent.ts",
14
15
  "core/count.ts",
15
16
  "core/index.ts",
16
17
  "core/otp.ts",
@@ -23,8 +24,9 @@
23
24
  "react/locale.ts",
24
25
  "react/props.ts"
25
26
  ],
26
- "sourceHash": "sha256:50ff11f4a9255189704b330123d35d4f4e3550bd4cf2eed2d0735c92ba876b03"
27
+ "sourceHash": "sha256:6d1ef1229a82962e2bd3bf32a15de6f7d9e43c2270e65df9f7303cd74dd479d4"
27
28
  },
29
+ "purpose": "A single-line text field with its label, hint and error wired together. OtpInput is the same field split into one box per character for a one-time code.",
28
30
  "props": [
29
31
  {
30
32
  "name": "aria-describedby",
@@ -130,7 +132,8 @@
130
132
  "required": false,
131
133
  "origin": "derived",
132
134
  "description": "Message below the field; also marks it invalid.",
133
- "descriptionOrigin": "jsdoc"
135
+ "descriptionOrigin": "jsdoc",
136
+ "note": "Set by the host after validating. The component never derives it."
134
137
  },
135
138
  {
136
139
  "name": "form",
@@ -205,7 +208,8 @@
205
208
  "default": 6,
206
209
  "origin": "derived",
207
210
  "description": "Number of characters. Default 6.",
208
- "descriptionOrigin": "jsdoc"
211
+ "descriptionOrigin": "jsdoc",
212
+ "note": "OtpInput only: the number of boxes, which is also the expected code length."
209
213
  },
210
214
  {
211
215
  "name": "localeText",
@@ -222,7 +226,8 @@
222
226
  "default": false,
223
227
  "origin": "derived",
224
228
  "description": "Show dots instead of the characters.",
225
- "descriptionOrigin": "jsdoc"
229
+ "descriptionOrigin": "jsdoc",
230
+ "note": "Shapes what can be typed. Changing it can make an already-valid value unreachable."
226
231
  },
227
232
  {
228
233
  "name": "max",
@@ -523,20 +528,92 @@
523
528
  }
524
529
  ],
525
530
  "accessibility": {
526
- "supplied": [],
527
- "required": []
531
+ "supplied": [
532
+ "the label is a real <label> bound to the input, so clicking it focuses the field",
533
+ "description and error are linked by aria-describedby",
534
+ "aria-invalid while `error` is set",
535
+ "the character counter is a live region when `showCount` is on",
536
+ "OtpInput moves focus between boxes and handles paste of a whole code"
537
+ ],
538
+ "required": [
539
+ "a `label`, or `aria-label` when the field is visually unlabelled",
540
+ "`type` matching the data — email, tel, url — so the right keyboard and autofill appear",
541
+ "`autoComplete` on anything a browser or password manager should fill"
542
+ ]
543
+ },
544
+ "composition": {
545
+ "mustNotContain": [
546
+ "label",
547
+ "input"
548
+ ],
549
+ "notes": "The component renders its own <label> and <input>. Wrapping it in another label binds the text to the wrong control and breaks the click target."
550
+ },
551
+ "operations": {
552
+ "setValue": {
553
+ "summary": "Write a value into the field, as though a person had entered it.",
554
+ "effects": [
555
+ "replaces the field's current value",
556
+ "fires the host's change handler"
557
+ ],
558
+ "reversible": true
559
+ },
560
+ "clear": {
561
+ "summary": "Empty the field.",
562
+ "effects": [
563
+ "discards the field's current value",
564
+ "fires the host's change handler"
565
+ ],
566
+ "reversible": true
567
+ },
568
+ "focus": {
569
+ "summary": "Move keyboard focus to the field.",
570
+ "effects": [
571
+ "moves focus"
572
+ ],
573
+ "reversible": false
574
+ }
575
+ },
576
+ "intentDomains": [
577
+ "ActionIntent"
578
+ ],
579
+ "safeMutations": {
580
+ "allowed": [
581
+ "size",
582
+ "placeholder",
583
+ "description",
584
+ "leading",
585
+ "trailing",
586
+ "clearable",
587
+ "showCount",
588
+ "revealPassword"
589
+ ],
590
+ "forbidden": [
591
+ "value",
592
+ "defaultValue",
593
+ "name",
594
+ "type",
595
+ "required",
596
+ "disabled",
597
+ "readOnly",
598
+ "pattern",
599
+ "mask",
600
+ "onValueChange",
601
+ "error"
602
+ ],
603
+ "rationale": "Appearance and affordances are safe. `value` and `name` are the form's data, `type`/`pattern`/`mask` decide what is accepted, and `error` is the host's verdict on validity — an agent that writes it is claiming a validation result it did not compute."
528
604
  },
529
- "composition": {},
530
- "operations": {},
531
- "intentDomains": [],
532
- "safeMutations": {},
533
605
  "examples": [],
534
606
  "coverage": {
535
607
  "propsTotal": 18,
536
608
  "propsDescribed": 16,
537
609
  "describedPct": 89,
538
610
  "authoredSections": [
539
- "accessibility"
611
+ "accessibility",
612
+ "composition",
613
+ "intentDomains",
614
+ "operations",
615
+ "purpose",
616
+ "safeMutations"
540
617
  ]
541
618
  }
542
619
  }
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Input",
4
+ "purpose": "A single-line text field with its label, hint and error wired together. OtpInput is the same field split into one box per character for a one-time code.",
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
+ "clear": {
19
+ "summary": "Empty the field.",
20
+ "effects": [
21
+ "discards the field's current value",
22
+ "fires the host's change handler"
23
+ ],
24
+ "reversible": true
25
+ },
26
+ "focus": {
27
+ "summary": "Move keyboard focus to the field.",
28
+ "effects": [
29
+ "moves focus"
30
+ ],
31
+ "reversible": false
32
+ }
33
+ },
34
+ "accessibility": {
35
+ "supplied": [
36
+ "the label is a real <label> bound to the input, so clicking it focuses the field",
37
+ "description and error are linked by aria-describedby",
38
+ "aria-invalid while `error` is set",
39
+ "the character counter is a live region when `showCount` is on",
40
+ "OtpInput moves focus between boxes and handles paste of a whole code"
41
+ ],
42
+ "required": [
43
+ "a `label`, or `aria-label` when the field is visually unlabelled",
44
+ "`type` matching the data — email, tel, url — so the right keyboard and autofill appear",
45
+ "`autoComplete` on anything a browser or password manager should fill"
46
+ ]
47
+ },
48
+ "composition": {
49
+ "mustNotContain": [
50
+ "label",
51
+ "input"
52
+ ],
53
+ "notes": "The component renders its own <label> and <input>. Wrapping it in another label binds the text to the wrong control and breaks the click target."
54
+ },
55
+ "safeMutations": {
56
+ "allowed": [
57
+ "size",
58
+ "placeholder",
59
+ "description",
60
+ "leading",
61
+ "trailing",
62
+ "clearable",
63
+ "showCount",
64
+ "revealPassword"
65
+ ],
66
+ "forbidden": [
67
+ "value",
68
+ "defaultValue",
69
+ "name",
70
+ "type",
71
+ "required",
72
+ "disabled",
73
+ "readOnly",
74
+ "pattern",
75
+ "mask",
76
+ "onValueChange",
77
+ "error"
78
+ ],
79
+ "rationale": "Appearance and affordances are safe. `value` and `name` are the form's data, `type`/`pattern`/`mask` decide what is accepted, and `error` is the host's verdict on validity — an agent that writes it is claiming a validation result it did not compute."
80
+ },
81
+ "propNotes": {
82
+ "mask": "Shapes what can be typed. Changing it can make an already-valid value unreachable.",
83
+ "length": "OtpInput only: the number of boxes, which is also the expected code length.",
84
+ "error": "Set by the host after validating. The component never derives it."
85
+ }
86
+ }
@@ -16,7 +16,7 @@
16
16
  ]
17
17
  },
18
18
  "source": {
19
- "libraryVersion": "2.6.3",
19
+ "libraryVersion": "2.7.1",
20
20
  "files": [
21
21
  "core/index.ts",
22
22
  "core/navigation.ts",
@@ -31,6 +31,7 @@
31
31
  ],
32
32
  "sourceHash": "sha256:9a6248a4986051f6506746c084b9e89728fecd0a0b430d1962f5f3ed544ce1e5"
33
33
  },
34
+ "purpose": "A menu of commands with arrow keys, typeahead, checkbox and radio items and submenus, following the WAI-ARIA menu pattern.",
34
35
  "props": [
35
36
  {
36
37
  "name": "align",
@@ -105,7 +106,8 @@
105
106
  "default": false,
106
107
  "origin": "derived",
107
108
  "description": "Styles it as a destructive action.",
108
- "descriptionOrigin": "jsdoc"
109
+ "descriptionOrigin": "jsdoc",
110
+ "note": "Styles the item as dangerous. Remove it only if the item stops being dangerous."
109
111
  },
110
112
  {
111
113
  "name": "disabled",
@@ -216,7 +218,8 @@
216
218
  "required": false,
217
219
  "origin": "derived",
218
220
  "description": "A hint such as ⌘K. The menu shows it; binding the key is up to you.",
219
- "descriptionOrigin": "jsdoc"
221
+ "descriptionOrigin": "jsdoc",
222
+ "note": "Displayed only. The component does not bind the key; the application does."
220
223
  },
221
224
  {
222
225
  "name": "side",
@@ -319,20 +322,72 @@
319
322
  }
320
323
  ],
321
324
  "accessibility": {
322
- "supplied": [],
323
- "required": []
325
+ "supplied": [
326
+ "the menu pattern: role=menu with menuitem, menuitemcheckbox and menuitemradio children",
327
+ "arrow keys move, typeahead jumps to a matching item, Escape closes and returns focus to the trigger",
328
+ "submenus open on ArrowRight and close on ArrowLeft",
329
+ "a disabled item stays focusable so it can be read, and is not activated"
330
+ ],
331
+ "required": [
332
+ "an accessible name on the trigger when it is an icon",
333
+ "`destructive` on items that delete, so they are not styled as ordinary commands"
334
+ ]
335
+ },
336
+ "composition": {
337
+ "allowedChildren": [
338
+ "MenuItem",
339
+ "MenuCheckboxItem",
340
+ "MenuRadioItem",
341
+ "MenuGroup",
342
+ "MenuSeparator",
343
+ "MenuSub"
344
+ ],
345
+ "mustNotContain": [
346
+ "input",
347
+ "textarea",
348
+ "Modal"
349
+ ],
350
+ "notes": "A menu holds commands, not fields. The roving focus of the menu pattern and a text field's own key handling cannot both own the arrow keys."
324
351
  },
325
- "composition": {},
326
352
  "operations": {},
327
- "intentDomains": [],
328
- "safeMutations": {},
353
+ "intentDomains": [
354
+ "ActionIntent"
355
+ ],
356
+ "safeMutations": {
357
+ "allowed": [
358
+ "side",
359
+ "align",
360
+ "gap",
361
+ "icon",
362
+ "shortcut",
363
+ "closeOnSelect"
364
+ ],
365
+ "forbidden": [
366
+ "open",
367
+ "defaultOpen",
368
+ "onOpenChange",
369
+ "onSelect",
370
+ "onValueChange",
371
+ "onCheckedChange",
372
+ "value",
373
+ "checked",
374
+ "trigger",
375
+ "destructive",
376
+ "disabled"
377
+ ],
378
+ "rationale": "Placement and ornament are safe. `onSelect` is the command itself, and `destructive` is a warning an agent must not remove."
379
+ },
329
380
  "examples": [],
330
381
  "coverage": {
331
382
  "propsTotal": 20,
332
383
  "propsDescribed": 17,
333
384
  "describedPct": 85,
334
385
  "authoredSections": [
335
- "accessibility"
386
+ "accessibility",
387
+ "composition",
388
+ "intentDomains",
389
+ "purpose",
390
+ "safeMutations"
336
391
  ]
337
392
  }
338
393
  }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Menu",
4
+ "purpose": "A menu of commands with arrow keys, typeahead, checkbox and radio items and submenus, following the WAI-ARIA menu pattern.",
5
+ "intentDomains": [
6
+ "ActionIntent"
7
+ ],
8
+ "accessibility": {
9
+ "supplied": [
10
+ "the menu pattern: role=menu with menuitem, menuitemcheckbox and menuitemradio children",
11
+ "arrow keys move, typeahead jumps to a matching item, Escape closes and returns focus to the trigger",
12
+ "submenus open on ArrowRight and close on ArrowLeft",
13
+ "a disabled item stays focusable so it can be read, and is not activated"
14
+ ],
15
+ "required": [
16
+ "an accessible name on the trigger when it is an icon",
17
+ "`destructive` on items that delete, so they are not styled as ordinary commands"
18
+ ]
19
+ },
20
+ "composition": {
21
+ "allowedChildren": [
22
+ "MenuItem",
23
+ "MenuCheckboxItem",
24
+ "MenuRadioItem",
25
+ "MenuGroup",
26
+ "MenuSeparator",
27
+ "MenuSub"
28
+ ],
29
+ "mustNotContain": [
30
+ "input",
31
+ "textarea",
32
+ "Modal"
33
+ ],
34
+ "notes": "A menu holds commands, not fields. The roving focus of the menu pattern and a text field's own key handling cannot both own the arrow keys."
35
+ },
36
+ "safeMutations": {
37
+ "allowed": [
38
+ "side",
39
+ "align",
40
+ "gap",
41
+ "icon",
42
+ "shortcut",
43
+ "closeOnSelect"
44
+ ],
45
+ "forbidden": [
46
+ "open",
47
+ "defaultOpen",
48
+ "onOpenChange",
49
+ "onSelect",
50
+ "onValueChange",
51
+ "onCheckedChange",
52
+ "value",
53
+ "checked",
54
+ "trigger",
55
+ "destructive",
56
+ "disabled"
57
+ ],
58
+ "rationale": "Placement and ornament are safe. `onSelect` is the command itself, and `destructive` is a warning an agent must not remove."
59
+ },
60
+ "propNotes": {
61
+ "shortcut": "Displayed only. The component does not bind the key; the application does.",
62
+ "destructive": "Styles the item as dangerous. Remove it only if the item stops being dangerous."
63
+ }
64
+ }
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.3",
11
+ "libraryVersion": "2.7.1",
12
12
  "files": [
13
13
  "core/dismiss.ts",
14
14
  "core/index.ts",
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "sourceHash": "sha256:078ceea5f6cf953d95a999fd958a6b2ed8a61e3c9060665eeca4efd53fb70413"
23
23
  },
24
+ "purpose": "Forms, details and drawers above the page, on the native <dialog> element, with focus trapped inside and the rest of the page inert.",
24
25
  "props": [
25
26
  {
26
27
  "name": "aria-label",
@@ -70,7 +71,8 @@
70
71
  "default": true,
71
72
  "origin": "derived",
72
73
  "description": "Default true.",
73
- "descriptionOrigin": "jsdoc"
74
+ "descriptionOrigin": "jsdoc",
75
+ "note": "Turning it off should be reserved for work that would be lost. A user who cannot dismiss a dialog is stuck."
74
76
  },
75
77
  {
76
78
  "name": "closeOnEscape",
@@ -150,7 +152,8 @@
150
152
  "required": false,
151
153
  "origin": "derived",
152
154
  "description": "Return `false` (or a promise of it) to keep the modal open, e.g. for unsaved changes.",
153
- "descriptionOrigin": "jsdoc"
155
+ "descriptionOrigin": "jsdoc",
156
+ "note": "Return false to refuse a close, for unsaved work. It runs for Escape and the backdrop too."
154
157
  },
155
158
  {
156
159
  "name": "onOpenChange",
@@ -276,20 +279,60 @@
276
279
  }
277
280
  ],
278
281
  "accessibility": {
279
- "supplied": [],
280
- "required": []
282
+ "supplied": [
283
+ "native <dialog> semantics: the top layer, :modal, and the rest of the page genuinely inert rather than merely covered",
284
+ "focus moves in on open and returns to the trigger on close",
285
+ "Escape closes, unless onBeforeClose refuses",
286
+ "the heading is linked as the dialog's accessible name"
287
+ ],
288
+ "required": [
289
+ "a `description` or a heading — a modal with no name announces as nothing",
290
+ "`initialFocus` when the first focusable element is not the one a user should land on"
291
+ ]
292
+ },
293
+ "composition": {
294
+ "mustNotContain": [
295
+ "Modal",
296
+ "dialog"
297
+ ],
298
+ "notes": "A modal inside a modal puts two dialogs in the top layer and the focus trap of the inner one fights the outer. Use one modal and change its contents. Short questions belong in Dialog, not here."
281
299
  },
282
- "composition": {},
283
300
  "operations": {},
284
- "intentDomains": [],
285
- "safeMutations": {},
301
+ "intentDomains": [
302
+ "ActionIntent"
303
+ ],
304
+ "safeMutations": {
305
+ "allowed": [
306
+ "size",
307
+ "placement",
308
+ "closeButton",
309
+ "description"
310
+ ],
311
+ "forbidden": [
312
+ "open",
313
+ "defaultOpen",
314
+ "onOpenChange",
315
+ "onBeforeClose",
316
+ "closeOnBackdrop",
317
+ "closeOnEscape",
318
+ "initialFocus",
319
+ "keepMounted",
320
+ "footer",
321
+ "children"
322
+ ],
323
+ "rationale": "Size and placement are presentation. `open` is the application's state, and the dismissal props decide whether a user can get out — removing an escape route is never a cosmetic change."
324
+ },
286
325
  "examples": [],
287
326
  "coverage": {
288
327
  "propsTotal": 17,
289
328
  "propsDescribed": 14,
290
329
  "describedPct": 82,
291
330
  "authoredSections": [
292
- "accessibility"
331
+ "accessibility",
332
+ "composition",
333
+ "intentDomains",
334
+ "purpose",
335
+ "safeMutations"
293
336
  ]
294
337
  }
295
338
  }
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Modal",
4
+ "purpose": "Forms, details and drawers above the page, on the native <dialog> element, with focus trapped inside and the rest of the page inert.",
5
+ "intentDomains": [
6
+ "ActionIntent"
7
+ ],
8
+ "accessibility": {
9
+ "supplied": [
10
+ "native <dialog> semantics: the top layer, :modal, and the rest of the page genuinely inert rather than merely covered",
11
+ "focus moves in on open and returns to the trigger on close",
12
+ "Escape closes, unless onBeforeClose refuses",
13
+ "the heading is linked as the dialog's accessible name"
14
+ ],
15
+ "required": [
16
+ "a `description` or a heading — a modal with no name announces as nothing",
17
+ "`initialFocus` when the first focusable element is not the one a user should land on"
18
+ ]
19
+ },
20
+ "composition": {
21
+ "mustNotContain": [
22
+ "Modal",
23
+ "dialog"
24
+ ],
25
+ "notes": "A modal inside a modal puts two dialogs in the top layer and the focus trap of the inner one fights the outer. Use one modal and change its contents. Short questions belong in Dialog, not here."
26
+ },
27
+ "safeMutations": {
28
+ "allowed": [
29
+ "size",
30
+ "placement",
31
+ "closeButton",
32
+ "description"
33
+ ],
34
+ "forbidden": [
35
+ "open",
36
+ "defaultOpen",
37
+ "onOpenChange",
38
+ "onBeforeClose",
39
+ "closeOnBackdrop",
40
+ "closeOnEscape",
41
+ "initialFocus",
42
+ "keepMounted",
43
+ "footer",
44
+ "children"
45
+ ],
46
+ "rationale": "Size and placement are presentation. `open` is the application's state, and the dismissal props decide whether a user can get out — removing an escape route is never a cosmetic change."
47
+ },
48
+ "propNotes": {
49
+ "closeOnBackdrop": "Turning it off should be reserved for work that would be lost. A user who cannot dismiss a dialog is stuck.",
50
+ "onBeforeClose": "Return false to refuse a close, for unsaved work. It runs for Escape and the backdrop too."
51
+ }
52
+ }
@@ -0,0 +1,38 @@
1
+ /*
2
+ * NumberInput's agent contract.
3
+ *
4
+ * `min`, `max` and `step` become policy: a proposal outside them is refused
5
+ * by name rather than silently clamped, so the caller learns which rule it hit.
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 NumberInputAgentProps {
16
+ label?: string;
17
+ disabled?: boolean;
18
+ readOnly?: boolean;
19
+ required?: boolean;
20
+ min?: number;
21
+ max?: number;
22
+ step?: number;
23
+ }
24
+
25
+ export function numberInputContract(props: NumberInputAgentProps = {}): FieldContract {
26
+ return {
27
+ component: "NumberInput",
28
+ kind: "number",
29
+ operations: ["setValue", "clear", "focus"],
30
+ label: props.label,
31
+ disabled: props.disabled,
32
+ readOnly: props.readOnly,
33
+ required: props.required,
34
+ min: props.min,
35
+ max: props.max,
36
+ step: props.step,
37
+ };
38
+ }
@@ -2,3 +2,5 @@ export { formatNumber, numberFormat } from "./format";
2
2
  export { localeParts, parseNumber, toEditText, type NumberLocaleParts } from "./parse";
3
3
  export { clamp, decimalsOf, round, snapToStep, stepBy } from "./step";
4
4
  export type * from "./types";
5
+ // The agent contract: what a model may ask this field to do.
6
+ export { numberInputContract, type NumberInputAgentProps } from "./agent";