@handamade/psi-react 0.20.0 → 0.22.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 (115) hide show
  1. package/README.md +1 -1
  2. package/dist/AppShell/AppShell.d.ts +51 -0
  3. package/dist/AppShell/AppShell.d.ts.map +1 -0
  4. package/dist/Banner/Banner.d.ts +35 -0
  5. package/dist/Banner/Banner.d.ts.map +1 -0
  6. package/dist/Button/Button.d.ts +4 -0
  7. package/dist/Button/Button.d.ts.map +1 -1
  8. package/dist/CopyButton/CopyButton.d.ts +23 -0
  9. package/dist/CopyButton/CopyButton.d.ts.map +1 -0
  10. package/dist/CursorPagination/CursorPagination.d.ts +29 -0
  11. package/dist/CursorPagination/CursorPagination.d.ts.map +1 -0
  12. package/dist/Field/Field.d.ts +16 -2
  13. package/dist/Field/Field.d.ts.map +1 -1
  14. package/dist/Field/useFieldControl.d.ts +40 -0
  15. package/dist/Field/useFieldControl.d.ts.map +1 -0
  16. package/dist/InlineAlert/InlineAlert.d.ts +32 -0
  17. package/dist/InlineAlert/InlineAlert.d.ts.map +1 -0
  18. package/dist/NavBar/NavBar.d.ts +10 -2
  19. package/dist/NavBar/NavBar.d.ts.map +1 -1
  20. package/dist/NavTree/NavGroup.d.ts +22 -0
  21. package/dist/NavTree/NavGroup.d.ts.map +1 -0
  22. package/dist/NavTree/NavItem.d.ts +29 -0
  23. package/dist/NavTree/NavItem.d.ts.map +1 -0
  24. package/dist/NavTree/NavTree.d.ts +23 -0
  25. package/dist/NavTree/NavTree.d.ts.map +1 -0
  26. package/dist/Skeleton/Skeleton.d.ts +22 -0
  27. package/dist/Skeleton/Skeleton.d.ts.map +1 -0
  28. package/dist/SkipLink/SkipLink.d.ts +25 -0
  29. package/dist/SkipLink/SkipLink.d.ts.map +1 -0
  30. package/dist/Table/Table.d.ts +6 -3
  31. package/dist/Table/Table.d.ts.map +1 -1
  32. package/dist/Table/TableBody.d.ts +7 -4
  33. package/dist/Table/TableBody.d.ts.map +1 -1
  34. package/dist/Table/TableCaption.d.ts +21 -0
  35. package/dist/Table/TableCaption.d.ts.map +1 -0
  36. package/dist/Table/TableCell.d.ts +15 -5
  37. package/dist/Table/TableCell.d.ts.map +1 -1
  38. package/dist/Table/TableHead.d.ts +5 -3
  39. package/dist/Table/TableHead.d.ts.map +1 -1
  40. package/dist/Table/TableHeaderCell.d.ts +5 -3
  41. package/dist/Table/TableHeaderCell.d.ts.map +1 -1
  42. package/dist/Table/TableRow.d.ts +5 -3
  43. package/dist/Table/TableRow.d.ts.map +1 -1
  44. package/dist/Tabs/Tab.d.ts.map +1 -1
  45. package/dist/Tabs/Tabs.d.ts +20 -1
  46. package/dist/Tabs/Tabs.d.ts.map +1 -1
  47. package/dist/Toast/Announcement.d.ts +38 -0
  48. package/dist/Toast/Announcement.d.ts.map +1 -0
  49. package/dist/Toast/Toast.d.ts +16 -2
  50. package/dist/Toast/Toast.d.ts.map +1 -1
  51. package/dist/Toast/ToastProvider.d.ts +27 -4
  52. package/dist/Toast/ToastProvider.d.ts.map +1 -1
  53. package/dist/Toast/ToastRegion.d.ts +26 -8
  54. package/dist/Toast/ToastRegion.d.ts.map +1 -1
  55. package/dist/Toast/status.d.ts +15 -0
  56. package/dist/Toast/status.d.ts.map +1 -0
  57. package/dist/Toast/useToast.d.ts +17 -3
  58. package/dist/Toast/useToast.d.ts.map +1 -1
  59. package/dist/Toolbar/Toolbar.d.ts +26 -4
  60. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  61. package/dist/a11y-meta.d.ts.map +1 -1
  62. package/dist/index.d.ts +30 -4
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +1305 -958
  65. package/dist/manifest.json +1049 -235
  66. package/dist/patterns.json +671 -3
  67. package/dist/styles.css +1 -1
  68. package/docs/Announcement.md +37 -0
  69. package/docs/AppShell.md +46 -0
  70. package/docs/AspectRatio.md +1 -1
  71. package/docs/Banner.md +54 -0
  72. package/docs/Button.md +2 -1
  73. package/docs/Card.md +1 -1
  74. package/docs/Checkbox.md +1 -1
  75. package/docs/CopyButton.md +51 -0
  76. package/docs/CursorPagination.md +45 -0
  77. package/docs/DescriptionItem.md +1 -1
  78. package/docs/DescriptionList.md +1 -1
  79. package/docs/Dialog.md +1 -1
  80. package/docs/Field.md +4 -3
  81. package/docs/IconButton.md +1 -1
  82. package/docs/InlineAlert.md +53 -0
  83. package/docs/Input.md +1 -1
  84. package/docs/Menu.md +1 -1
  85. package/docs/MenuItem.md +1 -1
  86. package/docs/MenuSeparator.md +1 -1
  87. package/docs/NavBar.md +4 -2
  88. package/docs/NavGroup.md +40 -0
  89. package/docs/NavItem.md +40 -0
  90. package/docs/NavTree.md +39 -0
  91. package/docs/Pagination.md +1 -1
  92. package/docs/Panel.md +1 -1
  93. package/docs/Select.md +1 -1
  94. package/docs/Skeleton.md +50 -0
  95. package/docs/SkipLink.md +40 -0
  96. package/docs/Switch.md +1 -1
  97. package/docs/Tab.md +1 -1
  98. package/docs/TabList.md +1 -1
  99. package/docs/TabPanel.md +1 -1
  100. package/docs/Table.md +1 -1
  101. package/docs/TableBody.md +3 -2
  102. package/docs/TableCaption.md +39 -0
  103. package/docs/TableCell.md +5 -2
  104. package/docs/TableHead.md +2 -1
  105. package/docs/TableHeaderCell.md +2 -1
  106. package/docs/TableRow.md +2 -1
  107. package/docs/Tabs.md +3 -3
  108. package/docs/Tag.md +1 -1
  109. package/docs/Toast.md +5 -3
  110. package/docs/ToastProvider.md +6 -3
  111. package/docs/ToastRegion.md +5 -5
  112. package/docs/Toolbar.md +5 -3
  113. package/docs/Tooltip.md +1 -1
  114. package/llms.txt +41 -9
  115. package/package.json +2 -2
@@ -51,6 +51,233 @@
51
51
  "blocked": false,
52
52
  "preset": "<Toast action={<Button variant=\"ghost\">Undo</Button>} variant=\"success\">[what just happened]</Toast>\n"
53
53
  },
54
+ {
55
+ "id": "announcer",
56
+ "intent": "Under a ToastProvider, do not render this — call useToast().announce() (D91). For an application with its own ToastRegion and no provider: one region holding visible toasts and speech-only Announcements, each routed by politeness rather than by tone. For a single confirmation toast, use action-feedback.",
57
+ "match": [
58
+ "announcer",
59
+ "live region",
60
+ "screen reader announcement",
61
+ "aria-live",
62
+ "status message",
63
+ "announce without a toast",
64
+ "polite and assertive"
65
+ ],
66
+ "compose": {
67
+ "component": "ToastRegion",
68
+ "slots": {
69
+ "body": [
70
+ {
71
+ "component": "Toast",
72
+ "props": {
73
+ "variant": "success",
74
+ "politeness": "{param:outcome-politeness}"
75
+ },
76
+ "slots": {
77
+ "body": [
78
+ "{content:outcome}"
79
+ ]
80
+ }
81
+ },
82
+ {
83
+ "component": "Announcement",
84
+ "props": {
85
+ "politeness": "polite"
86
+ },
87
+ "slots": {
88
+ "body": [
89
+ "{content:state-change}"
90
+ ]
91
+ }
92
+ },
93
+ {
94
+ "component": "Announcement",
95
+ "props": {
96
+ "politeness": "assertive"
97
+ },
98
+ "slots": {
99
+ "body": [
100
+ "{content:field-errors}"
101
+ ]
102
+ }
103
+ }
104
+ ]
105
+ }
106
+ },
107
+ "parameters": [
108
+ {
109
+ "key": "outcome-politeness",
110
+ "ask": "Should the outcome of the user's own action interrupt, success included?",
111
+ "options": [
112
+ "assertive",
113
+ "polite"
114
+ ],
115
+ "default": "assertive"
116
+ }
117
+ ],
118
+ "content": {
119
+ "outcome": "[the outcome of the action the user just took]",
120
+ "state-change": "[a change to the object on screen, spoken and not shown]",
121
+ "field-errors": "[how many fields need attention, spoken after a failed submission]"
122
+ },
123
+ "gaps": [],
124
+ "blocked": false,
125
+ "preset": "<ToastRegion>\n <Toast politeness=\"assertive\" variant=\"success\">[the outcome of the action the user just took]</Toast>\n <Announcement politeness=\"polite\">[a change to the object on screen, spoken and not shown]</Announcement>\n <Announcement politeness=\"assertive\">[how many fields need attention, spoken after a failed submission]</Announcement>\n</ToastRegion>\n"
126
+ },
127
+ {
128
+ "id": "app-shell",
129
+ "intent": "An application frame: a skip link, a full-width header with a Menu toggle that names the sidebar (aria-expanded and aria-controls — wire both and sidebarOpen to one piece of state), a dark sub-themed sidebar holding a NavTree, and a main region that scrolls on its own so nothing hides under the header. A closed sidebar is hidden, not narrowed. A desktop frame, not a responsive drawer. Put a link of your own router in each NavItem instead of href, and aria-current=\"page\" on the current one",
130
+ "match": [
131
+ "app shell",
132
+ "application shell",
133
+ "sidebar layout",
134
+ "admin layout",
135
+ "side navigation"
136
+ ],
137
+ "compose": {
138
+ "component": "AppShell",
139
+ "props": {
140
+ "sidebarOpen": true,
141
+ "sidebarTheme": "dark"
142
+ },
143
+ "slots": {
144
+ "skipLink": [
145
+ {
146
+ "component": "SkipLink",
147
+ "props": {
148
+ "href": "#main"
149
+ },
150
+ "content": "skip-label"
151
+ }
152
+ ],
153
+ "header": [
154
+ {
155
+ "component": "NavBar",
156
+ "props": {
157
+ "fluid": true
158
+ },
159
+ "slots": {
160
+ "brand": [
161
+ {
162
+ "component": "Button",
163
+ "props": {
164
+ "variant": "ghost",
165
+ "size": 32,
166
+ "aria-expanded": true,
167
+ "aria-controls": "sidebar"
168
+ },
169
+ "content": "menu-label"
170
+ },
171
+ "{content:brand}"
172
+ ],
173
+ "actions": [
174
+ {
175
+ "component": "Button",
176
+ "props": {
177
+ "variant": "neutral",
178
+ "size": 32
179
+ },
180
+ "content": "account-label"
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ ],
186
+ "sidebar": [
187
+ {
188
+ "component": "NavTree",
189
+ "props": {
190
+ "aria-label": "{content:nav-label}"
191
+ },
192
+ "slots": {
193
+ "body": [
194
+ {
195
+ "component": "NavItem",
196
+ "props": {
197
+ "href": "#overview",
198
+ "current": true
199
+ },
200
+ "content": "overview-label"
201
+ },
202
+ {
203
+ "component": "NavGroup",
204
+ "props": {
205
+ "label": "{content:group-1-label}",
206
+ "open": true
207
+ },
208
+ "slots": {
209
+ "body": [
210
+ {
211
+ "component": "NavItem",
212
+ "props": {
213
+ "href": "#group-1-first"
214
+ },
215
+ "content": "group-1-first-label"
216
+ },
217
+ {
218
+ "component": "NavItem",
219
+ "props": {
220
+ "href": "#group-1-second"
221
+ },
222
+ "content": "group-1-second-label"
223
+ }
224
+ ]
225
+ }
226
+ },
227
+ {
228
+ "component": "NavGroup",
229
+ "props": {
230
+ "label": "{content:group-2-label}",
231
+ "open": true
232
+ },
233
+ "slots": {
234
+ "body": [
235
+ {
236
+ "component": "NavItem",
237
+ "props": {
238
+ "href": "#group-2-first"
239
+ },
240
+ "content": "group-2-first-label"
241
+ },
242
+ {
243
+ "component": "NavItem",
244
+ "props": {
245
+ "href": "#group-2-second"
246
+ },
247
+ "content": "group-2-second-label"
248
+ }
249
+ ]
250
+ }
251
+ }
252
+ ]
253
+ }
254
+ }
255
+ ],
256
+ "body": [
257
+ "{content:page}"
258
+ ]
259
+ }
260
+ },
261
+ "parameters": [],
262
+ "content": {
263
+ "skip-label": "Skip to content",
264
+ "menu-label": "Menu",
265
+ "brand": "[product name]",
266
+ "account-label": "[account]",
267
+ "nav-label": "Main",
268
+ "overview-label": "Overview",
269
+ "group-1-label": "[group name]",
270
+ "group-1-first-label": "[page name]",
271
+ "group-1-second-label": "[page name]",
272
+ "group-2-label": "[group name]",
273
+ "group-2-first-label": "[page name]",
274
+ "group-2-second-label": "[page name]",
275
+ "page": "[the page; it scrolls on its own]"
276
+ },
277
+ "gaps": [],
278
+ "blocked": false,
279
+ "preset": "<AppShell\n header={<NavBar\n actions={<Button size={32} variant=\"neutral\">[account]</Button>}\n brand={<>\n <Button aria-controls=\"sidebar\" aria-expanded={true} size={32} variant=\"ghost\">Menu</Button>\n [product name]\n </>}\n fluid={true}\n />}\n sidebar={<NavTree\n aria-label=\"Main\"\n >\n <NavItem current={true} href=\"#overview\">Overview</NavItem>\n <NavGroup\n label=\"[group name]\"\n open={true}\n >\n <NavItem href=\"#group-1-first\">[page name]</NavItem>\n <NavItem href=\"#group-1-second\">[page name]</NavItem>\n </NavGroup>\n <NavGroup\n label=\"[group name]\"\n open={true}\n >\n <NavItem href=\"#group-2-first\">[page name]</NavItem>\n <NavItem href=\"#group-2-second\">[page name]</NavItem>\n </NavGroup>\n </NavTree>}\n sidebarOpen={true}\n sidebarTheme=\"dark\"\n skipLink={<SkipLink href=\"#main\">Skip to content</SkipLink>}\n>\n [the page; it scrolls on its own]\n</AppShell>\n"
280
+ },
54
281
  {
55
282
  "id": "bulk-action-bar",
56
283
  "intent": "Bar that appears when table rows are selected: selection count, bulk actions, and a clear-selection control",
@@ -110,9 +337,110 @@
110
337
  "blocked": false,
111
338
  "preset": "<Toolbar\n gap={12}\n>\n <Tag subtle={true} variant=\"accent\">[n] selected</Tag>\n <Button variant=\"neutral\">Export</Button>\n <Button variant=\"danger-subtle\">Delete</Button>\n <Button variant=\"ghost\">Clear selection</Button>\n</Toolbar>\n"
112
339
  },
340
+ {
341
+ "id": "copyable-id",
342
+ "intent": "A labelled identifier with a button that copies it; the button announces nothing, the application speaks the result",
343
+ "match": [
344
+ "copy id",
345
+ "copyable identifier",
346
+ "copy to clipboard",
347
+ "reference number",
348
+ "request id"
349
+ ],
350
+ "compose": {
351
+ "component": "DescriptionList",
352
+ "props": {
353
+ "layout": "inline"
354
+ },
355
+ "slots": {
356
+ "body": [
357
+ {
358
+ "component": "DescriptionItem",
359
+ "props": {
360
+ "term": "{content:term}"
361
+ },
362
+ "slots": {
363
+ "body": [
364
+ "{content:value}",
365
+ {
366
+ "component": "CopyButton",
367
+ "props": {
368
+ "value": "{content:value}",
369
+ "size": 24
370
+ }
371
+ }
372
+ ]
373
+ }
374
+ }
375
+ ]
376
+ }
377
+ },
378
+ "parameters": [],
379
+ "content": {
380
+ "term": "[identifier name]",
381
+ "value": "[the identifier — set it in a mono class such as psi-text-mono-14-20-regular]"
382
+ },
383
+ "gaps": [],
384
+ "blocked": false,
385
+ "preset": "<DescriptionList\n layout=\"inline\"\n>\n <DescriptionItem\n term=\"[identifier name]\"\n >\n [the identifier — set it in a mono class such as psi-text-mono-14-20-regular]\n <CopyButton size={24} value=\"[the identifier — set it in a mono class such as psi-text-mono-14-20-regular]\" />\n </DescriptionItem>\n</DescriptionList>\n"
386
+ },
387
+ {
388
+ "id": "cursor-pagination",
389
+ "intent": "Page-size selector and previous/next control beneath a list that pages by opaque token — no page count, no page numbers; pair with a TableCaption whose detail states the range and takes focus after a page change",
390
+ "match": [
391
+ "cursor pagination",
392
+ "token pagination",
393
+ "next page token",
394
+ "previous next",
395
+ "load more",
396
+ "keyset pagination",
397
+ "page size"
398
+ ],
399
+ "compose": {
400
+ "component": "Toolbar",
401
+ "props": {
402
+ "gap": 12,
403
+ "align": "end"
404
+ },
405
+ "slots": {
406
+ "body": [
407
+ {
408
+ "component": "Field",
409
+ "slots": {
410
+ "label": [
411
+ "{content:page-size-label}"
412
+ ],
413
+ "body": [
414
+ {
415
+ "component": "Select",
416
+ "props": {
417
+ "size": 32
418
+ }
419
+ }
420
+ ]
421
+ }
422
+ },
423
+ {
424
+ "component": "CursorPagination",
425
+ "props": {
426
+ "hasPrevious": true,
427
+ "hasNext": true
428
+ }
429
+ }
430
+ ]
431
+ }
432
+ },
433
+ "parameters": [],
434
+ "content": {
435
+ "page-size-label": "Rows per page"
436
+ },
437
+ "gaps": [],
438
+ "blocked": false,
439
+ "preset": "<Toolbar\n align=\"end\"\n gap={12}\n>\n <Field\n label=\"Rows per page\"\n >\n <Select size={32} />\n </Field>\n <CursorPagination hasNext={true} hasPrevious={true} />\n</Toolbar>\n"
440
+ },
113
441
  {
114
442
  "id": "data-table",
115
- "intent": "Sortable, selectable table of records with numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
443
+ "intent": "Sortable, selectable table of records with a caption stating the range, numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
116
444
  "match": [
117
445
  "data table",
118
446
  "transactions table",
@@ -128,6 +456,13 @@
128
456
  },
129
457
  "slots": {
130
458
  "body": [
459
+ {
460
+ "component": "TableCaption",
461
+ "props": {
462
+ "detail": "{content:range}"
463
+ },
464
+ "content": "caption"
465
+ },
131
466
  {
132
467
  "component": "TableHead",
133
468
  "slots": {
@@ -209,6 +544,8 @@
209
544
  },
210
545
  "parameters": [],
211
546
  "content": {
547
+ "caption": "[what the table lists — its visible name]",
548
+ "range": "[the rows shown, e.g. Rows 1–50 of 1,204; focus moves here after a page change]",
212
549
  "date-label": "Date",
213
550
  "payee-label": "Payee",
214
551
  "amount-label": "Amount",
@@ -220,7 +557,7 @@
220
557
  },
221
558
  "gaps": [],
222
559
  "blocked": false,
223
- "preset": "<Table\n selectable={true}\n sortable={true}\n>\n <TableHead>\n <TableRow>\n <TableHeaderCell sortKey=\"date\">Date</TableHeaderCell>\n <TableHeaderCell sortKey=\"payee\">Payee</TableHeaderCell>\n <TableHeaderCell numeric={true} sortKey=\"amount\">Amount</TableHeaderCell>\n <TableHeaderCell>[a visible \"Actions\" header — the column needs an accessible name, so never an empty th]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody>\n <TableRow\n rowId=\"row-id\"\n >\n <TableCell>[one row per record]</TableCell>\n <TableCell>[the record's payee]</TableCell>\n <TableCell numeric={true}>[numeric columns align right and use tabular figures]</TableCell>\n <TableCell>[the row-actions menu — compose it from the row-actions pattern]</TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
560
+ "preset": "<Table\n selectable={true}\n sortable={true}\n>\n <TableCaption detail=\"[the rows shown, e.g. Rows 1–50 of 1,204; focus moves here after a page change]\">[what the table lists — its visible name]</TableCaption>\n <TableHead>\n <TableRow>\n <TableHeaderCell sortKey=\"date\">Date</TableHeaderCell>\n <TableHeaderCell sortKey=\"payee\">Payee</TableHeaderCell>\n <TableHeaderCell numeric={true} sortKey=\"amount\">Amount</TableHeaderCell>\n <TableHeaderCell>[a visible \"Actions\" header — the column needs an accessible name, so never an empty th]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody>\n <TableRow\n rowId=\"row-id\"\n >\n <TableCell>[one row per record]</TableCell>\n <TableCell>[the record's payee]</TableCell>\n <TableCell numeric={true}>[numeric columns align right and use tabular figures]</TableCell>\n <TableCell>[the row-actions menu — compose it from the row-actions pattern]</TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
224
561
  },
225
562
  {
226
563
  "id": "date-range-filter",
@@ -470,9 +807,85 @@
470
807
  "blocked": false,
471
808
  "preset": "<Panel\n padding={24}\n>\n [nothing matches these filters]\n [say whether the dataset is empty or the filters are too narrow]\n <Button variant=\"neutral\">Clear filters</Button>\n</Panel>\n"
472
809
  },
810
+ {
811
+ "id": "filter-form",
812
+ "intent": "Labelled filters above a list, submitted together with a Find button: a Toolbar rendered as a named form (as=\"form\", so Find and Enter submit — wire onSubmit), aligned to the start so every control sits on one line under its visible Field label and Find drops onto that line. A field's description or error line hangs below its control and moves nothing else; the error line is the Field's own, with its usual aria-describedby wiring — set error on the Field, not beside the row. Every label in the row must be one line: a label that wraps pushes its control off the shared line. Add IconSearch before the Find label. For filters that apply as they change, named without a visible label, use filter-toolbar instead; never name a control by its placeholder alone",
813
+ "match": [
814
+ "filter form",
815
+ "search form",
816
+ "labelled filters",
817
+ "filters with labels",
818
+ "find button",
819
+ "submit filters"
820
+ ],
821
+ "compose": {
822
+ "component": "Toolbar",
823
+ "props": {
824
+ "as": "form",
825
+ "align": "start",
826
+ "gap": 12,
827
+ "aria-label": "{content:form-label}"
828
+ },
829
+ "slots": {
830
+ "body": [
831
+ {
832
+ "component": "Field",
833
+ "slots": {
834
+ "label": [
835
+ "{content:text-label}"
836
+ ],
837
+ "body": [
838
+ {
839
+ "component": "Input",
840
+ "props": {
841
+ "size": 32
842
+ }
843
+ }
844
+ ]
845
+ }
846
+ },
847
+ {
848
+ "component": "Field",
849
+ "slots": {
850
+ "label": [
851
+ "{content:select-label}"
852
+ ],
853
+ "body": [
854
+ {
855
+ "component": "Select",
856
+ "props": {
857
+ "size": 32
858
+ }
859
+ }
860
+ ]
861
+ }
862
+ },
863
+ {
864
+ "component": "Button",
865
+ "props": {
866
+ "type": "submit",
867
+ "variant": "accent",
868
+ "size": 32
869
+ },
870
+ "content": "find-label"
871
+ }
872
+ ]
873
+ }
874
+ },
875
+ "parameters": [],
876
+ "content": {
877
+ "form-label": "Filters",
878
+ "text-label": "[field name]",
879
+ "select-label": "[field name]",
880
+ "find-label": "Find"
881
+ },
882
+ "gaps": [],
883
+ "blocked": false,
884
+ "preset": "<Toolbar\n align=\"start\"\n aria-label=\"Filters\"\n as=\"form\"\n gap={12}\n>\n <Field\n label=\"[field name]\"\n >\n <Input size={32} />\n </Field>\n <Field\n label=\"[field name]\"\n >\n <Select size={32} />\n </Field>\n <Button size={32} type=\"submit\" variant=\"accent\">Find</Button>\n</Toolbar>\n"
885
+ },
473
886
  {
474
887
  "id": "filter-toolbar",
475
- "intent": "Toolbar of filter controls: search, category select, active-filter tags — controls are named by aria-label/placeholder, not a visible label, so no Field (contrast table-pagination)",
888
+ "intent": "Toolbar of filter controls that apply as they change: search, category select, active-filter tags — controls are named by aria-label/placeholder, not a visible label, so no Field (contrast table-pagination). For filters with visible labels, submitted together with a Find button, use filter-form",
476
889
  "match": [
477
890
  "filter toolbar",
478
891
  "filter bar",
@@ -515,6 +928,261 @@
515
928
  "blocked": false,
516
929
  "preset": "<Toolbar>\n <Input aria-label=\"Search transactions\" placeholder=\"Search transactions\" type=\"search\" />\n <Select aria-label=\"Category\" />\n <Tag>[active filter]</Tag>\n</Toolbar>\n"
517
930
  },
931
+ {
932
+ "id": "form-feedback",
933
+ "intent": "A message above a form after a rejected submission, holding the reason and a copyable identifier; not a live region — the application speaks it",
934
+ "match": [
935
+ "form error summary",
936
+ "submission failed",
937
+ "rejected form",
938
+ "error id",
939
+ "support reference"
940
+ ],
941
+ "compose": {
942
+ "component": "InlineAlert",
943
+ "props": {
944
+ "variant": "danger"
945
+ },
946
+ "slots": {
947
+ "title": [
948
+ "{content:title}"
949
+ ],
950
+ "body": [
951
+ "{content:message}",
952
+ {
953
+ "component": "CopyButton",
954
+ "props": {
955
+ "value": "{content:identifier}",
956
+ "label": "{content:copy-label}"
957
+ }
958
+ }
959
+ ]
960
+ }
961
+ },
962
+ "parameters": [],
963
+ "content": {
964
+ "title": "[what was rejected]",
965
+ "message": "[why, and what to change]",
966
+ "identifier": "[the request or error id, for support]",
967
+ "copy-label": "Copy id"
968
+ },
969
+ "gaps": [],
970
+ "blocked": false,
971
+ "preset": "<InlineAlert\n title=\"[what was rejected]\"\n variant=\"danger\"\n>\n [why, and what to change]\n <CopyButton label=\"Copy id\" value=\"[the request or error id, for support]\" />\n</InlineAlert>\n"
972
+ },
973
+ {
974
+ "id": "loading-table",
975
+ "intent": "A table whose rows are still loading — Skeleton lines in the cells, aria-busy on the body; the skeletons are hidden from assistive tech, the busy state is what it reads",
976
+ "match": [
977
+ "loading table",
978
+ "table skeleton",
979
+ "placeholder rows",
980
+ "loading state",
981
+ "busy table"
982
+ ],
983
+ "compose": {
984
+ "component": "Table",
985
+ "props": {
986
+ "aria-label": "{content:table-label}"
987
+ },
988
+ "slots": {
989
+ "body": [
990
+ {
991
+ "component": "TableHead",
992
+ "slots": {
993
+ "body": [
994
+ {
995
+ "component": "TableRow",
996
+ "slots": {
997
+ "body": [
998
+ {
999
+ "component": "TableHeaderCell",
1000
+ "content": "col-1"
1001
+ },
1002
+ {
1003
+ "component": "TableHeaderCell",
1004
+ "content": "col-2"
1005
+ },
1006
+ {
1007
+ "component": "TableHeaderCell",
1008
+ "content": "col-3"
1009
+ }
1010
+ ]
1011
+ }
1012
+ }
1013
+ ]
1014
+ }
1015
+ },
1016
+ {
1017
+ "component": "TableBody",
1018
+ "props": {
1019
+ "aria-busy": "true"
1020
+ },
1021
+ "slots": {
1022
+ "body": [
1023
+ {
1024
+ "component": "TableRow",
1025
+ "slots": {
1026
+ "body": [
1027
+ {
1028
+ "component": "TableCell",
1029
+ "slots": {
1030
+ "body": [
1031
+ {
1032
+ "component": "Skeleton"
1033
+ }
1034
+ ]
1035
+ }
1036
+ },
1037
+ {
1038
+ "component": "TableCell",
1039
+ "slots": {
1040
+ "body": [
1041
+ {
1042
+ "component": "Skeleton"
1043
+ }
1044
+ ]
1045
+ }
1046
+ },
1047
+ {
1048
+ "component": "TableCell",
1049
+ "slots": {
1050
+ "body": [
1051
+ {
1052
+ "component": "Skeleton"
1053
+ }
1054
+ ]
1055
+ }
1056
+ }
1057
+ ]
1058
+ }
1059
+ }
1060
+ ]
1061
+ }
1062
+ }
1063
+ ]
1064
+ }
1065
+ },
1066
+ "parameters": [],
1067
+ "content": {
1068
+ "table-label": "[what the table lists], loading",
1069
+ "col-1": "[first column]",
1070
+ "col-2": "[second column]",
1071
+ "col-3": "[third column]"
1072
+ },
1073
+ "gaps": [],
1074
+ "blocked": false,
1075
+ "preset": "<Table\n aria-label=\"[what the table lists], loading\"\n>\n <TableHead>\n <TableRow>\n <TableHeaderCell>[first column]</TableHeaderCell>\n <TableHeaderCell>[second column]</TableHeaderCell>\n <TableHeaderCell>[third column]</TableHeaderCell>\n </TableRow>\n </TableHead>\n <TableBody\n aria-busy=\"true\"\n >\n <TableRow>\n <TableCell>\n <Skeleton />\n </TableCell>\n <TableCell>\n <Skeleton />\n </TableCell>\n <TableCell>\n <Skeleton />\n </TableCell>\n </TableRow>\n </TableBody>\n</Table>\n"
1076
+ },
1077
+ {
1078
+ "id": "page-banner",
1079
+ "intent": "A full-width, page-level message above the content, with a Refresh action; it is what the page shows, and announcing stays the application's (route the same event through the announcer)",
1080
+ "match": [
1081
+ "page banner",
1082
+ "site-wide message",
1083
+ "stale data",
1084
+ "service notice",
1085
+ "maintenance banner"
1086
+ ],
1087
+ "compose": {
1088
+ "component": "Banner",
1089
+ "props": {
1090
+ "variant": "{param:variant}"
1091
+ },
1092
+ "slots": {
1093
+ "title": [
1094
+ "{content:title}"
1095
+ ],
1096
+ "body": [
1097
+ "{content:message}"
1098
+ ],
1099
+ "action": [
1100
+ {
1101
+ "component": "Button",
1102
+ "props": {
1103
+ "variant": "ghost",
1104
+ "size": 32
1105
+ },
1106
+ "content": "action-label"
1107
+ }
1108
+ ]
1109
+ }
1110
+ },
1111
+ "parameters": [
1112
+ {
1113
+ "key": "variant",
1114
+ "ask": "What kind of message is this?",
1115
+ "options": [
1116
+ "neutral",
1117
+ "warning",
1118
+ "danger",
1119
+ "success"
1120
+ ],
1121
+ "default": "neutral"
1122
+ }
1123
+ ],
1124
+ "content": {
1125
+ "title": "[what changed, in a few words]",
1126
+ "message": "[what it means for the user and what to do]",
1127
+ "action-label": "Refresh"
1128
+ },
1129
+ "gaps": [],
1130
+ "blocked": false,
1131
+ "preset": "<Banner action={<Button size={32} variant=\"ghost\">Refresh</Button>} title=\"[what changed, in a few words]\" variant=\"neutral\">[what it means for the user and what to do]</Banner>\n"
1132
+ },
1133
+ {
1134
+ "id": "required-field",
1135
+ "intent": "A required field that says so in text, not by a glyph alone: a Field with required and requiredText around an Input, with a description",
1136
+ "match": [
1137
+ "required field",
1138
+ "mandatory field",
1139
+ "required in text",
1140
+ "required label",
1141
+ "(Required)"
1142
+ ],
1143
+ "compose": {
1144
+ "component": "Field",
1145
+ "props": {
1146
+ "required": true,
1147
+ "requiredText": "{content:required-text}"
1148
+ },
1149
+ "slots": {
1150
+ "label": [
1151
+ "{content:label}"
1152
+ ],
1153
+ "body": [
1154
+ {
1155
+ "component": "Input",
1156
+ "props": {
1157
+ "size": "{param:size}"
1158
+ }
1159
+ }
1160
+ ],
1161
+ "description": [
1162
+ "{content:description}"
1163
+ ]
1164
+ }
1165
+ },
1166
+ "parameters": [
1167
+ {
1168
+ "key": "size",
1169
+ "ask": "Control size?",
1170
+ "options": [
1171
+ 32,
1172
+ 40
1173
+ ],
1174
+ "default": 32
1175
+ }
1176
+ ],
1177
+ "content": {
1178
+ "label": "[field name]",
1179
+ "required-text": "(Required)",
1180
+ "description": "[why it is needed]"
1181
+ },
1182
+ "gaps": [],
1183
+ "blocked": false,
1184
+ "preset": "<Field\n description=\"[why it is needed]\"\n label=\"[field name]\"\n required={true}\n requiredText=\"(Required)\"\n>\n <Input size={32} />\n</Field>\n"
1185
+ },
518
1186
  {
519
1187
  "id": "row-actions",
520
1188
  "intent": "List or table row with an icon-only actions menu, including a destructive item — hand the destructive item's confirmation off to the destructive-confirm pattern",