@westonkd/sprint 0.5.0 → 0.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "library": "sprint",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "conventions": {
5
5
  "componentAttribute": "data-sprint",
6
6
  "partAttribute": "data-sprint-part",
@@ -110,6 +110,186 @@
110
110
  "notes": "Danger and warning render role=alert and announce assertively; info and neutral render role=status. The dismiss control is a labelled button. Render the alert when the condition occurs rather than toggling its visibility, or the announcement is lost."
111
111
  }
112
112
  },
113
+ {
114
+ "name": "Breadcrumb",
115
+ "category": "navigation",
116
+ "summary": "A navigation landmark that renders as one line: the path to the current page, where every crumb opens the level it sits in. Every destination in the tree stays in the page as an addressable part; the crumbs decide which of them a person is shown.",
117
+ "whenToUse": "Use it as an application's primary navigation when the destinations form a tree, or when the catalogue is larger than a rail can hold. A crumb opens its siblings, a branch drills into its children, and typing in an open crumb searches the whole tree, so what a person chooses from is one level rather than the whole set. Put a page-level command or two in actions. It takes destinations as data because it counts, filters and orders them.",
118
+ "whenNotToUse": "Do not use it for a handful of links that fit in a sidebar; that is Nav with NavGroup, which keeps every destination visible at once. Do not use it for links inside prose, and do not pass components in items or actions: each destination is a label, an href and its children, not a node.",
119
+ "status": "experimental",
120
+ "props": {
121
+ "label": {
122
+ "kind": "string",
123
+ "description": "What this navigation is for. Rendered as the landmark's accessible name and as the root of the path.",
124
+ "required": true
125
+ },
126
+ "href": {
127
+ "kind": "string",
128
+ "description": "Where the root of the path leads, such as the list a detail page belongs to. Without it the root is plain text."
129
+ },
130
+ "items": {
131
+ "kind": "array",
132
+ "description": "The destinations, as a tree of { label, href?, active?, external?, children? }. The item marked active is the current page, and the crumbs are the path to it. A branch without an href is a level, not a destination.",
133
+ "required": true
134
+ },
135
+ "actions": {
136
+ "kind": "array",
137
+ "description": "Commands for the whole page, rendered at the end of the bar, as { label, href?, external?, onSelect? }. An action with an href is a link; one without runs onSelect."
138
+ },
139
+ "maxCrumbs": {
140
+ "kind": "number",
141
+ "description": "How many crumbs the bar shows before it folds the middle of the path behind an ellipsis. On a narrow screen every crumb but the last folds regardless.",
142
+ "default": 4
143
+ },
144
+ "emptyLabel": {
145
+ "kind": "string",
146
+ "description": "What the bar says when a filter matches no destination.",
147
+ "default": "No match"
148
+ },
149
+ "trailEmptyLabel": {
150
+ "kind": "string",
151
+ "description": "What the bar says when nothing has been visited through it yet.",
152
+ "default": "Nothing visited yet"
153
+ },
154
+ "open": {
155
+ "kind": "string",
156
+ "description": "Which crumb is open, as its depth from 0, or 'trail', or 'closed'. Pass it to drive the bar from outside, such as from an application-level shortcut; leave it off and the bar keeps its own state."
157
+ },
158
+ "onOpenChange": {
159
+ "kind": "handler",
160
+ "description": "Called with the crumb depth the bar wants open, 'trail', or 'closed'. Required when open is controlled, so the bar can still close itself."
161
+ },
162
+ "defaultVisited": {
163
+ "kind": "array",
164
+ "description": "The hrefs already visited, oldest first, to seed the trail with. Use it to restore a trail recorded through onNavigate when the bar remounts."
165
+ },
166
+ "visited": {
167
+ "kind": "array",
168
+ "description": "The trail as hrefs, oldest first, when the owner keeps it. Leave it off and the bar keeps its own trail for as long as it is mounted."
169
+ },
170
+ "onNavigate": {
171
+ "kind": "handler",
172
+ "description": "Called with the item when a destination is chosen, before the browser follows the href. Use it to record the visit somewhere that outlives this component."
173
+ },
174
+ "agentName": {
175
+ "kind": "string",
176
+ "description": "Overrides the label when deriving the action tool's name."
177
+ },
178
+ "agentTool": {
179
+ "kind": "boolean",
180
+ "description": "Whether actions without an href register a WebMCP tool.",
181
+ "default": true
182
+ }
183
+ },
184
+ "state": {
185
+ "href": {
186
+ "description": "On the root: where the root of the path leads. On a destination or an action: where it goes.",
187
+ "attribute": "data-sprint-href"
188
+ },
189
+ "recency": {
190
+ "description": "On a group of results while the trail is open: 1 for the group visited most recently, counting up. Orders the groups without a style attribute.",
191
+ "attribute": "data-sprint-recency"
192
+ },
193
+ "path": {
194
+ "description": "The path to the current page, labels joined by ' / '. Absent when no item is active.",
195
+ "attribute": "data-sprint-path"
196
+ },
197
+ "destinations": {
198
+ "description": "How many destinations the tree holds.",
199
+ "attribute": "data-sprint-destinations"
200
+ },
201
+ "visited": {
202
+ "description": "How many destinations have been visited through this bar.",
203
+ "attribute": "data-sprint-visited"
204
+ },
205
+ "open": {
206
+ "description": "Which crumb is open, as its depth, or trail. Absent when the bar is closed to its one line.",
207
+ "attribute": "data-sprint-open"
208
+ },
209
+ "matches": {
210
+ "description": "How many entries the open crumb is showing.",
211
+ "attribute": "data-sprint-matches"
212
+ },
213
+ "shown": {
214
+ "description": "On a destination: present when the open crumb is currently showing it to a person. Every destination stays in the page either way.",
215
+ "attribute": "data-sprint-shown"
216
+ },
217
+ "parent": {
218
+ "description": "On a destination: the labels of the levels above it, joined by ' / '.",
219
+ "attribute": "data-sprint-parent"
220
+ },
221
+ "active": {
222
+ "description": "On a destination: present when it is the current page.",
223
+ "attribute": "data-sprint-active"
224
+ },
225
+ "ancestor": {
226
+ "description": "On a destination: present when it is on the path to the current page.",
227
+ "attribute": "data-sprint-ancestor"
228
+ }
229
+ },
230
+ "tools": {
231
+ "act": {
232
+ "verb": "act",
233
+ "description": "Run one of this breadcrumb's trailing actions by its visible label, exactly as a person pressing it would. Only actions that do something in the page are offered; an action that is a link is reachable by its href instead. Returns the breadcrumb's state after the action, so a follow-up read is usually unnecessary.",
234
+ "inputSchema": {
235
+ "type": "object",
236
+ "properties": {
237
+ "action": {
238
+ "type": "string",
239
+ "description": "The visible label of the action to run, as shown at the end of the bar."
240
+ }
241
+ },
242
+ "required": [
243
+ "action"
244
+ ]
245
+ },
246
+ "readOnly": false,
247
+ "untrustedContent": true,
248
+ "registeredWhen": "The breadcrumb is mounted, has at least one action without an href, and no other component claims the same tool name. The registered schema enumerates those actions' labels.",
249
+ "unregisteredWhen": "The breadcrumb unmounts, or its last action without an href is removed."
250
+ }
251
+ },
252
+ "agentView": {
253
+ "example": "- **Breadcrumb** \"Workbench\" [destinations=2, path=display / Table, visited=0] → tool `act-workbench`\n - part `action` \"Copy link\"\n - part `destination` \"Button\" [href=#/Button, parent=action]\n - part `destination` \"Table\" [active, href=#/Table, parent=display]"
254
+ },
255
+ "a11y": {
256
+ "role": "navigation",
257
+ "notes": "The label is the landmark's accessible name and the crumbs are an ordered list, the current one carrying aria-current=page. Destinations the open crumb is not showing are hidden from the accessibility tree by CSS, so a screen reader travels one level at a time exactly as a sighted reader does, while the page itself keeps all of them. From the field, ArrowDown enters the results and ArrowUp at the top returns to it; typing anywhere in the results goes back to the field and keeps the character. Enter takes the first result. Escape closes the bar, as does a press outside it, and the match count is a polite live region. ArrowUp in an empty field recalls the trail, the way a console recalls history. The ellipsis that folds the middle of the path is a toggle with aria-expanded."
258
+ },
259
+ "relatedComponents": [
260
+ "Nav",
261
+ "NavGroup",
262
+ "Link",
263
+ "Shell"
264
+ ],
265
+ "examples": [
266
+ {
267
+ "title": "A path you can edit",
268
+ "description": "One line of chrome over a tree. Each crumb opens the level it sits in and filters it as you type; a branch drills into its children.",
269
+ "code": "<Breadcrumb\n label=\"Workbench\"\n items={[\n { label: \"action\", children: [{ href: \"#/Button\", label: \"Button\" }] },\n {\n label: \"display\",\n children: [{ href: \"#/Table\", label: \"Table\", active: true }],\n },\n ]}\n/>"
270
+ },
271
+ {
272
+ "title": "A deep path that folds",
273
+ "description": "Past maxCrumbs the middle of the path folds behind an ellipsis, which unfolds it again. A crumb that is open is never folded.",
274
+ "code": "<Breadcrumb\n label=\"Store\"\n maxCrumbs={3}\n items={[\n {\n label: \"Clothing\",\n href: \"#/clothing\",\n children: [\n {\n label: \"Outerwear\",\n href: \"#/clothing/outerwear\",\n children: [\n {\n label: \"Jackets\",\n href: \"#/clothing/outerwear/jackets\",\n children: [{ label: \"Rain shell\", href: \"#/rain-shell\", active: true }],\n },\n ],\n },\n ],\n },\n ]}\n/>"
275
+ },
276
+ {
277
+ "title": "Trailing actions",
278
+ "description": "Commands for the whole page sit at the end of the bar. A link action publishes its href; one with onSelect registers a WebMCP tool, since an agent has no URL to reach it by.",
279
+ "code": "<Breadcrumb\n label=\"Docs\"\n items={[{ label: \"guides\", children: [{ href: \"#/guide/webmcp\", label: \"WebMCP\", active: true }] }]}\n actions={[\n { label: \"Copy link\", onSelect: () => navigator.clipboard.writeText(location.href) },\n { label: \"Source\", href: \"https://github.com/westonkd/sprint\", external: true },\n ]}\n/>"
280
+ },
281
+ {
282
+ "title": "A root that leads back",
283
+ "description": "Give the root an href and it becomes a link back to the list the page belongs to, the way \"People\" leads from a person back to everyone.",
284
+ "code": "<Breadcrumb\n label=\"People\"\n href=\"#/people\"\n items={[\n {\n label: \"Admin\",\n href: \"#/people?role=admin\",\n children: [{ label: \"Tess Ocampo\", href: \"#/people/tess\", active: true }],\n },\n ]}\n/>"
285
+ },
286
+ {
287
+ "title": "Recording where a person has been",
288
+ "description": "The bar keeps a trail of the destinations chosen through it, reachable from the visited count at its head. Record visits through onNavigate and hand them back in defaultVisited, and the trail survives the bar remounting.",
289
+ "code": "<Breadcrumb\n label=\"Docs\"\n items={[{ label: \"guides\", children: [{ href: \"#/guide/webmcp\", label: \"WebMCP\" }] }]}\n defaultVisited={[\"#/guide/webmcp\"]}\n onNavigate={(item) => console.log(item.href)}\n/>"
290
+ }
291
+ ]
292
+ },
113
293
  {
114
294
  "name": "Button",
115
295
  "category": "action",
@@ -319,6 +499,99 @@
319
499
  "notes": "The whole block is one control: a link when it has an href, a button when it acts. The title names it, and the body is read as its content rather than as part of the name."
320
500
  }
321
501
  },
502
+ {
503
+ "name": "ChangeList",
504
+ "category": "display",
505
+ "summary": "A review of what will change or has changed: each row is marked added, removed or changed, and a changed row reads from → to. The kind of every row is a glyph, a tone and spoken text at once, and an agent reads it as state rather than from the glyph.",
506
+ "whenToUse": "Use it to show a diff before someone confirms it: a role change from Write to Maintain, members added to a team, settings a migration will drop. Pass the changes as data so each row is an addressable part carrying its kind, from and to. It registers no tool, because it only reports: the action is the Button that confirms the change, and the rows are already fully readable in the agent view.",
507
+ "whenNotToUse": "Do not use it for a list whose marks carry no meaning; that is List. Do not use it for a full record of fields; that is DescriptionList or Table. Do not put components in a change; label, from, to and detail are text.",
508
+ "status": "experimental",
509
+ "props": {
510
+ "label": {
511
+ "kind": "string",
512
+ "description": "What is changing. Names the list for a screen reader and for the agent view.",
513
+ "required": true
514
+ },
515
+ "changes": {
516
+ "kind": "array",
517
+ "description": "The rows in order, each { kind, label, from?, to?, detail? }. kind is \"added\", \"removed\" or \"changed\"; label is the thing that changed; from and to are its old and new values, usually on a changed row; detail is one short line of consequence beneath it.",
518
+ "required": true
519
+ },
520
+ "emptyLabel": {
521
+ "kind": "string",
522
+ "description": "What the list says when there is nothing to change.",
523
+ "default": "No changes"
524
+ }
525
+ },
526
+ "state": {
527
+ "changes": {
528
+ "description": "How many rows the list has.",
529
+ "attribute": "data-sprint-changes"
530
+ },
531
+ "added": {
532
+ "description": "How many rows are additions. Absent when there are none.",
533
+ "attribute": "data-sprint-added"
534
+ },
535
+ "removed": {
536
+ "description": "How many rows are removals. Absent when there are none.",
537
+ "attribute": "data-sprint-removed"
538
+ },
539
+ "changed": {
540
+ "description": "How many rows are changes of value. Absent when there are none.",
541
+ "attribute": "data-sprint-changed"
542
+ },
543
+ "empty": {
544
+ "description": "Present when there is nothing to change.",
545
+ "attribute": "data-sprint-empty"
546
+ },
547
+ "kind": {
548
+ "description": "On a change: whether the row was added, removed or changed.",
549
+ "attribute": "data-sprint-kind",
550
+ "values": [
551
+ "added",
552
+ "removed",
553
+ "changed"
554
+ ]
555
+ },
556
+ "from": {
557
+ "description": "On a change: the old value, when it has one.",
558
+ "attribute": "data-sprint-from"
559
+ },
560
+ "to": {
561
+ "description": "On a change: the new value, when it has one.",
562
+ "attribute": "data-sprint-to"
563
+ }
564
+ },
565
+ "agentView": {
566
+ "example": "- **ChangeList** \"Role changes\" [changed=1, changes=1]\n - part `change` \"Changed: Ada Lovelace, from Write to Maintain\" [from=Write, kind=changed, to=Maintain]"
567
+ },
568
+ "a11y": {
569
+ "role": "list",
570
+ "notes": "A ul named by its label, with an explicit list role. The +, − and → glyphs are aria-hidden; each row instead starts with visually hidden text naming its kind, and a changed row says from and to in words, so the kind never rests on the glyph or its color alone. Old values are a del and new values an ins."
571
+ },
572
+ "relatedComponents": [
573
+ "List",
574
+ "DescriptionList",
575
+ "Dialog"
576
+ ],
577
+ "examples": [
578
+ {
579
+ "title": "A role change",
580
+ "description": "The one row a permission review is about: what it was, and what it becomes.",
581
+ "code": "<ChangeList\n label=\"Role changes\"\n changes={[{ kind: \"changed\", label: \"Ada Lovelace\", from: \"Write\", to: \"Maintain\" }]}\n/>"
582
+ },
583
+ {
584
+ "title": "A review before confirming",
585
+ "description": "Additions, removals and changes together, each with a line of consequence where one matters.",
586
+ "code": "<ChangeList\n label=\"Team changes\"\n changes={[\n { kind: \"added\", label: \"Grace Hopper\", detail: \"Gets read access to every repository.\" },\n { kind: \"removed\", label: \"Alan Turing\" },\n { kind: \"changed\", label: \"Ada Lovelace\", from: \"Write\", to: \"Maintain\" },\n ]}\n/>"
587
+ },
588
+ {
589
+ "title": "Nothing to change",
590
+ "description": "An empty review says so rather than rendering nothing.",
591
+ "code": "<ChangeList label=\"Role changes\" changes={[]} emptyLabel=\"No role changes\" />"
592
+ }
593
+ ]
594
+ },
322
595
  {
323
596
  "name": "Checkbox",
324
597
  "category": "input",
@@ -443,6 +716,146 @@
443
716
  "notes": "A native checkbox input wrapped by its label, visually replaced by a keylined square. Focus draws an offset keyline around the square; errors set aria-invalid and link with aria-describedby."
444
717
  }
445
718
  },
719
+ {
720
+ "name": "ChoiceGrid",
721
+ "category": "input",
722
+ "summary": "A question answered by pressing one of several large, equal-sized tiles, each a glyph such as an emoji over a text label. It works in two modes: submit mode, where every tile is a real submit button carrying its value, and select mode, where the tiles are a radio group driven by value and onChange.",
723
+ "whenToUse": "Use it when one tap should answer a question and the options are few enough to show at once, ideally six to twelve: an emoji verification step at sign-in (\"pick the emoji you were sent\"), a reaction, a mood, or a category picked by icon. Submit mode needs no client state, so it suits a server-rendered form page: give it a name and no onChange, and the pressed tile submits its form with name=value. Use select mode, by passing value and onChange, when the answer is one field among several and the form is submitted by something else. A verification challenge meant for a human, such as the emoji check, should pass agentTool={false} so the page does not offer an agent a one-call way through it.",
724
+ "whenNotToUse": "Do not use it for two to four short text options, which is a SegmentedControl, for long lists, which are a Select, or for choosing several at once, which is a set of Checkboxes. Do not put components in an option: label and glyph are strings, and the label is the tile's accessible name because an emoji on its own is not one.",
725
+ "status": "experimental",
726
+ "props": {
727
+ "label": {
728
+ "kind": "string",
729
+ "description": "The question the tiles answer. Rendered as the legend of the grid's fieldset and used to derive the tool name, so write it as a person would read it, such as \"Which emoji were you sent?\".",
730
+ "required": true
731
+ },
732
+ "options": {
733
+ "kind": "array",
734
+ "description": "The tiles in display order: { value, label, glyph? }. value is what the form receives, label is what a person reads and what the choose tool accepts, and glyph is a short string such as an emoji drawn large above the label and hidden from assistive technology.",
735
+ "required": true
736
+ },
737
+ "columns": {
738
+ "kind": "enum",
739
+ "values": [
740
+ "2",
741
+ "3",
742
+ "4"
743
+ ],
744
+ "description": "How many columns the grid has at 40rem and wider. Narrower screens always get two, so every tile stays large enough to press.",
745
+ "default": 3
746
+ },
747
+ "name": {
748
+ "kind": "string",
749
+ "description": "The form field name. In submit mode each tile is a submit button with this name and its option's value; in select mode a hidden input carries the selected value under this name."
750
+ },
751
+ "value": {
752
+ "kind": "string",
753
+ "description": "Select mode only: the value of the selected option. Ignored in submit mode, where nothing stays selected."
754
+ },
755
+ "onChange": {
756
+ "kind": "handler",
757
+ "description": "Passing it switches the grid to select mode. Called with the chosen value; the choose tool drives a real click, so it runs for agent choices too."
758
+ },
759
+ "disabled": {
760
+ "kind": "boolean",
761
+ "description": "Disable every tile and unregister the choose tool.",
762
+ "default": false
763
+ },
764
+ "agentName": {
765
+ "kind": "string",
766
+ "description": "Override the label used to derive the tool name, when the question is long or two grids on a page would collide."
767
+ },
768
+ "agentTool": {
769
+ "kind": "boolean",
770
+ "description": "Set false to render the grid without registering a choose tool. Pass false for any challenge that exists to prove a human is present.",
771
+ "default": true
772
+ }
773
+ },
774
+ "state": {
775
+ "mode": {
776
+ "description": "submit when pressing a tile submits the form, select when it changes a selection.",
777
+ "attribute": "data-sprint-mode",
778
+ "values": [
779
+ "submit",
780
+ "select"
781
+ ]
782
+ },
783
+ "value": {
784
+ "description": "Select mode: the value of the option currently selected.",
785
+ "attribute": "data-sprint-value"
786
+ },
787
+ "columns": {
788
+ "description": "The column count at 40rem and wider.",
789
+ "attribute": "data-sprint-columns",
790
+ "values": [
791
+ "2",
792
+ "3",
793
+ "4"
794
+ ]
795
+ },
796
+ "disabled": {
797
+ "description": "Present when no tile can be pressed.",
798
+ "attribute": "data-sprint-disabled"
799
+ }
800
+ },
801
+ "tools": {
802
+ "choose": {
803
+ "verb": "choose",
804
+ "description": "Choose one of this grid's options by its visible label, exactly as a person pressing it would. In submit mode the choice submits the surrounding form with the option's value, so it can navigate away; in select mode it replaces the current selection. Returns the grid's state after the choice.",
805
+ "inputSchema": {
806
+ "type": "object",
807
+ "properties": {
808
+ "option": {
809
+ "type": "string",
810
+ "description": "The visible label of the option to choose, as shown under its glyph."
811
+ }
812
+ },
813
+ "required": [
814
+ "option"
815
+ ]
816
+ },
817
+ "readOnly": false,
818
+ "untrustedContent": true,
819
+ "registeredWhen": "The grid is mounted, enabled, has a resolvable label, agentTool is not false, and no other component claims the same tool name. The registered schema enumerates the current option labels.",
820
+ "unregisteredWhen": "The grid unmounts, becomes disabled, or agentTool turns false."
821
+ }
822
+ },
823
+ "agentView": {
824
+ "example": "- **ChoiceGrid** \"Pick a reaction\" [columns=4, mode=select, value=fire] → tool `choose-pick-a-reaction`\n - part `choice` \"Thumbs up\" [value=up]\n - part `choice` \"Fire\" [checked, value=fire]\n - part `choice` \"Laughing\" [value=laugh]\n - part `choice` \"Sad\" [value=sad]"
825
+ },
826
+ "examples": [
827
+ {
828
+ "title": "An emoji verification check",
829
+ "description": "Submit mode in a plain form: each tile is a submit button named emoji, so the form posts emoji=fox with no client state. agentTool is false because the check exists to prove a person is present.",
830
+ "code": "<form method=\"post\" action=\"verify\">\n <ChoiceGrid\n label=\"Which emoji were you sent?\"\n name=\"emoji\"\n agentTool={false}\n options={[\n { value: \"fox\", label: \"Fox\", glyph: \"🦊\" },\n { value: \"rocket\", label: \"Rocket\", glyph: \"🚀\" },\n { value: \"cactus\", label: \"Cactus\", glyph: \"🌵\" },\n { value: \"anchor\", label: \"Anchor\", glyph: \"⚓\" },\n { value: \"pizza\", label: \"Pizza\", glyph: \"🍕\" },\n { value: \"comet\", label: \"Comet\", glyph: \"☄️\" },\n ]}\n />\n</form>"
831
+ },
832
+ {
833
+ "title": "Picking one with state",
834
+ "description": "Select mode with four columns: the tiles are a radio group, and in agent view each one renders as its own control so an agent can choose without WebMCP.",
835
+ "code": "<ChoiceGrid\n label=\"Pick a reaction\"\n columns={4}\n name=\"reaction\"\n value={reaction}\n onChange={setReaction}\n options={[\n { value: \"up\", label: \"Thumbs up\", glyph: \"👍\" },\n { value: \"fire\", label: \"Fire\", glyph: \"🔥\" },\n { value: \"laugh\", label: \"Laughing\", glyph: \"😂\" },\n { value: \"sad\", label: \"Sad\", glyph: \"😢\" },\n ]}\n/>"
836
+ },
837
+ {
838
+ "title": "A disabled grid",
839
+ "description": "Disabled unregisters the tool and renders the agent view as text only, so an agent cannot press a tile a person could not.",
840
+ "code": "<ChoiceGrid\n label=\"Delivery window\"\n columns={2}\n disabled\n value=\"morning\"\n onChange={setWindow}\n options={[\n { value: \"morning\", label: \"Morning\", glyph: \"☀\" },\n { value: \"evening\", label: \"Evening\", glyph: \"☾\" },\n ]}\n/>"
841
+ }
842
+ ],
843
+ "a11y": {
844
+ "role": "group, or radiogroup in select mode",
845
+ "keyboard": [
846
+ "Tab reaches every tile in submit mode, and the group once in select mode",
847
+ "Arrow keys move to the next or previous tile; in select mode they also select it",
848
+ "Home and End move to the first and last tile",
849
+ "Enter or Space presses the focused tile"
850
+ ],
851
+ "notes": "The root is a fieldset labelled by its legend. A glyph is aria-hidden and the label is always visible text, so each tile's accessible name is its label. In select mode, selection follows focus with a roving tabindex. In agent view a hidden input stays in the form so a choice made there submits the same field a person's would."
852
+ },
853
+ "relatedComponents": [
854
+ "SegmentedControl",
855
+ "Select",
856
+ "Button"
857
+ ]
858
+ },
446
859
  {
447
860
  "name": "CodeBlock",
448
861
  "category": "display",
@@ -529,10 +942,83 @@
529
942
  }
530
943
  },
531
944
  {
532
- "name": "DescriptionList",
945
+ "name": "CopyField",
946
+ "category": "input",
947
+ "summary": "A labelled, read-only value with a Copy control: a setup link, an invite URL, a webhook address. The value truncates with an ellipsis on one line, stays selectable, and the control confirms with Copied for two seconds.",
948
+ "whenToUse": "Use it to hand a person a value they are meant to paste somewhere else, such as \"Copy the link\" under a device setup step. The whole value stays in the DOM and in the agent view however narrow the field is drawn, so truncation never hides it from a screen reader or an agent.",
949
+ "whenNotToUse": "Do not use it for a secret; that is a SecretField, which masks the value and keeps it off every agent surface. Do not use it for a plain read-only value nobody needs to copy; that is a TextInput with readOnly, or a DescriptionList row. Do not use it for multi-line code; that is a CodeBlock.",
950
+ "status": "experimental",
951
+ "props": {
952
+ "label": {
953
+ "kind": "string",
954
+ "description": "What the value is, shown above it as the field label and used as the node's label in the agent view, e.g. \"Setup link\".",
955
+ "required": true
956
+ },
957
+ "value": {
958
+ "kind": "string",
959
+ "description": "The text shown and copied, verbatim. It is never masked: an agent reads it from the value part.",
960
+ "required": true
961
+ },
962
+ "copyLabel": {
963
+ "kind": "string",
964
+ "description": "The copy control's label while idle.",
965
+ "default": "Copy"
966
+ },
967
+ "copiedLabel": {
968
+ "kind": "string",
969
+ "description": "The copy control's label for two seconds after the value reaches the clipboard.",
970
+ "default": "Copied"
971
+ },
972
+ "onCopy": {
973
+ "kind": "handler",
974
+ "description": "Called with the value once it is on the clipboard. Not called when the clipboard refuses the write."
975
+ }
976
+ },
977
+ "state": {
978
+ "copied": {
979
+ "description": "Present for two seconds after the copy control has put the value on the clipboard.",
980
+ "attribute": "data-sprint-copied"
981
+ },
982
+ "copy-failed": {
983
+ "description": "Present after the clipboard refused the write or is unavailable. The value's text is selected instead, so the person can copy it by hand.",
984
+ "attribute": "data-sprint-copy-failed"
985
+ }
986
+ },
987
+ "agentView": {
988
+ "example": "- **CopyField** \"Setup link\"\n - part `value` \"https://sprint.example/setup/7HW4\"\n - part `copy` \"Copy\""
989
+ },
990
+ "examples": [
991
+ {
992
+ "title": "Copy the link",
993
+ "description": "The first real need: a setup link a person pastes into another device. A long URL truncates in the field and is still copied whole.",
994
+ "code": "<CopyField\n label=\"Setup link\"\n value=\"https://sprint.example/setup/7HW4-XK92-QQ1D?station=KX-2209&expires=2026-10-01\"\n/>"
995
+ },
996
+ {
997
+ "title": "Custom control labels",
998
+ "description": "Relabel the control in the language of the task, and react once the value is on the clipboard.",
999
+ "code": "<CopyField\n label=\"Invite code\"\n value=\"NOMAD-0042\"\n copyLabel=\"Copy code\"\n copiedLabel=\"Code copied\"\n onCopy={() => setShared(true)}\n/>"
1000
+ }
1001
+ ],
1002
+ "a11y": {
1003
+ "role": "group",
1004
+ "keyboard": [
1005
+ "Tab reaches the copy control",
1006
+ "Enter or Space copies"
1007
+ ],
1008
+ "notes": "The group is named by its visible label. The full value is in the DOM however it is truncated, and its title shows it on hover. The copy control is a real button whose label swap is a polite live region. If the clipboard refuses, the value's text is selected so a keyboard copy still works. No WebMCP tool is registered: the value is already readable in the agent view, and an agent gains nothing from writing it to the person's clipboard that reading it does not give."
1009
+ },
1010
+ "relatedComponents": [
1011
+ "SecretField",
1012
+ "TextInput",
1013
+ "CodeBlock",
1014
+ "DescriptionList"
1015
+ ]
1016
+ },
1017
+ {
1018
+ "name": "DescriptionList",
533
1019
  "category": "display",
534
1020
  "summary": "Labelled term–description pairs for the details of one thing: metadata, settings, profile fields.",
535
- "whenToUse": "Use for the properties of a single entity: a token's created date and scopes, a session's device and last activity, a profile's fields. Each item pairs one term with one description.",
1021
+ "whenToUse": "Use for the properties of a single entity: a token's created date and scopes, a session's device and last activity, a profile's fields. Each item pairs one term with one description. It lays itself out from its own width, not the viewport's: the term sits above its value until the list is at least 32rem wide, then moves into a column of its own, so it reads the same in a sidebar as in a full-width panel.",
536
1022
  "whenNotToUse": "Do not use for many entities with the same fields; that is a Table. Do not put components inside term or description; both are flattened to text for the agent view, so only inline content survives. Do not use for prose sequences; that is a List.",
537
1023
  "status": "experimental",
538
1024
  "props": {
@@ -550,6 +1036,16 @@
550
1036
  "kind": "string",
551
1037
  "description": "Text shown when items is empty. The region keeps its frame.",
552
1038
  "default": "Empty"
1039
+ },
1040
+ "loading": {
1041
+ "kind": "boolean",
1042
+ "description": "Set while the pairs are being fetched. Sets aria-busy and sweeps a bar along the top edge. Existing pairs stay visible; with none yet, the empty slot says loadingLabel instead of emptyLabel.",
1043
+ "default": false
1044
+ },
1045
+ "loadingLabel": {
1046
+ "kind": "string",
1047
+ "description": "What the empty slot says while loading.",
1048
+ "default": "Loading"
553
1049
  }
554
1050
  },
555
1051
  "state": {
@@ -560,6 +1056,10 @@
560
1056
  "empty": {
561
1057
  "description": "Present when there are no pairs.",
562
1058
  "attribute": "data-sprint-empty"
1059
+ },
1060
+ "loading": {
1061
+ "description": "Present while the pairs are being fetched. Alongside empty it means nothing has arrived yet, not that there is nothing.",
1062
+ "attribute": "data-sprint-loading"
563
1063
  }
564
1064
  },
565
1065
  "agentView": {
@@ -677,192 +1177,549 @@
677
1177
  }
678
1178
  },
679
1179
  {
680
- "name": "Heading",
681
- "category": "typography",
682
- "summary": "A section title, rendered as a real h element at the level you pick so it joins the document outline.",
683
- "whenToUse": "Use it for the title of a page or of a region inside one, and keep levels in document order so the outline an agent or a screen reader builds is the outline you meant.",
684
- "whenNotToUse": "Do not use it for the label on a bordered region; Panel takes a label prop, draws its own header, and joins the outline through its headingLevel prop. Do not pick a level for its size, only for its place in the outline.",
1180
+ "name": "Disclosure",
1181
+ "category": "layout",
1182
+ "summary": "A labelled region a person can show or hide with a toggle that carries its expanded state. Collapsing conceals the content from a person only: it stays in the page, and the agent view always renders it.",
1183
+ "whenToUse": "Use it for secondary detail a person reads on demand beside the primary content, such as an access breakdown on a user page behind \"Show access breakdown\". It replaces a Button that swaps its own label, because the toggle publishes aria-expanded and the region it controls, and the content stays readable to an agent without a click.",
1184
+ "whenNotToUse": "Do not use it to hide content an agent should not read; collapsing is a human affordance and the agent view carries the content regardless. Do not use it for a region that is always visible, which is a Panel, or for content that must interrupt the page, which is a Dialog. Do not use it to switch between alternative views of the same data, which is a SegmentedControl.",
685
1185
  "status": "experimental",
686
1186
  "props": {
1187
+ "label": {
1188
+ "kind": "string",
1189
+ "description": "What the region is, as a noun phrase such as \"Access breakdown\". It names the region for a screen reader, derives the toggle text and the tool name.",
1190
+ "required": true
1191
+ },
687
1192
  "children": {
688
1193
  "kind": "node",
689
- "description": "The title. Keep it short; long titles truncate in chrome.",
690
- "required": true
1194
+ "description": "The region's content. It is always mounted: collapsing hides it from a person with CSS, so components inside keep their tools and stay in the page projection."
691
1195
  },
692
- "level": {
1196
+ "expanded": {
1197
+ "kind": "boolean",
1198
+ "description": "Whether the region is revealed. Pass it with onExpandedChange to control the disclosure; leave it unset to let the disclosure keep its own state."
1199
+ },
1200
+ "defaultExpanded": {
1201
+ "kind": "boolean",
1202
+ "description": "The initial state when the disclosure is uncontrolled.",
1203
+ "default": false
1204
+ },
1205
+ "onExpandedChange": {
1206
+ "kind": "handler",
1207
+ "description": "Called with the new state whenever the toggle is pressed, by a person, a DOM-driving agent, or the expand tool."
1208
+ },
1209
+ "showLabel": {
1210
+ "kind": "string",
1211
+ "description": "The toggle text while collapsed.",
1212
+ "default": "\"Show \" followed by the label"
1213
+ },
1214
+ "hideLabel": {
1215
+ "kind": "string",
1216
+ "description": "The toggle text while expanded.",
1217
+ "default": "\"Hide \" followed by the label"
1218
+ },
1219
+ "agentName": {
1220
+ "kind": "string",
1221
+ "description": "Override the label used to derive the tool name, when two disclosures on a page would otherwise collide."
1222
+ },
1223
+ "agentTool": {
1224
+ "kind": "boolean",
1225
+ "description": "Set false to render the disclosure without registering an expand tool.",
1226
+ "default": true
1227
+ }
1228
+ },
1229
+ "state": {
1230
+ "expanded": {
1231
+ "description": "Present while the region is revealed to a person. Absent means collapsed: the content is still in the page, concealed by CSS rather than unmounted or marked hidden.",
1232
+ "attribute": "data-sprint-expanded"
1233
+ }
1234
+ },
1235
+ "tools": {
1236
+ "expand": {
1237
+ "verb": "expand",
1238
+ "description": "Expand or collapse this region for the person viewing the page, by stating the end state, exactly as a person pressing its toggle would. The content is already readable in the agent view either way, so call this only to change what a person sees. Setting the state it already has succeeds and changes nothing. Returns the region's state after the call.",
1239
+ "inputSchema": {
1240
+ "type": "object",
1241
+ "properties": {
1242
+ "expanded": {
1243
+ "type": "boolean",
1244
+ "description": "The end state: true reveals the region, false conceals it."
1245
+ }
1246
+ },
1247
+ "required": [
1248
+ "expanded"
1249
+ ]
1250
+ },
1251
+ "readOnly": false,
1252
+ "untrustedContent": true,
1253
+ "registeredWhen": "The disclosure is mounted, has a resolvable label, and no other component claims the same tool name.",
1254
+ "unregisteredWhen": "The disclosure unmounts or agentTool is set false."
1255
+ }
1256
+ },
1257
+ "agentView": {
1258
+ "example": "- **Disclosure** \"Access breakdown\" → tool `expand-access-breakdown`\n - part `toggle` \"Show access breakdown\"\n - **Text** \"Admin through the Operators group.\""
1259
+ },
1260
+ "examples": [
1261
+ {
1262
+ "title": "Secondary detail on demand",
1263
+ "description": "Uncontrolled: the disclosure keeps its own state. In agent view the content renders beneath the line whether or not a person has opened it, and the toggle is one control.",
1264
+ "code": "<Disclosure label=\"Access breakdown\">\n <Text>Admin through the Operators group.</Text>\n</Disclosure>"
1265
+ },
1266
+ {
1267
+ "title": "A controlled disclosure",
1268
+ "description": "Pass expanded and onExpandedChange when something else on the page needs to know or set whether the region is open. The expand tool drives the same toggle.",
1269
+ "code": "<Disclosure\n label=\"Access breakdown\"\n expanded={open}\n onExpandedChange={setOpen}\n>\n <Text>Admin through the Operators group.</Text>\n</Disclosure>"
1270
+ },
1271
+ {
1272
+ "title": "Custom toggle text",
1273
+ "description": "showLabel and hideLabel replace the derived toggle text when the noun phrase does not read naturally after Show and Hide.",
1274
+ "code": "<Disclosure label=\"Audit trail\" showLabel=\"Show 12 events\" hideLabel=\"Hide events\">\n <Text>Last change was a role grant by the on-call operator.</Text>\n</Disclosure>"
1275
+ }
1276
+ ],
1277
+ "a11y": {
1278
+ "role": "region",
1279
+ "keyboard": [
1280
+ "Enter toggles",
1281
+ "Space toggles"
1282
+ ],
1283
+ "notes": "The root is a section named by the label. The toggle is a real button with aria-expanded and aria-controls pointing at the content. Collapsed content is display: none, which removes it from the accessibility tree for a person and a screen reader while leaving it in the DOM for the agent projection."
1284
+ },
1285
+ "relatedComponents": [
1286
+ "Panel",
1287
+ "Switch"
1288
+ ]
1289
+ },
1290
+ {
1291
+ "name": "Divider",
1292
+ "category": "layout",
1293
+ "summary": "A horizontal rule that segments a page: the line between a header or a breadcrumb and the body under it, or between two runs of content. It draws in the active theme's own keyline or band, and can name the segment it opens.",
1294
+ "whenToUse": "Use it where a page changes subject and nothing else marks the change: under a PageHeader or a Breadcrumb before the body starts, or between groups of panels. Give it a label when the segment that follows has a name worth reading; an unlabelled Divider is invisible in agent view, because an agent does not care where a line is drawn, only what the line announces.",
1295
+ "whenNotToUse": "Do not use it to space things out; that is the gap on Stack. Do not use it to frame a region with a header; that is Panel, which draws its own keylines. Do not stack two in a row, and do not use it inside a Panel to separate a list's items; the list draws its own.",
1296
+ "status": "experimental",
1297
+ "props": {
1298
+ "label": {
1299
+ "kind": "string",
1300
+ "description": "The name of the segment the divider opens, rendered small on the rule."
1301
+ },
1302
+ "weight": {
693
1303
  "kind": "enum",
694
- "description": "Outline depth, rendered as the matching h element. 1 is the page title and there should be one per page.",
1304
+ "description": "How hard the break is. \"hairline\" is a keyline, \"heavy\" a thick strong keyline, \"band\" a strip of the theme's own ornament for the one break that matters most on a page.",
695
1305
  "values": [
696
- "1",
697
- "2",
698
- "3",
699
- "4"
1306
+ "hairline",
1307
+ "heavy",
1308
+ "band"
700
1309
  ],
701
- "default": "2"
1310
+ "default": "hairline"
702
1311
  }
703
1312
  },
704
1313
  "state": {
705
- "level": {
706
- "description": "The outline depth, and so the type voice in use.",
707
- "attribute": "data-sprint-level",
1314
+ "weight": {
1315
+ "description": "How hard the break is.",
1316
+ "attribute": "data-sprint-weight",
708
1317
  "values": [
709
- "1",
710
- "2",
711
- "3",
712
- "4"
1318
+ "hairline",
1319
+ "heavy",
1320
+ "band"
713
1321
  ]
714
1322
  }
715
1323
  },
716
1324
  "agentView": {
717
- "example": "- **Heading** \"WebMCP tools\" [level=2]"
1325
+ "example": "- **Divider** \"Results\" [weight=band]"
1326
+ },
1327
+ "a11y": {
1328
+ "role": "separator",
1329
+ "notes": "The rule is a native hr, so assistive technology announces a separator. A label is ordinary text read just before it, the way a person reads it on the rule."
718
1330
  },
1331
+ "relatedComponents": [
1332
+ "Stack",
1333
+ "PageHeader",
1334
+ "Breadcrumb",
1335
+ "Panel"
1336
+ ],
719
1337
  "examples": [
720
1338
  {
721
- "title": "A page title",
722
- "code": "<Heading level={1}>Button</Heading>"
1339
+ "title": "Separating the header from the body",
1340
+ "description": "The plainest break: a keyline under the page's chrome, before the content starts.",
1341
+ "code": "<Stack>\n <Breadcrumb\n label=\"Docs\"\n items={[{ label: \"guides\", children: [{ href: \"#/guide/webmcp\", label: \"WebMCP\", active: true }] }]}\n />\n <Divider />\n <Text>The body of the page starts here.</Text>\n</Stack>"
723
1342
  },
724
1343
  {
725
- "title": "A section title",
726
- "description": "The default level, for a region inside a page.",
727
- "code": "<Heading>Every variant</Heading>"
1344
+ "title": "A named segment",
1345
+ "description": "A label names what follows, on the rule itself and to an agent reading the page.",
1346
+ "code": "<Divider label=\"Results\" weight=\"heavy\" />"
1347
+ },
1348
+ {
1349
+ "title": "The page's main break",
1350
+ "description": "A band draws the theme's own ornament: hatching in the default register, pins in calorie, a barcode strip in trax, a row of dots in ambient. Use it once per page.",
1351
+ "code": "<Divider label=\"Body\" weight=\"band\" />"
728
1352
  }
729
1353
  ]
730
1354
  },
731
1355
  {
732
- "name": "Image",
733
- "category": "display",
734
- "summary": "A framed picture in a slot that holds its shape. It is the one component whose content an agent cannot read, so alt is not decoration: it is the whole of what the agent gets, and the source URL is published beside it for an agent that can fetch pixels of its own.",
735
- "whenToUse": "Use it for any picture that carries meaning: a screenshot, a diagram, a photograph, a logo in running content. Describe it in alt as if to someone on the phone. Pass alt=\"\" only for a picture that adds nothing, which then disappears from the agent view entirely.",
736
- "whenNotToUse": "Do not use it for a decorative texture; that is the ornament vocabulary, which costs no request and no description. Do not use it for an icon whose meaning is already in adjacent text.",
1356
+ "name": "EmptyState",
1357
+ "category": "feedback",
1358
+ "summary": "A region that says there is nothing here, and why: a headline, an optional sentence, and an optional way out such as clearing filters or creating the first record. It marks the empty field with the register's own empty ornament.",
1359
+ "whenToUse": "Use it in place of a list, grid, or set of results that has nothing to show, so the space says it is empty rather than rendering nothing. Set reason=\"filtered\" when records exist but none match, so an agent knows to relax the filters rather than conclude there is no data.",
1360
+ "whenNotToUse": "Do not use it for an empty Table, List, or Panel; those already carry their own emptyLabel. Do not use it for a failure; that is an Alert. Do not use it while data is still loading; that is Pending.",
737
1361
  "status": "experimental",
738
1362
  "props": {
739
- "src": {
740
- "kind": "string",
741
- "description": "Where the pixels are. Published as data-sprint-src so an agent that can see can fetch the file itself.",
742
- "required": true
743
- },
744
- "alt": {
1363
+ "label": {
745
1364
  "kind": "string",
746
- "description": "What the picture shows, in a sentence. This is the entire agent rendering of the image, so write it for a reader who will never see it. Empty means \"this picture carries no meaning\", and the component then renders nothing in agent view.",
1365
+ "description": "The headline, saying what is empty in a few words: \"No one matches these filters\". The region's accessible name.",
747
1366
  "required": true
748
1367
  },
749
- "caption": {
1368
+ "description": {
750
1369
  "kind": "string",
751
- "description": "A visible line under the frame, for a credit, a date, or a figure number. It is read as well as seen, so it does not repeat alt."
1370
+ "description": "One sentence of explanation or next step. Carried as the description part."
752
1371
  },
753
- "ratio": {
754
- "kind": "enum",
755
- "description": "The shape the slot holds while the picture loads and if it fails. auto lets the file decide its own height once it arrives.",
756
- "values": [
757
- "1:1",
758
- "4:3",
759
- "3:2",
760
- "16:9",
761
- "auto"
762
- ],
763
- "default": "auto"
1372
+ "action": {
1373
+ "kind": "object",
1374
+ "description": "The one way out, as data: { label, onSelect?, href? }. An href renders a Link, otherwise a Button that registers its own press tool, so the action is a single control in the agent view."
764
1375
  },
765
- "fit": {
1376
+ "reason": {
766
1377
  "kind": "enum",
767
- "description": "How the picture fills a slot of a fixed ratio. cover crops it, contain letterboxes it. Ignored when ratio is auto.",
1378
+ "description": "Why it is empty: nothing exists yet, or nothing matches the current filters.",
768
1379
  "values": [
769
- "cover",
770
- "contain"
1380
+ "empty",
1381
+ "filtered"
771
1382
  ],
772
- "default": "cover"
1383
+ "default": "empty"
773
1384
  }
774
1385
  },
775
1386
  "state": {
776
- "src": {
777
- "description": "The image source, so an agent can fetch the file without the DOM.",
778
- "attribute": "data-sprint-src"
779
- },
780
- "status": {
781
- "description": "Whether the pixels have arrived. Present only in the human rendering, because in agent view nothing is fetched and any value would be a guess.",
782
- "attribute": "data-sprint-status",
783
- "values": [
784
- "loading",
785
- "ready",
786
- "error"
787
- ]
788
- },
789
- "ratio": {
790
- "description": "The shape the slot holds.",
791
- "attribute": "data-sprint-ratio",
792
- "values": [
793
- "1:1",
794
- "4:3",
795
- "3:2",
796
- "16:9",
797
- "auto"
798
- ]
1387
+ "empty": {
1388
+ "description": "Always present, so one selector finds every empty region.",
1389
+ "attribute": "data-sprint-empty"
799
1390
  },
800
- "fit": {
801
- "description": "Whether a fixed-ratio slot crops the picture or letterboxes it.",
802
- "attribute": "data-sprint-fit",
1391
+ "reason": {
1392
+ "description": "Why the region is empty.",
1393
+ "attribute": "data-sprint-reason",
803
1394
  "values": [
804
- "cover",
805
- "contain"
1395
+ "empty",
1396
+ "filtered"
806
1397
  ]
807
- },
808
- "decorative": {
809
- "description": "Present when the image was declared meaningless with an empty alt and no caption. It renders nothing at all in agent view.",
810
- "attribute": "data-sprint-decorative"
811
1398
  }
812
1399
  },
813
1400
  "agentView": {
814
- "example": "- **Image** \"Launch pad 39B under floodlights, gantry retracted\" [fit=cover, ratio=16:9, src=media/pad-39b.svg]\n - part `caption` \"Pad 39B, T-minus 4h\""
1401
+ "example": "- **EmptyState** \"No one matches these filters\" [empty, reason=filtered]\n - part `description` \"Try a different role or clear the search.\""
815
1402
  },
816
1403
  "examples": [
817
1404
  {
818
- "title": "A described picture",
819
- "description": "The alt text is the agent rendering. It registers no WebMCP tool: there is nothing to do to a picture, and the source URL is already on the element.",
820
- "code": "<Image\n src=\"media/pad-39b.svg\"\n alt=\"Launch pad 39B under floodlights, gantry retracted\"\n ratio=\"16:9\"\n/>"
1405
+ "title": "Nothing matches",
1406
+ "description": "Records exist but the filters exclude them all. The action is a Button, so it registers press-clear-filters.",
1407
+ "code": "<EmptyState\n reason=\"filtered\"\n label=\"No one matches these filters\"\n description=\"Try a different role or clear the search.\"\n action={{ label: \"Clear filters\", onSelect: clearFilters }}\n/>"
821
1408
  },
822
1409
  {
823
- "title": "A captioned figure",
824
- "description": "The caption is read as well as seen, and arrives as its own part, so it can say what alt should not repeat.",
825
- "code": "<Image\n src=\"media/telemetry.svg\"\n alt=\"Line chart of chamber pressure holding flat for nine minutes, then dropping\"\n caption=\"Static fire 04, chamber pressure\"\n ratio=\"3:2\"\n fit=\"contain\"\n/>"
1410
+ "title": "Nothing yet",
1411
+ "description": "The first-run case, with a link to where records are made.",
1412
+ "code": "<EmptyState\n label=\"No projects yet\"\n description=\"Projects you create or are invited to appear here.\"\n action={{ label: \"Create a project\", href: \"#/projects/new\" }}\n/>"
826
1413
  },
827
1414
  {
828
- "title": "A picture that means nothing",
829
- "description": "An empty alt is a claim, not an omission: it says this picture carries no meaning. The component then renders nothing in agent view, the way Stack does.",
830
- "code": "<Image src=\"media/grain.svg\" alt=\"\" ratio=\"1:1\" />"
1415
+ "title": "Just the headline",
1416
+ "code": "<EmptyState label=\"No notifications\" />"
831
1417
  }
832
1418
  ],
833
1419
  "a11y": {
834
- "notes": "The frame is a figure named by its alt text, and the picture inside carries the same alt natively, so a fallback still reads when the file fails. A caption is a figcaption. While the picture is loading or after it has failed, the slot keeps its border and states its condition rather than collapsing; that band is aria-hidden, because on failure it shows the alt text a screen reader has already been given."
1420
+ "role": "group",
1421
+ "notes": "The headline names the group and the description describes it. The action is an ordinary Button or Link inside it, reached in normal tab order."
835
1422
  },
836
1423
  "relatedComponents": [
837
- "Card",
838
- "Panel"
1424
+ "Pending",
1425
+ "Alert",
1426
+ "Button",
1427
+ "Link"
839
1428
  ]
840
1429
  },
841
1430
  {
842
- "name": "Link",
843
- "category": "navigation",
844
- "summary": "A navigation link. It publishes its destination as state, so an agent reading the page in text learns the URL rather than having to click to find out, and it renders a real anchor in both views.",
845
- "whenToUse": "Use it for anything that changes the address: a nav item, a cross-reference, a link out to a specification. Set active on the item matching the current route so the agent view and the human view agree about where you are. Under a client-side router, pass an onClick that prevents default and navigates; the handler rides the anchor in both views, so agent clicks and open tools go through the router too.",
846
- "whenNotToUse": "Do not use it for an action that stays on the page; that is a Button. Do not register a tool on ordinary navigation: an agent can already reach a URL, and a page of links would flood its tool list for no gain.",
1431
+ "name": "EntityRow",
1432
+ "category": "display",
1433
+ "summary": "One record in a list of records, as a single clickable row: a title, a few Tags, and one line of meta. Give it an href and the whole row is a link; give it onSelect and it is an action that registers an open tool.",
1434
+ "whenToUse": "Use it for a list of people, projects, or anything else a person scans and picks one of, where each entry needs a name, a classification, and a line of context such as last activity. Stack several in a Stack with gap=\"none\" and adjacent rows share their keylines. The title is the accessible name, so it is also what an agent selects on.",
1435
+ "whenNotToUse": "Do not use it when the entries are compared column by column; that is a Table. Do not use it for a catalogue entry with a paragraph of body; that is a Card. Do not put controls in it: the whole row is already one link or button, so tags and meta are data, not components.",
847
1436
  "status": "experimental",
848
1437
  "props": {
849
- "href": {
1438
+ "title": {
850
1439
  "kind": "string",
851
- "description": "The destination. Published as data-sprint-href and carried in the agent view, so the URL is readable without a click.",
1440
+ "description": "The record's name, and the row's accessible name. Also derives the tool name when the row acts.",
852
1441
  "required": true
853
1442
  },
854
- "children": {
855
- "kind": "node",
856
- "description": "The link text. It names the destination, so prefer the page's name over here or read more.",
857
- "required": true
1443
+ "href": {
1444
+ "kind": "string",
1445
+ "description": "Destination, which makes the whole row a link. A row that navigates registers no tool by default, because an agent can follow the href itself."
858
1446
  },
859
- "active": {
860
- "kind": "boolean",
861
- "description": "Mark the link as the current location. Sets aria-current so a screen reader and an agent learn it the same way.",
862
- "default": false
1447
+ "onSelect": {
1448
+ "kind": "handler",
1449
+ "description": "What clicking does, called with the click event. Alone it makes the row a button that registers an open tool by default. Alongside href the row stays a link and the handler rides the click, so a router can call preventDefault."
863
1450
  },
864
- "external": {
865
- "kind": "boolean",
1451
+ "tags": {
1452
+ "kind": "array",
1453
+ "description": "Classifications rendered as Tags after the title: { label, tone? }, where tone is one of the Tag tones. Carried as Tag lines under the row in the agent view."
1454
+ },
1455
+ "meta": {
1456
+ "kind": "array",
1457
+ "description": "One line of context, each entry a string or { term?, detail }, joined with a middle dot. Truncates rather than wraps when the row is short of room. Carried as the meta part."
1458
+ },
1459
+ "description": {
1460
+ "kind": "string",
1461
+ "description": "An optional sentence under the row, for a record that needs more than a line of meta. Carried as the description part."
1462
+ },
1463
+ "disabled": {
1464
+ "kind": "boolean",
1465
+ "description": "Disable an acting row and unregister its tool. Has no effect on a row that navigates or does nothing.",
1466
+ "default": false
1467
+ },
1468
+ "agentTool": {
1469
+ "kind": "boolean",
1470
+ "description": "Override the default: on for a row that acts, off for a row that navigates. A row with neither href nor onSelect never registers one."
1471
+ },
1472
+ "agentName": {
1473
+ "kind": "string",
1474
+ "description": "Override the title used to derive the tool name, when two rows share a title."
1475
+ }
1476
+ },
1477
+ "state": {
1478
+ "href": {
1479
+ "description": "Where the row goes, when it navigates.",
1480
+ "attribute": "data-sprint-href"
1481
+ },
1482
+ "disabled": {
1483
+ "description": "Present when an acting row cannot be opened.",
1484
+ "attribute": "data-sprint-disabled"
1485
+ }
1486
+ },
1487
+ "tools": {
1488
+ "open": {
1489
+ "verb": "open",
1490
+ "description": "Open this row, exactly as a person clicking it would. What opening does is the row's own business: it may select the record, reveal its detail, or start a flow. Returns the row's state after the click.",
1491
+ "inputSchema": {
1492
+ "type": "object",
1493
+ "properties": {}
1494
+ },
1495
+ "readOnly": false,
1496
+ "untrustedContent": true,
1497
+ "registeredWhen": "The row acts rather than navigates, is mounted, enabled, has a resolvable title, and no other component claims the same tool name.",
1498
+ "unregisteredWhen": "The row unmounts, becomes disabled, or turns into a link by taking an href."
1499
+ }
1500
+ },
1501
+ "agentView": {
1502
+ "example": "- **EntityRow** \"Ada Lovelace\" [href=#/people/ada]\n - part `title` \"Ada Lovelace\"\n - part `meta` \"Last sign-in: 3 days ago · 2 apps\"\n - **Tag** \"admin\" [tone=info]\n - **Tag** \"billing\" [tone=neutral]"
1503
+ },
1504
+ "examples": [
1505
+ {
1506
+ "title": "A person in a directory",
1507
+ "description": "A row that navigates. No tool, because the href is already public; tags and meta are data.",
1508
+ "code": "<EntityRow\n title=\"Ada Lovelace\"\n href=\"#/people/ada\"\n tags={[{ label: \"admin\", tone: \"info\" }, { label: \"billing\" }]}\n meta={[{ term: \"Last sign-in\", detail: \"3 days ago\" }, \"2 apps\"]}\n/>"
1509
+ },
1510
+ {
1511
+ "title": "A row that acts",
1512
+ "description": "onSelect instead of href, so the row registers open-grace-hopper and an agent can pick it.",
1513
+ "code": "<EntityRow\n title=\"Grace Hopper\"\n onSelect={() => select(\"grace\")}\n tags={[{ label: \"owner\", tone: \"warning\" }]}\n meta={[\"Invited yesterday\"]}\n/>"
1514
+ },
1515
+ {
1516
+ "title": "A directory of rows",
1517
+ "description": "Rows stacked with no gap share their keylines, so the list reads as one ruled block.",
1518
+ "code": "<Stack gap=\"none\">\n {people.map((person) => (\n <EntityRow\n key={person.id}\n title={person.name}\n href={person.href}\n tags={person.roles.map((role) => ({ label: role }))}\n meta={[{ term: \"Last sign-in\", detail: person.lastSignIn }]}\n />\n ))}\n</Stack>"
1519
+ },
1520
+ {
1521
+ "title": "With a description",
1522
+ "description": "A sentence under the row, for a record that needs one.",
1523
+ "code": "<EntityRow\n title=\"Payments service\"\n href=\"#/projects/payments\"\n tags={[{ label: \"degraded\", tone: \"danger\" }]}\n meta={[\"Updated 4 min ago\"]}\n description=\"Card authorisations are timing out in eu-west.\"\n/>"
1524
+ }
1525
+ ],
1526
+ "a11y": {
1527
+ "notes": "The whole row is one control: a link with an href, a button with onSelect, or a labelled group with neither. The title is the accessible name, and the tags, meta, and description are its accessible description, so a screen reader announces the name first and the context after."
1528
+ },
1529
+ "relatedComponents": [
1530
+ "Card",
1531
+ "Tag",
1532
+ "Stack",
1533
+ "Table"
1534
+ ]
1535
+ },
1536
+ {
1537
+ "name": "Heading",
1538
+ "category": "typography",
1539
+ "summary": "A section title, rendered as a real h element at the level you pick so it joins the document outline.",
1540
+ "whenToUse": "Use it for the title of a page or of a region inside one, and keep levels in document order so the outline an agent or a screen reader builds is the outline you meant.",
1541
+ "whenNotToUse": "Do not use it for the label on a bordered region; Panel takes a label prop, draws its own header, and joins the outline through its headingLevel prop. Do not pick a level for its size, only for its place in the outline.",
1542
+ "status": "experimental",
1543
+ "props": {
1544
+ "children": {
1545
+ "kind": "node",
1546
+ "description": "The title. Keep it short; long titles truncate in chrome.",
1547
+ "required": true
1548
+ },
1549
+ "level": {
1550
+ "kind": "enum",
1551
+ "description": "Outline depth, rendered as the matching h element. 1 is the page title and there should be one per page.",
1552
+ "values": [
1553
+ "1",
1554
+ "2",
1555
+ "3",
1556
+ "4"
1557
+ ],
1558
+ "default": "2"
1559
+ }
1560
+ },
1561
+ "state": {
1562
+ "level": {
1563
+ "description": "The outline depth, and so the type voice in use.",
1564
+ "attribute": "data-sprint-level",
1565
+ "values": [
1566
+ "1",
1567
+ "2",
1568
+ "3",
1569
+ "4"
1570
+ ]
1571
+ }
1572
+ },
1573
+ "agentView": {
1574
+ "example": "- **Heading** \"WebMCP tools\" [level=2]"
1575
+ },
1576
+ "examples": [
1577
+ {
1578
+ "title": "A page title",
1579
+ "code": "<Heading level={1}>Button</Heading>"
1580
+ },
1581
+ {
1582
+ "title": "A section title",
1583
+ "description": "The default level, for a region inside a page.",
1584
+ "code": "<Heading>Every variant</Heading>"
1585
+ }
1586
+ ]
1587
+ },
1588
+ {
1589
+ "name": "Image",
1590
+ "category": "display",
1591
+ "summary": "A framed picture in a slot that holds its shape. It is the one component whose content an agent cannot read, so alt is not decoration: it is the whole of what the agent gets, and the source URL is published beside it for an agent that can fetch pixels of its own.",
1592
+ "whenToUse": "Use it for any picture that carries meaning: a screenshot, a diagram, a photograph, a logo in running content. Describe it in alt as if to someone on the phone. Pass alt=\"\" only for a picture that adds nothing, which then disappears from the agent view entirely.",
1593
+ "whenNotToUse": "Do not use it for a decorative texture; that is the ornament vocabulary, which costs no request and no description. Do not use it for an icon whose meaning is already in adjacent text.",
1594
+ "status": "experimental",
1595
+ "props": {
1596
+ "src": {
1597
+ "kind": "string",
1598
+ "description": "Where the pixels are. Published as data-sprint-src so an agent that can see can fetch the file itself.",
1599
+ "required": true
1600
+ },
1601
+ "alt": {
1602
+ "kind": "string",
1603
+ "description": "What the picture shows, in a sentence. This is the entire agent rendering of the image, so write it for a reader who will never see it. Empty means \"this picture carries no meaning\", and the component then renders nothing in agent view.",
1604
+ "required": true
1605
+ },
1606
+ "caption": {
1607
+ "kind": "string",
1608
+ "description": "A visible line under the frame, for a credit, a date, or a figure number. It is read as well as seen, so it does not repeat alt."
1609
+ },
1610
+ "ratio": {
1611
+ "kind": "enum",
1612
+ "description": "The shape the slot holds while the picture loads and if it fails. auto lets the file decide its own height once it arrives.",
1613
+ "values": [
1614
+ "1:1",
1615
+ "4:3",
1616
+ "3:2",
1617
+ "16:9",
1618
+ "auto"
1619
+ ],
1620
+ "default": "auto"
1621
+ },
1622
+ "fit": {
1623
+ "kind": "enum",
1624
+ "description": "How the picture fills a slot of a fixed ratio. cover crops it, contain letterboxes it. Ignored when ratio is auto.",
1625
+ "values": [
1626
+ "cover",
1627
+ "contain"
1628
+ ],
1629
+ "default": "cover"
1630
+ }
1631
+ },
1632
+ "state": {
1633
+ "src": {
1634
+ "description": "The image source, so an agent can fetch the file without the DOM.",
1635
+ "attribute": "data-sprint-src"
1636
+ },
1637
+ "status": {
1638
+ "description": "Whether the pixels have arrived. Present only in the human rendering, because in agent view nothing is fetched and any value would be a guess.",
1639
+ "attribute": "data-sprint-status",
1640
+ "values": [
1641
+ "loading",
1642
+ "ready",
1643
+ "error"
1644
+ ]
1645
+ },
1646
+ "ratio": {
1647
+ "description": "The shape the slot holds.",
1648
+ "attribute": "data-sprint-ratio",
1649
+ "values": [
1650
+ "1:1",
1651
+ "4:3",
1652
+ "3:2",
1653
+ "16:9",
1654
+ "auto"
1655
+ ]
1656
+ },
1657
+ "fit": {
1658
+ "description": "Whether a fixed-ratio slot crops the picture or letterboxes it.",
1659
+ "attribute": "data-sprint-fit",
1660
+ "values": [
1661
+ "cover",
1662
+ "contain"
1663
+ ]
1664
+ },
1665
+ "decorative": {
1666
+ "description": "Present when the image was declared meaningless with an empty alt and no caption. It renders nothing at all in agent view.",
1667
+ "attribute": "data-sprint-decorative"
1668
+ }
1669
+ },
1670
+ "agentView": {
1671
+ "example": "- **Image** \"Launch pad 39B under floodlights, gantry retracted\" [fit=cover, ratio=16:9, src=media/pad-39b.svg]\n - part `caption` \"Pad 39B, T-minus 4h\""
1672
+ },
1673
+ "examples": [
1674
+ {
1675
+ "title": "A described picture",
1676
+ "description": "The alt text is the agent rendering. It registers no WebMCP tool: there is nothing to do to a picture, and the source URL is already on the element.",
1677
+ "code": "<Image\n src=\"media/pad-39b.svg\"\n alt=\"Launch pad 39B under floodlights, gantry retracted\"\n ratio=\"16:9\"\n/>"
1678
+ },
1679
+ {
1680
+ "title": "A captioned figure",
1681
+ "description": "The caption is read as well as seen, and arrives as its own part, so it can say what alt should not repeat.",
1682
+ "code": "<Image\n src=\"media/telemetry.svg\"\n alt=\"Line chart of chamber pressure holding flat for nine minutes, then dropping\"\n caption=\"Static fire 04, chamber pressure\"\n ratio=\"3:2\"\n fit=\"contain\"\n/>"
1683
+ },
1684
+ {
1685
+ "title": "A picture that means nothing",
1686
+ "description": "An empty alt is a claim, not an omission: it says this picture carries no meaning. The component then renders nothing in agent view, the way Stack does.",
1687
+ "code": "<Image src=\"media/grain.svg\" alt=\"\" ratio=\"1:1\" />"
1688
+ }
1689
+ ],
1690
+ "a11y": {
1691
+ "notes": "The frame is a figure named by its alt text, and the picture inside carries the same alt natively, so a fallback still reads when the file fails. A caption is a figcaption. While the picture is loading or after it has failed, the slot keeps its border and states its condition rather than collapsing; that band is aria-hidden, because on failure it shows the alt text a screen reader has already been given."
1692
+ },
1693
+ "relatedComponents": [
1694
+ "Card",
1695
+ "Panel"
1696
+ ]
1697
+ },
1698
+ {
1699
+ "name": "Link",
1700
+ "category": "navigation",
1701
+ "summary": "A navigation link. It publishes its destination as state, so an agent reading the page in text learns the URL rather than having to click to find out, and it renders a real anchor in both views.",
1702
+ "whenToUse": "Use it for anything that changes the address: a nav item, a cross-reference, a link out to a specification. Set active on the item matching the current route so the agent view and the human view agree about where you are. Under a client-side router, pass an onClick that prevents default and navigates; the handler rides the anchor in both views, so agent clicks and open tools go through the router too.",
1703
+ "whenNotToUse": "Do not use it for an action that stays on the page; that is a Button. Do not register a tool on ordinary navigation: an agent can already reach a URL, and a page of links would flood its tool list for no gain.",
1704
+ "status": "experimental",
1705
+ "props": {
1706
+ "href": {
1707
+ "kind": "string",
1708
+ "description": "The destination. Published as data-sprint-href and carried in the agent view, so the URL is readable without a click.",
1709
+ "required": true
1710
+ },
1711
+ "children": {
1712
+ "kind": "node",
1713
+ "description": "The link text. It names the destination, so prefer the page's name over here or read more.",
1714
+ "required": true
1715
+ },
1716
+ "active": {
1717
+ "kind": "boolean",
1718
+ "description": "Mark the link as the current location. Sets aria-current so a screen reader and an agent learn it the same way.",
1719
+ "default": false
1720
+ },
1721
+ "external": {
1722
+ "kind": "boolean",
866
1723
  "description": "Mark a destination outside this app. Opens in a new context and adds the usual rel protections.",
867
1724
  "default": false
868
1725
  },
@@ -935,7 +1792,7 @@
935
1792
  "category": "display",
936
1793
  "summary": "A bulleted or numbered list built from an array of items. Each item is an addressable part carrying its position, so an agent can cite item three without counting lines.",
937
1794
  "whenToUse": "Use it for a short sequence of related points: rules, steps, links, caveats. Passing items as data rather than as children is what lets the agent view carry each one as its own part.",
938
- "whenNotToUse": "Do not use it for records with fields; that is Table. Do not use it as a layout for cards or controls; that is Stack.",
1795
+ "whenNotToUse": "Do not use it for a diff or a review of what will change, where each mark means added or removed; that is ChangeList. Do not use it for records with fields; that is Table. Do not use it as a layout for cards or controls; that is Stack.",
939
1796
  "status": "experimental",
940
1797
  "props": {
941
1798
  "label": {
@@ -953,10 +1810,31 @@
953
1810
  "description": "Number the items instead of bulleting them. Use it when the order is the point.",
954
1811
  "default": false
955
1812
  },
1813
+ "marker": {
1814
+ "kind": "enum",
1815
+ "description": "The mark before each item. \"plus\" is the house bullet, \"bullet\" a plain dot for quieter prose, \"number\" counts the items and makes the list ordered, \"none\" drops the marker column for a list whose items already lead with their own mark. Defaults to \"number\" when ordered is set and \"plus\" otherwise. The marker is drawn, not read: an item whose mark means something, such as added or removed, belongs in a ChangeList.",
1816
+ "values": [
1817
+ "plus",
1818
+ "bullet",
1819
+ "number",
1820
+ "none"
1821
+ ],
1822
+ "default": "plus"
1823
+ },
956
1824
  "emptyLabel": {
957
1825
  "kind": "string",
958
1826
  "description": "What the list says when it has no items.",
959
1827
  "default": "Empty"
1828
+ },
1829
+ "loading": {
1830
+ "kind": "boolean",
1831
+ "description": "Set while the items are being fetched. Sets aria-busy and sweeps a bar along the top edge. Existing items stay visible; with none yet, the empty slot says loadingLabel instead of emptyLabel.",
1832
+ "default": false
1833
+ },
1834
+ "loadingLabel": {
1835
+ "kind": "string",
1836
+ "description": "What the empty slot says while loading.",
1837
+ "default": "Loading"
960
1838
  }
961
1839
  },
962
1840
  "state": {
@@ -965,12 +1843,26 @@
965
1843
  "attribute": "data-sprint-items"
966
1844
  },
967
1845
  "ordered": {
968
- "description": "Present when the items are numbered rather than bulleted.",
1846
+ "description": "Present when the list is a real ol: ordered is set, or the marker is number.",
969
1847
  "attribute": "data-sprint-ordered"
970
1848
  },
971
- "empty": {
972
- "description": "Present when the list has no items.",
973
- "attribute": "data-sprint-empty"
1849
+ "marker": {
1850
+ "description": "The mark drawn before each item.",
1851
+ "attribute": "data-sprint-marker",
1852
+ "values": [
1853
+ "plus",
1854
+ "bullet",
1855
+ "number",
1856
+ "none"
1857
+ ]
1858
+ },
1859
+ "empty": {
1860
+ "description": "Present when the list has no items.",
1861
+ "attribute": "data-sprint-empty"
1862
+ },
1863
+ "loading": {
1864
+ "description": "Present while the items are being fetched. Alongside empty it means nothing has arrived yet, not that there is nothing.",
1865
+ "attribute": "data-sprint-loading"
974
1866
  },
975
1867
  "index": {
976
1868
  "description": "On an item: its 1-based position in the list.",
@@ -978,7 +1870,7 @@
978
1870
  }
979
1871
  },
980
1872
  "agentView": {
981
- "example": "- **List** \"Tool rules\" [items=1]\n - part `item` \"One tool, one action.\" [index=1]"
1873
+ "example": "- **List** \"Tool rules\" [items=1, marker=plus]\n - part `item` \"One tool, one action.\" [index=1]"
982
1874
  },
983
1875
  "examples": [
984
1876
  {
@@ -988,18 +1880,28 @@
988
1880
  {
989
1881
  "title": "A numbered sequence",
990
1882
  "code": "<List\n ordered\n label=\"Steps\"\n items={[\"Register the tool.\", \"Drive the DOM.\", \"Return the new state.\"]}\n/>"
1883
+ },
1884
+ {
1885
+ "title": "Plain bullets",
1886
+ "description": "A quieter dot for running prose, where the house plus would shout.",
1887
+ "code": "<List\n marker=\"bullet\"\n label=\"Caveats\"\n items={[\"Chrome 149 only.\", \"Tools are a no-op without WebMCP.\"]}\n/>"
1888
+ },
1889
+ {
1890
+ "title": "No marker",
1891
+ "description": "For items that already lead with their own mark, such as a link or a chip.",
1892
+ "code": "<List\n marker=\"none\"\n label=\"Related\"\n items={[\"Table for records.\", \"Stack for layout.\"]}\n/>"
991
1893
  }
992
1894
  ],
993
1895
  "a11y": {
994
1896
  "role": "list",
995
- "notes": "A real ul or ol named by its label, with an explicit list role because the custom markers require list-style none and Safari would otherwise drop the list semantics. The item count is announced, and the markers are drawn as pseudo-elements."
1897
+ "notes": "A real ul or ol named by its label, with an explicit list role because the custom markers require list-style none and Safari would otherwise drop the list semantics. The item count is announced, and the markers are drawn as pseudo-elements, so a marker never carries meaning a screen reader would miss."
996
1898
  }
997
1899
  },
998
1900
  {
999
1901
  "name": "MetaLine",
1000
1902
  "category": "display",
1001
1903
  "summary": "A slash-separated manifest line of term–detail pairs: serials, build strings, issue dates. It is chrome, not content, and in the agent view it reads as the same single line of text a person sees.",
1002
- "whenToUse": "Use it for the compact strip of identifying metadata that belongs to a page, panel, or footer: version and build identifiers, timestamps, serial numbers, owners. Values are short and the line truncates rather than wraps.",
1904
+ "whenToUse": "Use it for the compact strip of identifying metadata that belongs to a page, panel, or footer: version and build identifiers, timestamps, serial numbers, owners. Values are short. By default the line truncates with an ellipsis rather than wrapping; set wrap where it sits in a narrow container and every entry has to stay visible.",
1003
1905
  "whenNotToUse": "Do not use it for the details of a record a person is meant to study; that is a DescriptionList. Do not put anything interactive in it, and do not use it for prose.",
1004
1906
  "status": "experimental",
1005
1907
  "props": {
@@ -1007,12 +1909,21 @@
1007
1909
  "kind": "array",
1008
1910
  "description": "Term–detail pairs in display order: { term, detail }, both strings. Rendered as TERM: DETAIL, slash-separated, and carried as one line in the agent view. An empty array renders nothing.",
1009
1911
  "required": true
1912
+ },
1913
+ "wrap": {
1914
+ "kind": "boolean",
1915
+ "description": "Let entries flow onto further lines instead of truncating the line. Each entry stays whole on one line, and a separator stays at the end of the line it closes, so no line starts with a slash. An entry wider than the container is the only thing that still truncates.",
1916
+ "default": false
1010
1917
  }
1011
1918
  },
1012
1919
  "state": {
1013
1920
  "entries": {
1014
1921
  "description": "How many term–detail pairs the line carries.",
1015
1922
  "attribute": "data-sprint-entries"
1923
+ },
1924
+ "wrap": {
1925
+ "description": "Present when entries may flow onto further lines.",
1926
+ "attribute": "data-sprint-wrap"
1016
1927
  }
1017
1928
  },
1018
1929
  "agentView": {
@@ -1028,6 +1939,11 @@
1028
1939
  "title": "Version chrome for a footer",
1029
1940
  "description": "The line an app pins under its content or into a Shell rail.",
1030
1941
  "code": "<MetaLine\n entries={[\n { term: \"Sprint\", detail: \"v0.0.0\" },\n { term: \"Channel\", detail: \"dev\" },\n { term: \"WebMCP\", detail: \"chrome 149\" },\n ]}\n/>"
1942
+ },
1943
+ {
1944
+ "title": "Wrapping in a narrow rail",
1945
+ "description": "With wrap, a sidebar shows every entry on as many lines as it needs instead of cutting the later ones off.",
1946
+ "code": "<MetaLine\n wrap\n entries={[\n { term: \"Sprint\", detail: \"v0.0.0\" },\n { term: \"Channel\", detail: \"dev\" },\n { term: \"WebMCP\", detail: \"chrome 149\" },\n { term: \"Build\", detail: \"2744.07.22-a1\" },\n ]}\n/>"
1031
1947
  }
1032
1948
  ],
1033
1949
  "a11y": {
@@ -1080,282 +1996,573 @@
1080
1996
  ]
1081
1997
  },
1082
1998
  {
1083
- "name": "NavBar",
1999
+ "name": "NavGroup",
1084
2000
  "category": "navigation",
1085
- "summary": "A navigation landmark that renders as one line: where you are, written as a coordinate you can edit. Every destination stays in the page as an addressable part; the coordinate decides which of them a person is shown.",
1086
- "whenToUse": "Use it as an application's primary navigation when the catalogue is larger than a rail can hold, or when the layout needs its full width. Each segment of the coordinate opens its own level and filters as you type, so what a person chooses from is one level rather than the whole set. It takes destinations as data because it counts, filters and orders them.",
1087
- "whenNotToUse": "Do not use it for a handful of links that fit in a sidebar; that is Nav with NavGroup, which keeps every destination visible at once. Do not use it for links inside prose, and do not pass components in groups: each destination is a label and an href, not a node.",
2001
+ "summary": "A labelled cluster of links inside a Nav. The label names the group for screen readers and agents alike.",
2002
+ "whenToUse": "Use it when a Nav holds more than one kind of destination: guides versus components, product versus account. The label tells every reader, including an agent scanning for the right link, what the links below it have in common.",
2003
+ "whenNotToUse": "Do not use it outside a Nav; on its own it is just a heading over links, which Panel does better. Do not nest groups; one level of grouping is all a sidebar can carry.",
1088
2004
  "status": "experimental",
1089
2005
  "props": {
1090
2006
  "label": {
1091
2007
  "kind": "string",
1092
- "description": "What this navigation is for. Rendered as the landmark's accessible name and as the root of the coordinate.",
2008
+ "description": "What the links in this group have in common. Rendered as the rubric and as the group's accessible name.",
1093
2009
  "required": true
1094
2010
  },
1095
- "groups": {
1096
- "kind": "array",
1097
- "description": "The destinations, as groups of { label, items }, each item { href, label, active?, external? }. Order is the order a person travels them.",
2011
+ "children": {
2012
+ "kind": "node",
2013
+ "description": "The Link components this group collects.",
1098
2014
  "required": true
1099
- },
1100
- "emptyLabel": {
2015
+ }
2016
+ },
2017
+ "agentView": {
2018
+ "example": "- **NavGroup** \"Components\""
2019
+ },
2020
+ "a11y": {
2021
+ "role": "group",
2022
+ "notes": "The group carries its label as an accessible name, so screen readers announce the rubric when entering the cluster rather than reading an unlabelled run of links."
2023
+ },
2024
+ "relatedComponents": [
2025
+ "Nav",
2026
+ "Link"
2027
+ ],
2028
+ "examples": [
2029
+ {
2030
+ "title": "A labelled group of links",
2031
+ "code": "<NavGroup label=\"Reference\">\n <Link href=\"https://developer.chrome.com/docs/ai/webmcp\" external>\n Chrome docs\n </Link>\n <Link href=\"https://github.com/webmachinelearning/webmcp\" external>\n Specification\n </Link>\n</NavGroup>"
2032
+ }
2033
+ ]
2034
+ },
2035
+ {
2036
+ "name": "PageHeader",
2037
+ "category": "layout",
2038
+ "summary": "The top of a page: its h1 title, the Tag chips that classify it, an optional page-level control, and a lede underneath.",
2039
+ "whenToUse": "Use it once per page, as the first thing inside the content region. The label becomes the page's only h1, so the document outline starts here. Put status or category Tags in tags, a control that affects the whole page in actions, and the introductory sentence or two in children as Text.",
2040
+ "whenNotToUse": "Do not use it for a section within a page; that is a Panel with a headingLevel. Do not put navigation in actions; the page's links belong in a Nav.",
2041
+ "status": "experimental",
2042
+ "props": {
2043
+ "label": {
1101
2044
  "kind": "string",
1102
- "description": "What the bar says when a filter matches no destination.",
1103
- "default": "No match"
2045
+ "description": "The page title. Rendered as the page's h1.",
2046
+ "required": true
1104
2047
  },
1105
- "trailEmptyLabel": {
2048
+ "tags": {
2049
+ "kind": "node",
2050
+ "description": "Tag components that classify the page, rendered on the title line. Keep it to two or three."
2051
+ },
2052
+ "actions": {
2053
+ "kind": "node",
2054
+ "description": "A control that acts on the whole page, rendered at the end of the title line."
2055
+ },
2056
+ "children": {
2057
+ "kind": "node",
2058
+ "description": "The lede: a Text or two introducing the page."
2059
+ }
2060
+ },
2061
+ "agentView": {
2062
+ "example": "- **PageHeader** \"Button\""
2063
+ },
2064
+ "a11y": {
2065
+ "notes": "The label renders as the page's h1, so keep to one PageHeader per page. Tags and the lede are ordinary content after it; the header element itself takes no landmark role because it sits inside main."
2066
+ },
2067
+ "relatedComponents": [
2068
+ "Panel",
2069
+ "Heading",
2070
+ "Tag"
2071
+ ],
2072
+ "examples": [
2073
+ {
2074
+ "title": "A titled page with a lede",
2075
+ "code": "<PageHeader label=\"Reports\">\n <Text>Everything the quarter produced, in one place.</Text>\n</PageHeader>"
2076
+ },
2077
+ {
2078
+ "title": "Status tags and a page-level control",
2079
+ "description": "Tags classify the page on the title line; the action slot holds the one control that affects the whole page.",
2080
+ "code": "<PageHeader\n label=\"Button\"\n tags={<Tag tone=\"warning\" filled>experimental</Tag>}\n actions={<Button agentTool={false}>Refresh</Button>}\n>\n <Text>A single action a person or an agent can trigger.</Text>\n</PageHeader>"
2081
+ }
2082
+ ]
2083
+ },
2084
+ {
2085
+ "name": "Pagination",
2086
+ "category": "navigation",
2087
+ "summary": "A navigation landmark for a long set shown one page at a time: Previous and Next controls, a \"Page 2 of 3\" readout, and a \"Showing 26–50 of 61\" summary of which items are on screen. It takes counts rather than items, so it works the same over a local array or a server query.",
2088
+ "whenToUse": "Use it under or above a Table or List whose items arrive a page at a time, when the count is known. Give it onPageChange to page in place, which registers one turn tool that can jump straight to any page; give it href to make every page a URL, which needs no tool because a link is already reachable. Its state carries the page, the page count and the item range, so an agent reading the page knows how much it has not seen.",
2089
+ "whenNotToUse": "Do not use it when the total is unknown or the set grows as it is read; that is a load-more Button. Do not use it for steps in a flow a person must complete in order, and do not use it to move between unrelated pages, which is Nav or Breadcrumb. Do not use it for a set that fits on one screen: it renders, but has nothing to do.",
2090
+ "status": "experimental",
2091
+ "props": {
2092
+ "label": {
1106
2093
  "kind": "string",
1107
- "description": "What the bar says when nothing has been visited through it yet.",
1108
- "default": "Nothing visited yet"
2094
+ "description": "What is being paged. Names the navigation landmark and derives the tool name, so prefer a noun phrase such as \"Users pages\".",
2095
+ "required": true
1109
2096
  },
1110
- "open": {
1111
- "kind": "enum",
1112
- "description": "Which segment is open, or 'closed'. Pass it to drive the bar from outside, such as from an application-level shortcut; leave it off and the bar keeps its own state.",
1113
- "values": [
1114
- "trail",
1115
- "group",
1116
- "leaf",
1117
- "closed"
1118
- ]
2097
+ "page": {
2098
+ "kind": "number",
2099
+ "description": "The 1-based page currently shown. The component is controlled; a value outside 1 to the page count is clamped into it.",
2100
+ "required": true
1119
2101
  },
1120
- "onOpenChange": {
2102
+ "pageSize": {
2103
+ "kind": "number",
2104
+ "description": "How many items a full page holds. Values below 1 are treated as 1.",
2105
+ "required": true
2106
+ },
2107
+ "total": {
2108
+ "kind": "number",
2109
+ "description": "How many items the whole set holds. The page count is total divided by pageSize, rounded up, and never less than 1, so an empty set is one empty page.",
2110
+ "required": true
2111
+ },
2112
+ "onPageChange": {
1121
2113
  "kind": "handler",
1122
- "description": "Called with the segment the bar wants open, or 'closed'. Required when open is controlled, so the bar can still close itself."
2114
+ "description": "Called with the page to show. Without href the controls are buttons and the turn tool registers; with href it runs alongside the link, for routers that intercept clicks."
1123
2115
  },
1124
- "onNavigate": {
2116
+ "href": {
1125
2117
  "kind": "handler",
1126
- "description": "Called with the destination when one is chosen, before the browser follows the href. Use it to record the visit somewhere that outlives this component."
1127
- }
1128
- },
1129
- "state": {
1130
- "destinations": {
1131
- "description": "How many destinations the bar holds.",
1132
- "attribute": "data-sprint-destinations"
2118
+ "description": "Given a page number, returns its URL. When present, Previous and Next render as links, each publishes its URL as data-sprint-href, and no tool registers."
1133
2119
  },
1134
- "open": {
1135
- "description": "Which segment of the coordinate is open: trail, group, or leaf. Absent when the bar is closed to its one line.",
1136
- "attribute": "data-sprint-open"
2120
+ "previousLabel": {
2121
+ "kind": "string",
2122
+ "description": "The text of the control that goes back a page.",
2123
+ "default": "Previous"
1137
2124
  },
1138
- "depth": {
1139
- "description": "How many destinations have been visited through this bar.",
1140
- "attribute": "data-sprint-depth"
2125
+ "nextLabel": {
2126
+ "kind": "string",
2127
+ "description": "The text of the control that goes forward a page.",
2128
+ "default": "Next"
1141
2129
  },
1142
- "matches": {
1143
- "description": "How many destinations the open segment is showing.",
1144
- "attribute": "data-sprint-matches"
2130
+ "agentName": {
2131
+ "kind": "string",
2132
+ "description": "Override the label used to derive the tool name, when two paginations on a page would otherwise collide."
1145
2133
  },
1146
- "shown": {
1147
- "description": "On a destination: present when the open segment is currently showing it to a person. Every destination stays in the page either way.",
1148
- "attribute": "data-sprint-shown"
2134
+ "agentTool": {
2135
+ "kind": "boolean",
2136
+ "description": "Set false to render the controls without registering a turn tool.",
2137
+ "default": true
2138
+ }
2139
+ },
2140
+ "state": {
2141
+ "page": {
2142
+ "description": "The 1-based page shown, after clamping.",
2143
+ "attribute": "data-sprint-page"
1149
2144
  },
1150
- "group": {
1151
- "description": "On a destination: the group it belongs to.",
1152
- "attribute": "data-sprint-group"
2145
+ "pages": {
2146
+ "description": "How many pages the set spans. Never less than 1.",
2147
+ "attribute": "data-sprint-pages"
1153
2148
  },
1154
- "href": {
1155
- "description": "On a destination: where it goes.",
1156
- "attribute": "data-sprint-href"
2149
+ "total": {
2150
+ "description": "How many items the whole set holds.",
2151
+ "attribute": "data-sprint-total"
1157
2152
  },
1158
- "active": {
1159
- "description": "On a destination: present when it is the current page.",
1160
- "attribute": "data-sprint-active"
2153
+ "first": {
2154
+ "description": "The 1-based position of the first item on this page, or 0 when the set is empty.",
2155
+ "attribute": "data-sprint-first"
2156
+ },
2157
+ "last": {
2158
+ "description": "The 1-based position of the last item on this page, or 0 when the set is empty.",
2159
+ "attribute": "data-sprint-last"
2160
+ }
2161
+ },
2162
+ "tools": {
2163
+ "turn": {
2164
+ "verb": "turn",
2165
+ "description": "Go to a page of this paginated set by its 1-based number, exactly as a person pressing Previous or Next until they reach it would. Any page from 1 to the page count is accepted, not just the neighbours. Returns the pagination's state after the change, including which items are now showing, so a follow-up read is unnecessary.",
2166
+ "inputSchema": {
2167
+ "type": "object",
2168
+ "properties": {
2169
+ "page": {
2170
+ "type": "integer",
2171
+ "description": "The 1-based number of the page to show.",
2172
+ "minimum": 1
2173
+ }
2174
+ },
2175
+ "required": [
2176
+ "page"
2177
+ ]
2178
+ },
2179
+ "readOnly": false,
2180
+ "untrustedContent": false,
2181
+ "registeredWhen": "The pagination is mounted, changes page through onPageChange rather than href, has more than one page, has a resolvable label, and no other component claims the same tool name. The registered schema states the current page count.",
2182
+ "unregisteredWhen": "The pagination unmounts, is given href, loses onPageChange, or shrinks to a single page."
1161
2183
  }
1162
2184
  },
1163
2185
  "agentView": {
1164
- "example": "- **NavBar** \"Workbench\" [destinations=2, depth=0]\n - part `destination` \"Button\" [group=action, href=#/Button]\n - part `destination` \"Table\" [group=display, href=#/Table, active, shown]"
2186
+ "example": "- **Pagination** \"Users pages\" [first=26, last=50, page=2, pages=3, total=61] → tool `turn-users-pages`\n - part `previous` \"Previous\"\n - part `next` \"Next\""
1165
2187
  },
1166
2188
  "a11y": {
1167
2189
  "role": "navigation",
1168
- "notes": "The label is the landmark's accessible name. The current destination carries aria-current=page. Destinations the coordinate is not showing are hidden from the accessibility tree by CSS, so a screen reader travels one level at a time exactly as a sighted reader does, while the page itself keeps all of them. From the field, ArrowDown enters the results and ArrowUp at the top returns to it; typing anywhere in the results goes back to the field and keeps the character. Escape closes the bar, as does a press outside it, and the match count is a polite live region. ArrowUp in an empty field recalls the trail, the way a console recalls history."
2190
+ "keyboard": [
2191
+ "Tab reaches Previous and Next in order",
2192
+ "Enter or Space on a button, Enter on a link, turns the page"
2193
+ ],
2194
+ "notes": "The root is a nav landmark named by label. A control with nowhere to go is a disabled button, or a link with no href and aria-disabled, so it leaves the tab order. The item summary is a polite live region, so a screen reader hears the new range after a turn."
1169
2195
  },
1170
2196
  "relatedComponents": [
2197
+ "Table",
2198
+ "List",
1171
2199
  "Nav",
1172
- "NavGroup",
1173
- "Link",
1174
- "Shell"
2200
+ "Breadcrumb"
1175
2201
  ],
1176
2202
  "examples": [
1177
2203
  {
1178
- "title": "A coordinate bar",
1179
- "description": "One line of chrome over a grouped catalogue. Clicking a segment opens that level and filters it as you type.",
1180
- "code": "<NavBar\n label=\"Workbench\"\n groups={[\n { label: \"action\", items: [{ href: \"#/Button\", label: \"Button\" }] },\n { label: \"display\", items: [{ href: \"#/Table\", label: \"Table\", active: true }] },\n ]}\n/>"
2204
+ "title": "Paging a table in place",
2205
+ "description": "With onPageChange the controls are buttons and one turn tool registers, so an agent can jump to page 3 without pressing Next twice.",
2206
+ "code": "<Pagination\n label=\"Users pages\"\n page={page}\n pageSize={25}\n total={61}\n onPageChange={setPage}\n/>"
1181
2207
  },
1182
2208
  {
1183
- "title": "Recording where a person has been",
1184
- "description": "The bar keeps a trail of the destinations chosen through it, reachable from the depth count at its head. onNavigate is how that outlives the component.",
1185
- "code": "<NavBar\n label=\"Docs\"\n groups={[{ label: \"guides\", items: [{ href: \"#/guide/webmcp\", label: \"WebMCP\" }] }]}\n onNavigate={(destination) => console.log(destination.href)}\n/>"
2209
+ "title": "Pages as links",
2210
+ "description": "With href every page is a URL. The controls are links, nothing registers, and each link publishes where it goes.",
2211
+ "code": "<Pagination\n label=\"Changelog pages\"\n page={2}\n pageSize={10}\n total={42}\n href={(page) => `#/changelog?page=${page}`}\n/>"
2212
+ },
2213
+ {
2214
+ "title": "The last page, relabelled",
2215
+ "description": "On the last page Next is disabled and the summary shows the short final range. The labels are overridable for sets that read better as time.",
2216
+ "code": "<Pagination\n label=\"Activity pages\"\n page={4}\n pageSize={8}\n total={31}\n previousLabel=\"Newer\"\n nextLabel=\"Older\"\n onPageChange={setPage}\n/>"
2217
+ },
2218
+ {
2219
+ "title": "An empty set",
2220
+ "description": "No items is one empty page. Both controls are disabled, no tool registers, and the summary still says what it is showing.",
2221
+ "code": "<Pagination\n label=\"Search results pages\"\n page={1}\n pageSize={20}\n total={0}\n onPageChange={setPage}\n/>"
1186
2222
  }
1187
2223
  ]
1188
2224
  },
1189
2225
  {
1190
- "name": "NavGroup",
1191
- "category": "navigation",
1192
- "summary": "A labelled cluster of links inside a Nav. The label names the group for screen readers and agents alike.",
1193
- "whenToUse": "Use it when a Nav holds more than one kind of destination: guides versus components, product versus account. The label tells every reader, including an agent scanning for the right link, what the links below it have in common.",
1194
- "whenNotToUse": "Do not use it outside a Nav; on its own it is just a heading over links, which Panel does better. Do not nest groups; one level of grouping is all a sidebar can carry.",
2226
+ "name": "Panel",
2227
+ "category": "layout",
2228
+ "summary": "A labelled region with a header band and an optional slot for the controls that act on it. Everything on a Sprint page lives inside one.",
2229
+ "whenToUse": "Use it for every distinct region of a page: a section of documentation, a form, a readout, a preview. The label is the region's accessible name, so a person, a screen reader, and an agent all address the region by the same words.",
2230
+ "whenNotToUse": "Do not use it as a spacer or a plain box; that is Stack. Nesting reads clearly to about three deep, because each level alternates its ground and demotes its frame; past that, the depth cues repeat and the region wants a page of its own.",
1195
2231
  "status": "experimental",
1196
2232
  "props": {
1197
2233
  "label": {
1198
2234
  "kind": "string",
1199
- "description": "What the links in this group have in common. Rendered as the rubric and as the group's accessible name.",
2235
+ "description": "What this region is. Rendered in the header band and used as the region's accessible name.",
1200
2236
  "required": true
1201
2237
  },
1202
2238
  "children": {
1203
2239
  "kind": "node",
1204
- "description": "The Link components this group collects.",
1205
- "required": true
2240
+ "description": "The region's content. An empty panel says it is empty rather than collapsing."
2241
+ },
2242
+ "headingLevel": {
2243
+ "kind": "enum",
2244
+ "description": "Render the label as a real heading at this outline depth, so the section is reachable when a screen reader navigates by headings. Set it on every panelled section of a page; leave it unset only for chrome such as a preview frame.",
2245
+ "values": [
2246
+ "2",
2247
+ "3",
2248
+ "4"
2249
+ ]
2250
+ },
2251
+ "actions": {
2252
+ "kind": "node",
2253
+ "description": "Controls that act on this region, rendered at the end of the header band. Keep it to one or two."
2254
+ },
2255
+ "flush": {
2256
+ "kind": "boolean",
2257
+ "description": "Drop the body padding, for content that draws its own edges such as a Table or a CodeBlock.",
2258
+ "default": false
2259
+ },
2260
+ "emptyLabel": {
2261
+ "kind": "string",
2262
+ "description": "What the panel says when it has no content.",
2263
+ "default": "Empty"
2264
+ },
2265
+ "loading": {
2266
+ "kind": "boolean",
2267
+ "description": "Set while the content are being fetched. Sets aria-busy and sweeps a bar along the top edge. Existing content stay visible; with none yet, the empty slot says loadingLabel instead of emptyLabel.",
2268
+ "default": false
2269
+ },
2270
+ "loadingLabel": {
2271
+ "kind": "string",
2272
+ "description": "What the empty slot says while loading.",
2273
+ "default": "Loading"
2274
+ }
2275
+ },
2276
+ "state": {
2277
+ "flush": {
2278
+ "description": "Present when the body carries no padding of its own.",
2279
+ "attribute": "data-sprint-flush"
2280
+ },
2281
+ "empty": {
2282
+ "description": "Present when the panel has no content. The panel still renders its keyline and says it is empty.",
2283
+ "attribute": "data-sprint-empty"
2284
+ },
2285
+ "loading": {
2286
+ "description": "Present while the content are being fetched. Alongside empty it means nothing has arrived yet, not that there is nothing.",
2287
+ "attribute": "data-sprint-loading"
1206
2288
  }
1207
2289
  },
1208
2290
  "agentView": {
1209
- "example": "- **NavGroup** \"Components\""
2291
+ "example": "- **Panel** \"WebMCP tools\""
1210
2292
  },
1211
2293
  "a11y": {
1212
- "role": "group",
1213
- "notes": "The group carries its label as an accessible name, so screen readers announce the rubric when entering the cluster rather than reading an unlabelled run of links."
2294
+ "role": "region",
2295
+ "notes": "The section is a named landmark either way: the label is its accessible name. With headingLevel the label is also a heading element, so the page outline includes the region; without it the region is reachable only by landmark navigation."
1214
2296
  },
1215
- "relatedComponents": [
1216
- "Nav",
1217
- "Link"
1218
- ],
1219
2297
  "examples": [
1220
2298
  {
1221
- "title": "A labelled group of links",
1222
- "code": "<NavGroup label=\"Reference\">\n <Link href=\"https://developer.chrome.com/docs/ai/webmcp\" external>\n Chrome docs\n </Link>\n <Link href=\"https://github.com/webmachinelearning/webmcp\" external>\n Specification\n </Link>\n</NavGroup>"
2299
+ "title": "A section of a page",
2300
+ "description": "headingLevel puts the label in the page outline, so a screen reader finds the section by heading as well as by landmark.",
2301
+ "code": "<Panel label=\"When to use\" headingLevel={2}>\n <Text>Use it for any discrete action.</Text>\n</Panel>"
2302
+ },
2303
+ {
2304
+ "title": "A panel with a control in its header",
2305
+ "description": "The header slot is for controls that act on the region, not for navigation.",
2306
+ "code": "<Panel\n label=\"Preview\"\n actions={<Button agentName=\"Reset preview\">Reset</Button>}\n>\n <Button tone=\"action\">Prepare launch</Button>\n</Panel>"
2307
+ },
2308
+ {
2309
+ "title": "A flush panel around a table",
2310
+ "description": "Content that draws its own keylines sits flush, so borders do not double up.",
2311
+ "code": "<Panel label=\"Conventions\" flush>\n <Table label=\"Conventions\" columns={columns} rows={rows} />\n</Panel>"
2312
+ },
2313
+ {
2314
+ "title": "Nested panels",
2315
+ "description": "Depth styles itself: the outermost panel carries a doubled keyline, each nested level alternates its ground, and nested headers demote to a dashed rule, so a reader ranks the levels without counting borders.",
2316
+ "code": "<Panel label=\"The shape\" headingLevel={2}>\n <Panel label=\"Human view\" headingLevel={3}>\n <Panel label=\"Crew\" headingLevel={4}>\n <Text>Registration fields live here.</Text>\n </Panel>\n </Panel>\n</Panel>"
2317
+ },
2318
+ {
2319
+ "title": "An empty region",
2320
+ "description": "An empty panel keeps its border and states that it is empty, rather than vanishing and leaving a person or an agent unsure whether it failed to load.",
2321
+ "code": "<Panel label=\"Registered tools\" emptyLabel=\"No tools registered\" />"
1223
2322
  }
1224
2323
  ]
1225
2324
  },
1226
2325
  {
1227
- "name": "PageHeader",
1228
- "category": "layout",
1229
- "summary": "The top of a page: its h1 title, the Tag chips that classify it, an optional page-level control, and a lede underneath.",
1230
- "whenToUse": "Use it once per page, as the first thing inside the content region. The label becomes the page's only h1, so the document outline starts here. Put status or category Tags in tags, a control that affects the whole page in actions, and the introductory sentence or two in children as Text.",
1231
- "whenNotToUse": "Do not use it for a section within a page; that is a Panel with a headingLevel. Do not put navigation in actions; the page's links belong in a Nav.",
2326
+ "name": "Pending",
2327
+ "category": "feedback",
2328
+ "summary": "Marks a region whose data is being fetched. Keeps stale content visible and readable while it refreshes, and holds a labelled pending field when there is nothing to show yet.",
2329
+ "whenToUse": "Wrap a region you build yourself while its data loads: a profile card, a chart, a custom summary. Pass the children only once data exists, so the first load shows the pending field and a refetch keeps the old content under a busy bar. Table, List, DescriptionList and Panel take a loading prop of their own, so reach for that first.",
2330
+ "whenNotToUse": "Do not wrap a Table, List, DescriptionList or Panel; use its loading prop, which keeps the region's own label and frame. Do not use for a long job with a countable amount of work; that is a Progress. Do not use for a busy action; that is Button's loading prop.",
1232
2331
  "status": "experimental",
1233
2332
  "props": {
2333
+ "loading": {
2334
+ "kind": "boolean",
2335
+ "description": "Whether the region's data is being fetched. While false the wrapper adds nothing to the agent view.",
2336
+ "required": true
2337
+ },
1234
2338
  "label": {
1235
2339
  "kind": "string",
1236
- "description": "The page title. Rendered as the page's h1.",
2340
+ "description": "What is loading, as a phrase a person can read in the empty field, such as \"Loading profile\".",
1237
2341
  "required": true
1238
2342
  },
1239
- "tags": {
2343
+ "children": {
1240
2344
  "kind": "node",
1241
- "description": "Tag components that classify the page, rendered on the title line. Keep it to two or three."
2345
+ "description": "The region's content. Omit it until data exists; while loading with children they stay visible as stale content."
2346
+ }
2347
+ },
2348
+ "state": {
2349
+ "loading": {
2350
+ "description": "Present while the region's data is being fetched.",
2351
+ "attribute": "data-sprint-loading"
2352
+ },
2353
+ "empty": {
2354
+ "description": "Present while loading with no content yet, so nothing on screen is data.",
2355
+ "attribute": "data-sprint-empty"
2356
+ }
2357
+ },
2358
+ "agentView": {
2359
+ "example": "- **Pending** \"Loading profile\" [empty, loading]"
2360
+ },
2361
+ "examples": [
2362
+ {
2363
+ "title": "First load",
2364
+ "description": "No data yet, so the region holds a labelled pending field.",
2365
+ "code": "<Pending loading={isLoading} label=\"Loading profile\">\n {profile && <ProfileSummary profile={profile} />}\n</Pending>"
2366
+ },
2367
+ {
2368
+ "title": "Refetching stale content",
2369
+ "description": "Content already on screen stays readable under a busy bar, and an agent reads it nested under a loading line.",
2370
+ "code": "<Pending loading={isFetching} label=\"Refreshing pilot\">\n <Stack gap=\"tight\">\n <Heading level={3}>{pilot.callsign}</Heading>\n <Text tone=\"muted\">{pilot.status}</Text>\n </Stack>\n</Pending>"
2371
+ }
2372
+ ],
2373
+ "a11y": {
2374
+ "role": "group",
2375
+ "notes": "While loading the wrapper is a group named by its label with aria-busy set, so assistive technology knows the content may change. Idle, it is a plain element with no role."
2376
+ }
2377
+ },
2378
+ {
2379
+ "name": "Progress",
2380
+ "category": "feedback",
2381
+ "summary": "A labelled loading indicator for work in progress. Indeterminate by default; pass value for a known fraction complete.",
2382
+ "whenToUse": "Use while something the person is waiting on is still running: a page section fetching, an upload, an import, a long job. Omit value when the remaining work is unknown, and pass value with max when it can be counted. Keep the label a stable noun phrase for what is loading, not a percentage.",
2383
+ "whenNotToUse": "Do not use for a busy action; Button's loading prop already marks the button itself. Do not use for the outcome of work that finished or failed; that is an Alert. Do not use for a quantity that is not progress, such as disk usage.",
2384
+ "status": "experimental",
2385
+ "props": {
2386
+ "label": {
2387
+ "kind": "string",
2388
+ "description": "What is loading, such as \"Importing manifest\". It is the indicator's accessible name and its agent label.",
2389
+ "required": true
2390
+ },
2391
+ "value": {
2392
+ "kind": "number",
2393
+ "description": "Work completed so far, in the same units as max. Omit for an indeterminate indicator. Clamped to the range 0 to max."
1242
2394
  },
1243
- "actions": {
1244
- "kind": "node",
1245
- "description": "A control that acts on the whole page, rendered at the end of the title line."
2395
+ "max": {
2396
+ "kind": "number",
2397
+ "description": "The value at which the work is complete.",
2398
+ "default": 100
2399
+ }
2400
+ },
2401
+ "state": {
2402
+ "loading": {
2403
+ "description": "Present until the work completes. An indeterminate indicator is always loading.",
2404
+ "attribute": "data-sprint-loading"
1246
2405
  },
1247
- "children": {
1248
- "kind": "node",
1249
- "description": "The lede: a Text or two introducing the page."
2406
+ "value": {
2407
+ "description": "The fraction complete as a whole percentage, such as 40%. Absent while indeterminate.",
2408
+ "attribute": "data-sprint-value"
1250
2409
  }
1251
2410
  },
1252
2411
  "agentView": {
1253
- "example": "- **PageHeader** \"Button\""
2412
+ "example": "- **Progress** \"Importing manifest\" [loading, value=40%]"
1254
2413
  },
1255
- "a11y": {
1256
- "notes": "The label renders as the page's h1, so keep to one PageHeader per page. Tags and the lede are ordinary content after it; the header element itself takes no landmark role because it sits inside main."
1257
- },
1258
- "relatedComponents": [
1259
- "Panel",
1260
- "Heading",
1261
- "Tag"
1262
- ],
1263
2414
  "examples": [
1264
2415
  {
1265
- "title": "A titled page with a lede",
1266
- "code": "<PageHeader label=\"Reports\">\n <Text>Everything the quarter produced, in one place.</Text>\n</PageHeader>"
2416
+ "title": "Indeterminate load",
2417
+ "description": "Nothing to count yet, so the indicator only says that work is running.",
2418
+ "code": "<Progress label=\"Loading flight plan\" />"
1267
2419
  },
1268
2420
  {
1269
- "title": "Status tags and a page-level control",
1270
- "description": "Tags classify the page on the title line; the action slot holds the one control that affects the whole page.",
1271
- "code": "<PageHeader\n label=\"Button\"\n tags={<Tag tone=\"warning\" filled>experimental</Tag>}\n actions={<Button agentTool={false}>Refresh</Button>}\n>\n <Text>A single action a person or an agent can trigger.</Text>\n</PageHeader>"
2421
+ "title": "Counted progress",
2422
+ "description": "With value and max the agent reads a percentage instead of guessing.",
2423
+ "code": "<Progress label=\"Importing manifest\" value={imported} max={total} />"
2424
+ },
2425
+ {
2426
+ "title": "Complete",
2427
+ "description": "At max the loading state clears, so the line reads as finished rather than stalled.",
2428
+ "code": "<Progress label=\"Importing manifest\" value={240} max={240} />"
1272
2429
  }
1273
- ]
2430
+ ],
2431
+ "a11y": {
2432
+ "role": "progressbar",
2433
+ "notes": "Renders a native progress element named by the visible label. Omitting value leaves it indeterminate, which assistive technology announces as busy. The percentage readout is hidden from assistive technology because the progressbar already carries its value."
2434
+ }
1274
2435
  },
1275
2436
  {
1276
- "name": "Panel",
1277
- "category": "layout",
1278
- "summary": "A labelled region with a header band and an optional slot for the controls that act on it. Everything on a Sprint page lives inside one.",
1279
- "whenToUse": "Use it for every distinct region of a page: a section of documentation, a form, a readout, a preview. The label is the region's accessible name, so a person, a screen reader, and an agent all address the region by the same words.",
1280
- "whenNotToUse": "Do not use it as a spacer or a plain box; that is Stack. Nesting reads clearly to about three deep, because each level alternates its ground and demotes its frame; past that, the depth cues repeat and the region wants a page of its own.",
2437
+ "name": "SearchField",
2438
+ "category": "input",
2439
+ "summary": "A search box inside its own search landmark: a labelled query field with a clear control, Escape to clear, Enter to submit, and an optional page-wide key that focuses it. It registers one search tool that sets the query and submits it.",
2440
+ "whenToUse": "Use it to filter or search a collection on the page, such as narrowing a users list by name. The landmark lets screen reader users jump straight to it, and the optional shortcut gives keyboard users the familiar slash-to-search.",
2441
+ "whenNotToUse": "Do not use it for any other single-line value; a name, an email address, or an identifier is a TextInput. Do not use it to choose from a short known set, which is a Select or a SegmentedControl. Do not give two fields on one page the same shortcut.",
1281
2442
  "status": "experimental",
1282
2443
  "props": {
1283
2444
  "label": {
1284
2445
  "kind": "string",
1285
- "description": "What this region is. Rendered in the header band and used as the region's accessible name.",
2446
+ "description": "What is being searched, as a noun phrase such as \"Users\". Names the landmark and the field, and derives the tool name, so \"Users\" becomes search-users.",
1286
2447
  "required": true
1287
2448
  },
1288
- "children": {
1289
- "kind": "node",
1290
- "description": "The region's content. An empty panel says it is empty rather than collapsing."
2449
+ "value": {
2450
+ "kind": "string",
2451
+ "description": "The current query. The field is fully controlled.",
2452
+ "required": true
1291
2453
  },
1292
- "headingLevel": {
1293
- "kind": "enum",
1294
- "description": "Render the label as a real heading at this outline depth, so the section is reachable when a screen reader navigates by headings. Set it on every panelled section of a page; leave it unset only for chrome such as a preview frame.",
1295
- "values": [
1296
- "2",
1297
- "3",
1298
- "4"
1299
- ]
2454
+ "onChange": {
2455
+ "kind": "handler",
2456
+ "description": "Called with the new query on every change, including the clear control, Escape, and the search tool.",
2457
+ "required": true
1300
2458
  },
1301
- "actions": {
1302
- "kind": "node",
1303
- "description": "Controls that act on this region, rendered at the end of the header band. Keep it to one or two."
2459
+ "onSubmit": {
2460
+ "kind": "handler",
2461
+ "description": "Called with the query when Enter is pressed or the search tool runs. Leave it off for a live filter that reacts to onChange alone."
1304
2462
  },
1305
- "flush": {
2463
+ "placeholder": {
2464
+ "kind": "string",
2465
+ "description": "Ghost text shown while the field is empty, e.g. \"Name or email\"."
2466
+ },
2467
+ "hideLabel": {
1306
2468
  "kind": "boolean",
1307
- "description": "Drop the body padding, for content that draws its own edges such as a Table or a CodeBlock.",
2469
+ "description": "Hide the label visually while keeping it for screen readers and agents. Pair it with a placeholder so sighted people still know what the field searches.",
1308
2470
  "default": false
1309
2471
  },
1310
- "emptyLabel": {
2472
+ "shortcut": {
1311
2473
  "kind": "string",
1312
- "description": "What the panel says when it has no content.",
1313
- "default": "Empty"
2474
+ "description": "A single key that focuses the field from anywhere on the page, shown as a key chip while the field is empty and unfocused. Ignored while focus is in another editable element or a modifier is held. Pass \"/\" for the common convention; omit or pass false for none."
2475
+ },
2476
+ "name": {
2477
+ "kind": "string",
2478
+ "description": "The native form name of the query input."
2479
+ },
2480
+ "disabled": {
2481
+ "kind": "boolean",
2482
+ "description": "Disable the field, its shortcut, and its search tool.",
2483
+ "default": false
2484
+ },
2485
+ "agentName": {
2486
+ "kind": "string",
2487
+ "description": "Override the label used to derive the tool name, when two search fields on a page would otherwise collide."
2488
+ },
2489
+ "agentTool": {
2490
+ "kind": "boolean",
2491
+ "description": "Set false to render the field without registering a search tool.",
2492
+ "default": true
1314
2493
  }
1315
2494
  },
1316
2495
  "state": {
1317
- "flush": {
1318
- "description": "Present when the body carries no padding of its own.",
1319
- "attribute": "data-sprint-flush"
2496
+ "value": {
2497
+ "description": "The current query, absent while the field is empty.",
2498
+ "attribute": "data-sprint-value"
1320
2499
  },
1321
2500
  "empty": {
1322
- "description": "Present when the panel has no content. The panel still renders its keyline and says it is empty.",
2501
+ "description": "Present while the field holds no query.",
1323
2502
  "attribute": "data-sprint-empty"
2503
+ },
2504
+ "shortcut": {
2505
+ "description": "The key that focuses the field from anywhere on the page.",
2506
+ "attribute": "data-sprint-shortcut"
2507
+ },
2508
+ "disabled": {
2509
+ "description": "Present when the field cannot be edited.",
2510
+ "attribute": "data-sprint-disabled"
1324
2511
  }
1325
2512
  },
1326
- "agentView": {
1327
- "example": "- **Panel** \"WebMCP tools\""
2513
+ "tools": {
2514
+ "search": {
2515
+ "verb": "search",
2516
+ "description": "Search with this field: replace its text with the query, exactly as a person typing it would, then submit it as pressing Enter would. Pass an empty string to clear the search. Results usually render elsewhere on the page; this returns the field's state after the search, so read the results region next.",
2517
+ "inputSchema": {
2518
+ "type": "object",
2519
+ "properties": {
2520
+ "query": {
2521
+ "type": "string",
2522
+ "description": "The full search text. Replaces the current query rather than appending to it; an empty string clears the search."
2523
+ }
2524
+ },
2525
+ "required": [
2526
+ "query"
2527
+ ]
2528
+ },
2529
+ "readOnly": false,
2530
+ "untrustedContent": true,
2531
+ "registeredWhen": "The field is mounted, enabled, has a resolvable label, and no other component claims the same tool name.",
2532
+ "unregisteredWhen": "The field unmounts or becomes disabled."
2533
+ }
1328
2534
  },
1329
- "a11y": {
1330
- "role": "region",
1331
- "notes": "The section is a named landmark either way: the label is its accessible name. With headingLevel the label is also a heading element, so the page outline includes the region; without it the region is reachable only by landmark navigation."
2535
+ "agentView": {
2536
+ "example": "- **SearchField** \"Users\" [empty, shortcut=/] → tool `search-users`"
1332
2537
  },
1333
2538
  "examples": [
1334
2539
  {
1335
- "title": "A section of a page",
1336
- "description": "headingLevel puts the label in the page outline, so a screen reader finds the section by heading as well as by landmark.",
1337
- "code": "<Panel label=\"When to use\" headingLevel={2}>\n <Text>Use it for any discrete action.</Text>\n</Panel>"
1338
- },
1339
- {
1340
- "title": "A panel with a control in its header",
1341
- "description": "The header slot is for controls that act on the region, not for navigation.",
1342
- "code": "<Panel\n label=\"Preview\"\n actions={<Button agentName=\"Reset preview\">Reset</Button>}\n>\n <Button tone=\"action\">Prepare launch</Button>\n</Panel>"
1343
- },
1344
- {
1345
- "title": "A flush panel around a table",
1346
- "description": "Content that draws its own keylines sits flush, so borders do not double up.",
1347
- "code": "<Panel label=\"Conventions\" flush>\n <Table label=\"Conventions\" columns={columns} rows={rows} />\n</Panel>"
2540
+ "title": "Filtering a list",
2541
+ "description": "A live filter: onChange narrows the list as the person types, so there is no onSubmit. The label is hidden and the placeholder says what can be matched.",
2542
+ "code": "<SearchField\n label=\"Users\"\n hideLabel\n value={query}\n onChange={setQuery}\n placeholder=\"Name or email\"\n/>"
1348
2543
  },
1349
2544
  {
1350
- "title": "Nested panels",
1351
- "description": "Depth styles itself: the outermost panel carries a doubled keyline, each nested level alternates its ground, and nested headers demote to a dashed rule, so a reader ranks the levels without counting borders.",
1352
- "code": "<Panel label=\"The shape\" headingLevel={2}>\n <Panel label=\"Human view\" headingLevel={3}>\n <Panel label=\"Crew\" headingLevel={4}>\n <Text>Registration fields live here.</Text>\n </Panel>\n </Panel>\n</Panel>"
2545
+ "title": "A slash shortcut",
2546
+ "description": "Pressing / anywhere on the page focuses the field unless focus is already in something editable. The key chip disappears once the field has focus or a query.",
2547
+ "code": "<SearchField\n label=\"Users\"\n value={query}\n onChange={setQuery}\n shortcut=\"/\"\n placeholder=\"Name or email\"\n/>"
1353
2548
  },
1354
2549
  {
1355
- "title": "An empty region",
1356
- "description": "An empty panel keeps its border and states that it is empty, rather than vanishing and leaving a person or an agent unsure whether it failed to load.",
1357
- "code": "<Panel label=\"Registered tools\" emptyLabel=\"No tools registered\" />"
2550
+ "title": "Submitting a query",
2551
+ "description": "For a search that is too costly to run on every keystroke, onSubmit receives the query on Enter and when the search tool runs.",
2552
+ "code": "<SearchField\n label=\"Flight logs\"\n value={query}\n onChange={setQuery}\n onSubmit={runSearch}\n placeholder=\"Callsign or tail number\"\n/>"
1358
2553
  }
2554
+ ],
2555
+ "a11y": {
2556
+ "role": "search",
2557
+ "keyboard": [
2558
+ "Enter submits the query",
2559
+ "Escape clears a non-empty query and keeps focus; on an empty field it passes through, so an enclosing dialog can close",
2560
+ "The shortcut key, when set, focuses the field from anywhere on the page"
2561
+ ],
2562
+ "notes": "The root is a form with role search, labelled by the field's label, so it is listed as a search landmark. The input is type search and announces its shortcut with aria-keyshortcuts; the key chip itself is hidden from assistive technology. The clear control appears only while there is a query, and returns focus to the field."
2563
+ },
2564
+ "relatedComponents": [
2565
+ "TextInput"
1359
2566
  ]
1360
2567
  },
1361
2568
  {
@@ -1429,7 +2636,7 @@
1429
2636
  },
1430
2637
  "options": {
1431
2638
  "kind": "array",
1432
- "description": "The choices in display order: { value, label }. The label is what a person sees and what the select tool accepts, so an agent never has to know the value.",
2639
+ "description": "The choices in display order: { value, label, count? }. The label is what a person sees and what the select tool accepts, so an agent never has to know the value. count renders as a muted chip beside the label and reaches the agent view as part state, so never fold a count into the label.",
1433
2640
  "required": true
1434
2641
  },
1435
2642
  "value": {
@@ -1442,6 +2649,19 @@
1442
2649
  "description": "Called with the newly selected value. The select tool drives a real click, so this runs for agent selections too.",
1443
2650
  "required": true
1444
2651
  },
2652
+ "savedValue": {
2653
+ "kind": "string",
2654
+ "description": "The value currently in effect, for a control that stages a change until something confirms it. While it differs from value the saved option keeps a marker and the control reports itself dirty, so both the saved and the proposed choice stay readable."
2655
+ },
2656
+ "hint": {
2657
+ "kind": "string",
2658
+ "description": "Guidance shown under the options, linked with aria-describedby and carried into the agent view. Say what a staged change does, such as when it takes effect."
2659
+ },
2660
+ "block": {
2661
+ "kind": "boolean",
2662
+ "description": "Fill the container's width, with every option taking an equal share. Without it the control stays as wide as its options, even inside a stretching column.",
2663
+ "default": false
2664
+ },
1445
2665
  "disabled": {
1446
2666
  "kind": "boolean",
1447
2667
  "description": "Disable every option and unregister the select tool.",
@@ -1462,6 +2682,14 @@
1462
2682
  "description": "The value of the option currently selected.",
1463
2683
  "attribute": "data-sprint-value"
1464
2684
  },
2685
+ "dirty": {
2686
+ "description": "Present while savedValue is set and differs from value. The saved option carries data-sprint-saved.",
2687
+ "attribute": "data-sprint-dirty"
2688
+ },
2689
+ "block": {
2690
+ "description": "Present when the control fills its container.",
2691
+ "attribute": "data-sprint-block"
2692
+ },
1465
2693
  "disabled": {
1466
2694
  "description": "Present when no option can be chosen.",
1467
2695
  "attribute": "data-sprint-disabled"
@@ -1490,7 +2718,7 @@
1490
2718
  }
1491
2719
  },
1492
2720
  "agentView": {
1493
- "example": "- **SegmentedControl** \"Page view\" [value=human] → tool `select-page-view`\n - part `option` \"human\" [checked]\n - part `option` \"agent\""
2721
+ "example": "- **SegmentedControl** \"Access\" [dirty, value=write] → tool `select-access`\n - part `option` \"Read\" [count=17, saved]\n - part `option` \"Write\" [checked, count=4]\n - part `hint` \"Currently Read. Nothing changes until you confirm.\""
1494
2722
  },
1495
2723
  "examples": [
1496
2724
  {
@@ -1498,6 +2726,21 @@
1498
2726
  "description": "In agent view each option renders as its own control, so an agent driving the DOM can click one without WebMCP.",
1499
2727
  "code": "<SegmentedControl\n label=\"Page view\"\n value={view}\n onChange={setView}\n options={[\n { value: \"human\", label: \"human\" },\n { value: \"agent\", label: \"agent\" },\n ]}\n/>"
1500
2728
  },
2729
+ {
2730
+ "title": "Options with counts",
2731
+ "description": "A count is data, not label text: it renders as a chip, joins the accessible name, and reaches agents as part state while the select tool still takes the plain label.",
2732
+ "code": "<SegmentedControl\n label=\"Members\"\n value={filter}\n onChange={setFilter}\n options={[\n { value: \"all\", label: \"All\", count: 48 },\n { value: \"active\", label: \"Active\", count: 18 },\n { value: \"never\", label: \"Never signed in\", count: 30 },\n ]}\n/>"
2733
+ },
2734
+ {
2735
+ "title": "A staged change",
2736
+ "description": "savedValue keeps the value in effect visible while another is selected, and hint says what confirming will do. The control reports itself dirty until the two agree.",
2737
+ "code": "<SegmentedControl\n label=\"Access\"\n value={access}\n savedValue=\"read\"\n onChange={setAccess}\n hint=\"Currently Read. Nothing changes until you confirm.\"\n options={[\n { value: \"read\", label: \"Read\" },\n { value: \"write\", label: \"Write\" },\n { value: \"admin\", label: \"Admin\" },\n ]}\n/>"
2738
+ },
2739
+ {
2740
+ "title": "A full-width control",
2741
+ "description": "block fills the container and shares the width equally between options. Without it the control keeps its own width inside a stretching Stack.",
2742
+ "code": "<SegmentedControl\n label=\"Range\"\n block\n value={range}\n onChange={setRange}\n options={[\n { value: \"day\", label: \"Day\" },\n { value: \"week\", label: \"Week\" },\n { value: \"month\", label: \"Month\" },\n ]}\n/>"
2743
+ },
1501
2744
  {
1502
2745
  "title": "A disabled control",
1503
2746
  "description": "Disabled unregisters the tool, so an agent cannot select an option a person could not.",
@@ -1512,13 +2755,13 @@
1512
2755
  "End selects the last option",
1513
2756
  "Tab enters and leaves the group once"
1514
2757
  ],
1515
- "notes": "Roving tabindex: only the selected option is in the tab order. Selection follows focus, which is the expected behaviour for a radio group."
2758
+ "notes": "Roving tabindex: only the selected option is in the tab order. Selection follows focus, which is the expected behaviour for a radio group. An option with a count is named by its label and its count together; the hint is linked to the group with aria-describedby."
1516
2759
  }
1517
2760
  },
1518
2761
  {
1519
2762
  "name": "Select",
1520
2763
  "category": "input",
1521
- "summary": "A dropdown of mutually exclusive options behind a native select, carrying its own label, hint, and error. It registers a single select tool whose schema enumerates the option labels currently on offer.",
2764
+ "summary": "A dropdown of mutually exclusive options: a select-only combobox that opens a listbox on click or keyboard, carrying its own label, hint, and error. It registers a single select tool whose schema enumerates the option labels currently on offer.",
1522
2765
  "whenToUse": "Use it when one value is chosen from a list too long to lay out flat: a region, a squad, a category. Options are data ({ value, label }), the tool accepts the visible label, and in agent view every option renders as its own control, so an agent picks one without opening anything.",
1523
2766
  "whenNotToUse": "Do not use it for two to four short options a person should compare at a glance; that is a SegmentedControl. Do not use it for an on/off state, which is a Checkbox or a Switch, and never for navigation.",
1524
2767
  "status": "experimental",
@@ -1530,7 +2773,7 @@
1530
2773
  },
1531
2774
  "options": {
1532
2775
  "kind": "array",
1533
- "description": "The choices in display order: { value, label }. The label is what a person sees and what the select tool accepts, so an agent never has to know the value.",
2776
+ "description": "The choices in display order: { value, label, count? }. The label is what a person sees and what the select tool accepts, so an agent never has to know the value. count renders as a muted chip beside the label and reaches the agent view as part state, so never fold a count into the label.",
1534
2777
  "required": true
1535
2778
  },
1536
2779
  "value": {
@@ -1540,12 +2783,12 @@
1540
2783
  },
1541
2784
  "onChange": {
1542
2785
  "kind": "handler",
1543
- "description": "Called with the newly chosen value. The select tool drives a real change event, so this runs for agent selections too.",
2786
+ "description": "Called with the newly chosen value. The select tool clicks the real option, so this runs for agent selections too.",
1544
2787
  "required": true
1545
2788
  },
1546
2789
  "placeholder": {
1547
2790
  "kind": "string",
1548
- "description": "Shown while value is \"\". Rendered as a disabled option, so a person cannot choose it back."
2791
+ "description": "Shown in the closed control while value is \"\". It is not an option, so a person cannot choose it back."
1549
2792
  },
1550
2793
  "hint": {
1551
2794
  "kind": "string",
@@ -1557,7 +2800,7 @@
1557
2800
  },
1558
2801
  "name": {
1559
2802
  "kind": "string",
1560
- "description": "The native form name submitted with the surrounding form."
2803
+ "description": "The form name submitted with the surrounding form, through a hidden input carrying the value."
1561
2804
  },
1562
2805
  "disabled": {
1563
2806
  "kind": "boolean",
@@ -1599,6 +2842,10 @@
1599
2842
  "invalid": {
1600
2843
  "description": "Present while an error is set.",
1601
2844
  "attribute": "data-sprint-invalid"
2845
+ },
2846
+ "active": {
2847
+ "description": "On an option part, present while the list is open and that option is highlighted by the keyboard or pointer.",
2848
+ "attribute": "data-sprint-active"
1602
2849
  }
1603
2850
  },
1604
2851
  "tools": {
@@ -1624,7 +2871,7 @@
1624
2871
  }
1625
2872
  },
1626
2873
  "agentView": {
1627
- "example": "- **Select** \"Region\" [value=eu-1] → tool `select-region`\n - part `option` \"North Atlantic\"\n - part `option` \"Northern Europe\" [checked]\n - part `option` \"East Asia\""
2874
+ "example": "- **Select** \"Region\" [value=eu-1] → tool `select-region`\n - part `option` \"North Atlantic\" [count=12]\n - part `option` \"Northern Europe\" [checked, count=30]\n - part `option` \"East Asia\" [count=7]"
1628
2875
  },
1629
2876
  "examples": [
1630
2877
  {
@@ -1632,6 +2879,11 @@
1632
2879
  "description": "In agent view each option renders as its own control, so a DOM-driving agent chooses one directly.",
1633
2880
  "code": "<Select\n label=\"Region\"\n value={region}\n onChange={setRegion}\n placeholder=\"Choose a region\"\n options={[\n { value: \"na-1\", label: \"North Atlantic\" },\n { value: \"eu-1\", label: \"Northern Europe\" },\n { value: \"ap-1\", label: \"East Asia\" },\n ]}\n/>"
1634
2881
  },
2882
+ {
2883
+ "title": "Options with counts",
2884
+ "description": "A count is data, not label text: it renders as a chip in the list and the closed control, joins the accessible name, and reaches agents as part state while the select tool still takes the plain label.",
2885
+ "code": "<Select\n label=\"Region\"\n value={region}\n onChange={setRegion}\n placeholder=\"Choose a region\"\n options={[\n { value: \"na-1\", label: \"North Atlantic\", count: 12 },\n { value: \"eu-1\", label: \"Northern Europe\", count: 30 },\n { value: \"ap-1\", label: \"East Asia\", count: 7 },\n ]}\n/>"
2886
+ },
1635
2887
  {
1636
2888
  "title": "A required choice with an error",
1637
2889
  "description": "Empty plus required plus an error is how an unmade mandatory choice reads on every surface.",
@@ -1646,11 +2898,12 @@
1646
2898
  "a11y": {
1647
2899
  "role": "combobox",
1648
2900
  "keyboard": [
1649
- "Arrow keys move through the options",
1650
- "Enter or Space opens the list",
1651
- "Escape closes it"
2901
+ "Enter, Space, or an arrow key opens the list",
2902
+ "Arrow keys, Home, and End move through the options; typing a letter jumps to the next match",
2903
+ "Enter or Space chooses the highlighted option",
2904
+ "Escape or Tab closes the list without choosing"
1652
2905
  ],
1653
- "notes": "A native select element, so the platform owns the listbox interaction. The label is associated via htmlFor; errors set aria-invalid and link with aria-describedby."
2906
+ "notes": "A select-only combobox: a button with role combobox that opens a listbox on a plain click, so a synthetic element.click() opens it as reliably as a pointer does, and the list renders in the page rather than in browser chrome an automated session cannot see. Focus stays on the button and aria-activedescendant tracks the highlighted option. The label is linked with aria-labelledby; errors set aria-invalid and link with aria-describedby. An option with a count is named by its label and count together."
1654
2907
  }
1655
2908
  },
1656
2909
  {
@@ -1693,9 +2946,41 @@
1693
2946
  "kind": "string",
1694
2947
  "description": "Label of the drawer button while the drawer is open.",
1695
2948
  "default": "Close"
2949
+ },
2950
+ "collapsible": {
2951
+ "kind": "boolean",
2952
+ "description": "Lets a person hide the sidebar on wide viewports too. A second toggle appears beside the bar there, and while collapsed the page takes the narrow layout: the bar runs across the top and the sidebar is gone until it is shown again.",
2953
+ "default": false
2954
+ },
2955
+ "collapsed": {
2956
+ "kind": "boolean",
2957
+ "description": "Whether the sidebar is collapsed on wide viewports, when the owner keeps that state. Pair it with onCollapsedChange."
2958
+ },
2959
+ "defaultCollapsed": {
2960
+ "kind": "boolean",
2961
+ "description": "Whether a collapsible sidebar starts collapsed when the Shell keeps its own state.",
2962
+ "default": false
2963
+ },
2964
+ "onCollapsedChange": {
2965
+ "kind": "handler",
2966
+ "description": "Called with the collapsed state the Shell wants. Use it to remember the choice across visits."
2967
+ },
2968
+ "hideLabel": {
2969
+ "kind": "string",
2970
+ "description": "Label of the wide-viewport toggle while the sidebar is shown.",
2971
+ "default": "Hide menu"
2972
+ },
2973
+ "showLabel": {
2974
+ "kind": "string",
2975
+ "description": "Label of the wide-viewport toggle while the sidebar is collapsed.",
2976
+ "default": "Show menu"
1696
2977
  }
1697
2978
  },
1698
2979
  "state": {
2980
+ "collapsed": {
2981
+ "description": "Present while a collapsible sidebar is hidden on wide viewports. Narrow viewports ignore it and keep the drawer.",
2982
+ "attribute": "data-sprint-collapsed"
2983
+ },
1699
2984
  "open": {
1700
2985
  "description": "Present while the mobile drawer is open. On wide viewports the sidebar is always visible and this state is inert.",
1701
2986
  "attribute": "data-sprint-open"
@@ -1714,6 +2999,11 @@
1714
2999
  "title": "A sidebar app shell",
1715
3000
  "description": "One Shell per view. The sidebar collapses to a top bar with a drawer on narrow screens, and an agent reading the page sees the nav and the content with no frame in between.",
1716
3001
  "code": "<Shell\n bar={<Link href=\"#/\">ACME</Link>}\n side={\n <Nav label=\"Main\">\n <Link href=\"#/reports\" active>Reports</Link>\n <Link href=\"#/settings\">Settings</Link>\n </Nav>\n }\n>\n <Panel label=\"Reports\" headingLevel={2}>\n <Text>Quarterly numbers land here.</Text>\n </Panel>\n</Shell>"
3002
+ },
3003
+ {
3004
+ "title": "A sidebar that can be hidden",
3005
+ "description": "With collapsible, a person can put the sidebar away on a wide screen as well as a narrow one, and get the full width for the page.",
3006
+ "code": "<Shell\n collapsible\n bar={<Link href=\"#/\">ACME</Link>}\n side={\n <Nav label=\"Main\">\n <Link href=\"#/reports\" active>Reports</Link>\n <Link href=\"#/settings\">Settings</Link>\n </Nav>\n }\n>\n <Text>Quarterly numbers land here.</Text>\n</Shell>"
1717
3007
  }
1718
3008
  ]
1719
3009
  },
@@ -1783,8 +3073,20 @@
1783
3073
  "default": false
1784
3074
  },
1785
3075
  "min": {
1786
- "kind": "string",
1787
- "description": "Minimum track width for direction=\"grid\", as a CSS length. Tracks never exceed the container.",
3076
+ "kind": "enum",
3077
+ "description": "Minimum track width for direction=\"grid\", from a fixed scale of rem lengths. Tracks never exceed the container. Scale values are mapped in the stylesheet through data-sprint-min, so they work under a strict Content Security Policy. Any other CSS length is still accepted as an escape hatch, but it is written to an inline style attribute, which a style-src policy without unsafe-inline blocks: under such a policy an off-scale grid falls back to one column.",
3078
+ "values": [
3079
+ "10rem",
3080
+ "12rem",
3081
+ "14rem",
3082
+ "16rem",
3083
+ "18rem",
3084
+ "20rem",
3085
+ "22rem",
3086
+ "24rem",
3087
+ "28rem",
3088
+ "32rem"
3089
+ ],
1788
3090
  "default": "18rem"
1789
3091
  }
1790
3092
  },
@@ -1836,6 +3138,10 @@
1836
3138
  "collapse": {
1837
3139
  "description": "Present when the row stacks into a column on narrow viewports.",
1838
3140
  "attribute": "data-sprint-collapse"
3141
+ },
3142
+ "min": {
3143
+ "description": "The minimum track width in use, present only when direction=\"grid\". A value off the scale is carried here too, with the length itself in an inline style.",
3144
+ "attribute": "data-sprint-min"
1839
3145
  }
1840
3146
  },
1841
3147
  "examples": [
@@ -1855,6 +3161,91 @@
1855
3161
  }
1856
3162
  ]
1857
3163
  },
3164
+ {
3165
+ "name": "Steps",
3166
+ "category": "display",
3167
+ "summary": "An ordered set of numbered steps a person works through: each one a number badge, a short title and an optional body, with a rule between them. A step can be marked done or current, so the list doubles as a record of how far someone has got.",
3168
+ "whenToUse": "Use it when the order is the instruction: a handful of things to do one after another, each worth a title of its own, like the two things to send someone or the stages of a setup. Mark the step in progress as current and the finished ones as done when the page knows; leave every state off when the steps are simply instructions. Each step is an addressable part carrying its position and state, so an agent can say which step is next without counting lines.",
3169
+ "whenNotToUse": "Do not use it for a plain numbered list of short points with no titles or progress; that is List with ordered. Do not use it as a wizard that moves between screens: Steps only displays where someone is, it has no actions and registers no WebMCP tool, because there is nothing to press and an agent reads every step and its state from the agent view. Pair it with a Button when the page itself advances. Do not put components or links in a step; title and body are plain strings.",
3170
+ "status": "experimental",
3171
+ "props": {
3172
+ "label": {
3173
+ "kind": "string",
3174
+ "description": "What the steps achieve, as a short phrase like \"Send Tess two things\". Names the list for a screen reader and for the agent view.",
3175
+ "required": true
3176
+ },
3177
+ "steps": {
3178
+ "kind": "array",
3179
+ "description": "The steps in order, each { title: string; body?: string; state?: \"done\" | \"current\" }. Title is the instruction in a few words; body is an optional sentence of detail. A step with no state is upcoming. Mark at most one step current.",
3180
+ "required": true
3181
+ },
3182
+ "doneLabel": {
3183
+ "kind": "string",
3184
+ "description": "What a screen reader hears as the description of a finished step, since the done mark is drawn rather than written.",
3185
+ "default": "Done"
3186
+ },
3187
+ "emptyLabel": {
3188
+ "kind": "string",
3189
+ "description": "What the list says when it has no steps.",
3190
+ "default": "No steps"
3191
+ }
3192
+ },
3193
+ "state": {
3194
+ "steps": {
3195
+ "description": "How many steps there are.",
3196
+ "attribute": "data-sprint-steps"
3197
+ },
3198
+ "complete": {
3199
+ "description": "Present when every step is done.",
3200
+ "attribute": "data-sprint-complete"
3201
+ },
3202
+ "empty": {
3203
+ "description": "Present when there are no steps.",
3204
+ "attribute": "data-sprint-empty"
3205
+ },
3206
+ "index": {
3207
+ "description": "On a step: its 1-based position, which is also the number in its badge.",
3208
+ "attribute": "data-sprint-index"
3209
+ },
3210
+ "done": {
3211
+ "description": "On a step: present once the step is finished.",
3212
+ "attribute": "data-sprint-done"
3213
+ },
3214
+ "current": {
3215
+ "description": "On a step: present on the step in progress.",
3216
+ "attribute": "data-sprint-current"
3217
+ }
3218
+ },
3219
+ "agentView": {
3220
+ "example": "- **Steps** \"Send Tess two things\" [steps=2]\n - part `step` \"Copy the link\" [index=1]\n - part `step` \"Pass on the emoji\" [index=2]"
3221
+ },
3222
+ "a11y": {
3223
+ "role": "list",
3224
+ "notes": "A real ol named by its label, with an explicit list role because the drawn badges require list-style none and Safari would otherwise drop the list semantics, so the position of each step is announced. The badge number is aria-hidden for the same reason. The current step carries aria-current=\"step\", and a done step is described by doneLabel. Title and body are read as one item, separated by a colon that is hidden visually."
3225
+ },
3226
+ "relatedComponents": [
3227
+ "List",
3228
+ "Progress",
3229
+ "Panel"
3230
+ ],
3231
+ "examples": [
3232
+ {
3233
+ "title": "Send Tess two things",
3234
+ "description": "The plainest case: two instructions in order, with no progress to report.",
3235
+ "code": "<Steps\n label=\"Send Tess two things\"\n steps={[{ title: \"Copy the link\" }, { title: \"Pass on the emoji\" }]}\n/>"
3236
+ },
3237
+ {
3238
+ "title": "Partway through",
3239
+ "description": "A finished step, the one in progress, and one still to come, each with a line of detail.",
3240
+ "code": "<Steps\n label=\"Connect a tool\"\n steps={[\n {\n title: \"Register the tool\",\n body: \"Give it a name and an input schema.\",\n state: \"done\",\n },\n {\n title: \"Drive the DOM\",\n body: \"Click the real element rather than calling a prop.\",\n state: \"current\",\n },\n { title: \"Return the new state\", body: \"Read it back from the page.\" },\n ]}\n/>"
3241
+ },
3242
+ {
3243
+ "title": "Every step done",
3244
+ "description": "When all steps are done the list publishes that it is complete.",
3245
+ "code": "<Steps\n label=\"Send Tess two things\"\n steps={[\n { title: \"Copy the link\", state: \"done\" },\n { title: \"Pass on the emoji\", state: \"done\" },\n ]}\n/>"
3246
+ }
3247
+ ]
3248
+ },
1858
3249
  {
1859
3250
  "name": "Switch",
1860
3251
  "category": "input",
@@ -1964,7 +3355,7 @@
1964
3355
  },
1965
3356
  "columns": {
1966
3357
  "kind": "array",
1967
- "description": "Column definitions, in display order: { key, header, align?, width? }. The key addresses the cell in each row and appears on the cell as data-sprint-column.",
3358
+ "description": "Column definitions, in display order: { key, header, align?, width? }. The key addresses the cell in each row and appears on the cell as data-sprint-column. width is one of 4rem, 5rem, 6rem, 7rem, 8rem, 9rem, 10rem, 12rem, 14rem, 16rem, 20rem, or 24rem, mapped in the stylesheet through data-sprint-width on the header cell so it works under a strict Content Security Policy; any other CSS length is still accepted but is written to an inline style attribute, which a style-src policy without unsafe-inline blocks.",
1968
3359
  "required": true
1969
3360
  },
1970
3361
  "rows": {
@@ -1976,6 +3367,16 @@
1976
3367
  "kind": "string",
1977
3368
  "description": "What the table says when it has no rows.",
1978
3369
  "default": "No rows"
3370
+ },
3371
+ "loading": {
3372
+ "kind": "boolean",
3373
+ "description": "Set while the rows are being fetched. Sets aria-busy and sweeps a bar along the top edge. Existing rows stay visible; with none yet, the empty slot says loadingLabel instead of emptyLabel.",
3374
+ "default": false
3375
+ },
3376
+ "loadingLabel": {
3377
+ "kind": "string",
3378
+ "description": "What the empty slot says while loading.",
3379
+ "default": "Loading"
1979
3380
  }
1980
3381
  },
1981
3382
  "state": {
@@ -1991,6 +3392,10 @@
1991
3392
  "description": "Present when the table has no rows.",
1992
3393
  "attribute": "data-sprint-empty"
1993
3394
  },
3395
+ "loading": {
3396
+ "description": "Present while the rows are being fetched. Alongside empty it means nothing has arrived yet, not that there is nothing.",
3397
+ "attribute": "data-sprint-loading"
3398
+ },
1994
3399
  "column": {
1995
3400
  "description": "On a cell: which column it belongs to.",
1996
3401
  "attribute": "data-sprint-column"
@@ -1999,6 +3404,10 @@
1999
3404
  "description": "On a cell: which row it belongs to.",
2000
3405
  "attribute": "data-sprint-row"
2001
3406
  },
3407
+ "width": {
3408
+ "description": "On a column header: the width its column asked for, if any. A value off the scale is carried here too, with the length itself in an inline style.",
3409
+ "attribute": "data-sprint-width"
3410
+ },
2002
3411
  "align": {
2003
3412
  "description": "On a cell: the alignment its column asked for, if any.",
2004
3413
  "attribute": "data-sprint-align",
@@ -2021,6 +3430,11 @@
2021
3430
  "title": "A table with no rows",
2022
3431
  "description": "An empty table keeps its header and says so, rather than rendering a bare keyline.",
2023
3432
  "code": "<Table\n label=\"Registered tools\"\n emptyLabel=\"No tools registered\"\n columns={[{ key: \"name\", header: \"Name\" }]}\n rows={[]}\n/>"
3433
+ },
3434
+ {
3435
+ "title": "A table while fetching",
3436
+ "description": "Before the first rows arrive the table reads [empty, loading], so an agent waits rather than concluding there is nothing.",
3437
+ "code": "<Table\n label=\"Loadouts\"\n loading={isFetching}\n columns={[{ key: \"name\", header: \"Name\" }]}\n rows={loadouts ?? []}\n/>"
2024
3438
  }
2025
3439
  ],
2026
3440
  "a11y": {
@@ -2322,7 +3736,7 @@
2322
3736
  "category": "input",
2323
3737
  "summary": "A single-line text field carrying its own label, hint, and error. Fully controlled, and it registers one fill tool that replaces the field's text with an explicit value.",
2324
3738
  "whenToUse": "Use it for any free-form single-line value: a name, an email address, a search term. The label is part of the component, so a form never needs a separate label element, and the error prop is how validation reaches both a person and an agent.",
2325
- "whenNotToUse": "Do not use it for multi-line text, which is a Textarea. Do not use it to pick from a known set of values; that is a Select or a SegmentedControl. Do not use it for an on/off state, which is a Checkbox or a Switch.",
3739
+ "whenNotToUse": "Do not use it for multi-line text, which is a Textarea. Do not use it to pick from a known set of values; that is a Select or a SegmentedControl. Do not use it for an on/off state, which is a Checkbox or a Switch. Do not use a read-only TextInput to hand someone a value to paste elsewhere; that is a CopyField.",
2326
3740
  "status": "experimental",
2327
3741
  "props": {
2328
3742
  "label": {
@@ -2377,6 +3791,11 @@
2377
3791
  "description": "Disable the field and unregister its fill tool.",
2378
3792
  "default": false
2379
3793
  },
3794
+ "readOnly": {
3795
+ "kind": "boolean",
3796
+ "description": "Show the value without letting anyone change it. The field stays focusable and selectable, renders as plain text in the agent view, and registers no fill tool, because nothing can change it.",
3797
+ "default": false
3798
+ },
2380
3799
  "required": {
2381
3800
  "kind": "boolean",
2382
3801
  "description": "Mark the field required, visually and in the agent view.",
@@ -2409,6 +3828,10 @@
2409
3828
  "description": "Present when the field cannot be edited.",
2410
3829
  "attribute": "data-sprint-disabled"
2411
3830
  },
3831
+ "readonly": {
3832
+ "description": "Present when the field shows a value that cannot be changed. No fill tool is registered while it is set.",
3833
+ "attribute": "data-sprint-readonly"
3834
+ },
2412
3835
  "required": {
2413
3836
  "description": "Present when the field must be filled.",
2414
3837
  "attribute": "data-sprint-required"
@@ -2436,8 +3859,8 @@
2436
3859
  },
2437
3860
  "readOnly": false,
2438
3861
  "untrustedContent": true,
2439
- "registeredWhen": "The field is mounted, enabled, has a resolvable label, and no other component claims the same tool name.",
2440
- "unregisteredWhen": "The field unmounts or becomes disabled."
3862
+ "registeredWhen": "The field is mounted, enabled, editable rather than read-only, has a resolvable label, and no other component claims the same tool name.",
3863
+ "unregisteredWhen": "The field unmounts, becomes disabled, or becomes read-only."
2441
3864
  }
2442
3865
  },
2443
3866
  "agentView": {
@@ -2458,6 +3881,11 @@
2458
3881
  "title": "A password",
2459
3882
  "description": "The value stays off every agent surface: state reflects filled or empty, and tool results never echo the text.",
2460
3883
  "code": "<TextInput\n label=\"Access code\"\n type=\"password\"\n value={code}\n onChange={setCode}\n autoComplete=\"current-password\"\n/>"
3884
+ },
3885
+ {
3886
+ "title": "A read-only value",
3887
+ "description": "A value shown in the shape of a form field that nobody may edit. It stays focusable and selectable, reads as text in the agent view, and registers no fill tool. To hand someone a value to paste elsewhere, a CopyField adds the copy control.",
3888
+ "code": "<TextInput\n label=\"Station ID\"\n value=\"KX-2209-ALPHA\"\n onChange={() => {}}\n readOnly\n hint=\"Assigned at registration\"\n/>"
2461
3889
  }
2462
3890
  ],
2463
3891
  "a11y": {