@pithyjs/pidl-catalog 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/dist/specs/atoms/actions/button.pidl.json +289 -0
  3. package/dist/specs/atoms/actions/icon-group.pidl.json +141 -0
  4. package/dist/specs/atoms/actions/segmented-control.pidl.json +137 -0
  5. package/dist/specs/atoms/content/action-tile.pidl.json +135 -0
  6. package/dist/specs/atoms/content/badge.pidl.json +189 -0
  7. package/dist/specs/atoms/content/callout.pidl.json +170 -0
  8. package/dist/specs/atoms/content/code-block.pidl.json +170 -0
  9. package/dist/specs/atoms/content/heading.pidl.json +175 -0
  10. package/dist/specs/atoms/content/icon.pidl.json +127 -0
  11. package/dist/specs/atoms/content/image.pidl.json +227 -0
  12. package/dist/specs/atoms/content/prose.pidl.json +183 -0
  13. package/dist/specs/atoms/content/stat.pidl.json +158 -0
  14. package/dist/specs/atoms/content/tabs.pidl.json +207 -0
  15. package/dist/specs/atoms/content/text.pidl.json +212 -0
  16. package/dist/specs/atoms/content/tile.pidl.json +156 -0
  17. package/dist/specs/atoms/data/donut.pidl.json +352 -0
  18. package/dist/specs/atoms/data/gauge.pidl.json +274 -0
  19. package/dist/specs/atoms/forms/checkbox-group.pidl.json +170 -0
  20. package/dist/specs/atoms/forms/checkbox.pidl.json +323 -0
  21. package/dist/specs/atoms/forms/file-picker.pidl.json +161 -0
  22. package/dist/specs/atoms/forms/form-field.pidl.json +106 -0
  23. package/dist/specs/atoms/forms/input.pidl.json +466 -0
  24. package/dist/specs/atoms/forms/radio-group.pidl.json +174 -0
  25. package/dist/specs/atoms/forms/radio.pidl.json +312 -0
  26. package/dist/specs/atoms/forms/search-input.pidl.json +265 -0
  27. package/dist/specs/atoms/forms/select.pidl.json +420 -0
  28. package/dist/specs/atoms/forms/textarea.pidl.json +422 -0
  29. package/dist/specs/atoms/forms/toggle.pidl.json +314 -0
  30. package/dist/specs/layout/grid-item.pidl.json +120 -0
  31. package/dist/specs/layout/grid.pidl.json +157 -0
  32. package/dist/specs/layout/page-section.pidl.json +105 -0
  33. package/dist/specs/layout/smart-container.pidl.json +148 -0
  34. package/dist/specs/layout/stack.pidl.json +144 -0
  35. package/dist/specs/organisms/content/accordion.pidl.json +196 -0
  36. package/dist/specs/organisms/content/card.pidl.json +283 -0
  37. package/dist/specs/organisms/feedback/alert-dialog.pidl.json +158 -0
  38. package/dist/specs/organisms/feedback/modal.pidl.json +133 -0
  39. package/dist/specs/organisms/feedback/toast.pidl.json +150 -0
  40. package/dist/specs/organisms/forms/dynamic-form.pidl.json +314 -0
  41. package/dist/specs/organisms/forms/form.pidl.json +135 -0
  42. package/dist/specs/organisms/forms/subscribe-form.pidl.json +188 -0
  43. package/dist/specs/organisms/navigation/command-palette.pidl.json +263 -0
  44. package/dist/specs/organisms/navigation/docs-sidebar.pidl.json +264 -0
  45. package/dist/specs/organisms/navigation/navbar.pidl.json +356 -0
  46. package/dist/specs/organisms/navigation/table-of-contents.pidl.json +197 -0
  47. package/dist/specs/sections/contact-section.pidl.json +183 -0
  48. package/dist/specs/sections/cta-section.pidl.json +168 -0
  49. package/dist/specs/sections/faq-section.pidl.json +202 -0
  50. package/dist/specs/sections/features-section.pidl.json +214 -0
  51. package/dist/specs/sections/footer-section.pidl.json +187 -0
  52. package/dist/specs/sections/hero-section.pidl.json +202 -0
  53. package/dist/src/borders-psychology.d.ts +17 -0
  54. package/dist/src/borders-psychology.d.ts.map +1 -0
  55. package/dist/src/borders-psychology.js +42 -0
  56. package/dist/src/borders-psychology.js.map +1 -0
  57. package/dist/src/color-psychology.d.ts +20 -0
  58. package/dist/src/color-psychology.d.ts.map +1 -0
  59. package/dist/src/color-psychology.js +57 -0
  60. package/dist/src/color-psychology.js.map +1 -0
  61. package/dist/src/design-intelligence.d.ts +13 -0
  62. package/dist/src/design-intelligence.d.ts.map +1 -0
  63. package/dist/src/design-intelligence.js +36 -0
  64. package/dist/src/design-intelligence.js.map +1 -0
  65. package/dist/src/index.d.ts +6238 -0
  66. package/dist/src/index.d.ts.map +1 -0
  67. package/dist/src/index.js +105 -0
  68. package/dist/src/index.js.map +1 -0
  69. package/dist/src/motion-psychology.d.ts +19 -0
  70. package/dist/src/motion-psychology.d.ts.map +1 -0
  71. package/dist/src/motion-psychology.js +47 -0
  72. package/dist/src/motion-psychology.js.map +1 -0
  73. package/dist/src/mutation-vocabulary.d.ts +13 -0
  74. package/dist/src/mutation-vocabulary.d.ts.map +1 -0
  75. package/dist/src/mutation-vocabulary.js +67 -0
  76. package/dist/src/mutation-vocabulary.js.map +1 -0
  77. package/dist/src/radius-psychology.d.ts +17 -0
  78. package/dist/src/radius-psychology.d.ts.map +1 -0
  79. package/dist/src/radius-psychology.js +42 -0
  80. package/dist/src/radius-psychology.js.map +1 -0
  81. package/dist/src/shadows-psychology.d.ts +17 -0
  82. package/dist/src/shadows-psychology.d.ts.map +1 -0
  83. package/dist/src/shadows-psychology.js +44 -0
  84. package/dist/src/shadows-psychology.js.map +1 -0
  85. package/dist/src/spacing-psychology.d.ts +22 -0
  86. package/dist/src/spacing-psychology.d.ts.map +1 -0
  87. package/dist/src/spacing-psychology.js +59 -0
  88. package/dist/src/spacing-psychology.js.map +1 -0
  89. package/dist/src/typography-psychology.d.ts +22 -0
  90. package/dist/src/typography-psychology.d.ts.map +1 -0
  91. package/dist/src/typography-psychology.js +68 -0
  92. package/dist/src/typography-psychology.js.map +1 -0
  93. package/package.json +64 -0
@@ -0,0 +1,264 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.docs-sidebar",
4
+ "name": "DocsSidebar",
5
+ "displayName": "Docs sidebar",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "navigation",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Nested docs navigation tree. Renders a <nav> landmark around real nested <ul>/<li> lists of <a href> links, with native <button aria-expanded> disclosures for groups. The link matching the router's path carries aria-current=\"page\"; the ancestor chain of that link is force-expanded on mount and on every route change. Collapse/expand state persists to localStorage under a caller-supplied key, degrading silently when storage is unavailable. NO search or filtering — that is <CommandPalette>'s job. studioReady is FALSE on purpose: the tree is a data prop and the active state comes from the live router, so there is nothing meaningful to author or preview on a canvas.",
12
+ "props": [
13
+ {
14
+ "name": "items",
15
+ "label": "Navigation tree",
16
+ "editor": {
17
+ "tab": "content",
18
+ "type": "array"
19
+ },
20
+ "type": "DocsSidebarItem[]",
21
+ "default": [],
22
+ "itemSchema": {
23
+ "label": {
24
+ "type": "string",
25
+ "required": true,
26
+ "description": "Visible label. Also the fallback persistence key for a group — see `id`. An entry with a blank label is dropped."
27
+ },
28
+ "href": {
29
+ "type": "string",
30
+ "description": "Destination path. Matched against the router's path with EXACT string equality, so it must be written the way the router reports it (no trailing slash). Values rejected by isSafeHref are dropped with a DEV warning. Omit for a pure grouping heading."
31
+ },
32
+ "items": {
33
+ "type": "DocsSidebarItem[]",
34
+ "description": "Child entries. Presence is what makes an entry a group. An entry with BOTH href and items is a section landing page: it renders a link and a separate disclosure button as siblings."
35
+ },
36
+ "id": {
37
+ "type": "string",
38
+ "description": "Stable persistence key for this group's expanded state. Defaults to the '/'-joined chain of ancestor labels plus this entry's own label. Set it to keep a reader's collapsed state across a label rename, or to disambiguate two groups that share a label path."
39
+ },
40
+ "defaultOpen": {
41
+ "type": "boolean",
42
+ "default": true,
43
+ "description": "Expanded on first visit, before anything is persisted. Defaults to true: a docs sidebar that opens fully collapsed hides the navigation it exists to provide."
44
+ },
45
+ "prefetch": {
46
+ "type": "'none' | 'hover' | 'visible' | 'eager'",
47
+ "default": "none",
48
+ "description": "Prefetch strategy for this link. Requires a mounted router; ignored otherwise."
49
+ }
50
+ },
51
+ "description": "The navigation tree, depth-unbounded. Entries are filtered recursively before any DOM is built: a labelless entry, an entry with neither a usable href nor children, and a group whose whole subtree is unrenderable (items: [], or nothing but rejected entries) are all dropped — so the component never ships a disclosure button that expands to an empty list."
52
+ },
53
+ {
54
+ "name": "label",
55
+ "label": "Landmark name",
56
+ "type": "string",
57
+ "default": "Docs",
58
+ "placeholder": "Docs",
59
+ "description": "Accessible name for the <nav> landmark. Always applied, never optional: a docs page typically holds several <nav> elements — site header, this sidebar, the table of contents — and the name is what lets a screen-reader user tell them apart in a landmark list. A whitespace-only value falls back to the default rather than producing an empty accessible name."
60
+ },
61
+ {
62
+ "name": "title",
63
+ "label": "Title",
64
+ "editor": {
65
+ "tab": "content"
66
+ },
67
+ "type": "string",
68
+ "required": false,
69
+ "description": "Optional visible title above the tree, rendered as a real <Heading> so it joins the page's heading outline. Omit it in a layout that already labels the sidebar column."
70
+ },
71
+ {
72
+ "name": "titleLevel",
73
+ "label": "Title level",
74
+ "editor": {
75
+ "tab": "settings"
76
+ },
77
+ "type": "number",
78
+ "default": 2,
79
+ "description": "Heading level for `title`, 1-6. Pick the level that fits the page's real outline — the sidebar sits beside an article, so it is usually a peer of the article's own h2."
80
+ },
81
+ {
82
+ "name": "storageKey",
83
+ "label": "Storage key",
84
+ "editor": {
85
+ "tab": "settings"
86
+ },
87
+ "type": "string | null",
88
+ "default": "pithy:docs-sidebar",
89
+ "description": "localStorage key for the collapse/expand state. Pass null to disable persistence entirely — nothing is read and nothing is written. Two sidebars on one page MUST be given distinct keys or they overwrite each other."
90
+ },
91
+ {
92
+ "name": "class",
93
+ "label": "Extra classes",
94
+ "editor": {
95
+ "tab": "settings"
96
+ },
97
+ "type": "string",
98
+ "required": false,
99
+ "description": "Extra class names appended after 'ui-docs-sidebar'."
100
+ },
101
+ {
102
+ "name": "id",
103
+ "label": "Element id",
104
+ "editor": {
105
+ "tab": "settings"
106
+ },
107
+ "type": "string",
108
+ "required": false,
109
+ "description": "id for the <nav> element — useful as a skip-link target."
110
+ }
111
+ ],
112
+ "slots": [],
113
+ "tokens": {
114
+ "typography": "--font-size-sm + --line-height-body (tree text), --font-weight-semibold (current link, current group), --font-weight-medium (group labels, via <Text>)",
115
+ "color": "--color-text-muted (rest), --color-text (hover, current group), --color-primary via --_docs-sidebar-accent (current link text, current rail, hover/selected tint), --color-divider via --_docs-sidebar-rail (sublist guide rail)",
116
+ "spacing": "--space-xs (entry gap, link/toggle block padding, chevron gap, sublist offset), --space-sm (link/toggle inline padding, sublist indent, title margin)",
117
+ "border": "--border-width-thin + --border-style-solid (sublist guide rail), --border-width-medium (current-link accent rail)",
118
+ "radius": "--radius-sm (link and toggle boxes), --radius-pill (current-link accent rail)",
119
+ "control": "--control-min-touch (min block size on every link and toggle, and min inline size on the icon-only toggle) — WCAG 2.5.5",
120
+ "focus": "--focus-ring-width / --focus-ring-offset / --ring-color via the a11y focus-ring mixin on :focus-visible",
121
+ "state": "--state-hover-opacity and --state-selected-opacity via the a11y state-background mixin",
122
+ "motion": "--motion-transition-color / --motion-transition-background (links, toggles) and --motion-transition-transform (chevron), all inside prefers-reduced-motion: no-preference"
123
+ },
124
+ "spacing": {
125
+ "entry-gap": "--space-xs (Lovers) — sibling entries read as one continuous track, not as separate cards",
126
+ "group-to-children": "--space-xs block-start on the sublist (Lovers) — a group and its own contents are inseparable",
127
+ "sublist-indent": "--space-sm inline (Close Friends) — a child belongs to the group above it; depth reads from the indent plus the guide rail, never from a size or colour change",
128
+ "label-to-chevron": "--space-xs (Lovers) — a control and its own affordance",
129
+ "title-to-tree": "--space-sm block-end (Close Friends) — a title and the list it names",
130
+ "control-padding": "--space-xs block / --space-sm inline, on top of a --control-min-touch floor — enough hit area at every container width without turning the tree into a button stack"
131
+ },
132
+ "constraints": [
133
+ "The tree comes from the `items` prop — the component takes NO children and declares NO slot",
134
+ "SEARCH AND FILTERING ARE OUT OF SCOPE. A docs sidebar is a fixed map of the documentation; searching it is <CommandPalette>'s job, and putting a field here would duplicate that component's index, filter and combobox semantics",
135
+ "href is matched with EXACT string equality against the router path — the same predicate routerLink's exactMatch uses, character for character. Normalising on one side only would let the two disagree, and the disagreement would surface as an expanded group with no aria-current anywhere",
136
+ "A group may be a link AND a group. It renders an <a> and a <button> as siblings inside one row, because a single control cannot both navigate and toggle: a <button> cannot be followed to a URL, and an <a> carrying aria-expanded would claim to expand something while navigating away",
137
+ "Collapsed subtrees are `hidden`, which resolves to display:none — their links leave the tab order entirely rather than becoming invisible tab stops. `.ui-docs-sidebar-list` sets display:flex and an author display BEATS the UA [hidden] rule, so the stylesheet restates `&[hidden] { display: none }` explicitly; without it every collapsed group would stay visible and tabbable",
138
+ "Expansion precedence on mount: the active route's ancestor chain (forced) > persisted state > item.defaultOpen > true",
139
+ "The force-expand is NEVER written back to storage — it is a view decision for the current route, and persisting it would silently erase the reader's own collapse. Only a click on a disclosure button writes",
140
+ "Force-expand re-runs only when the ACTIVE ENTRY CHANGES, so collapsing the group you are currently inside stays collapsed until you navigate elsewhere",
141
+ "localStorage is guarded three ways: an SSR check, a try/catch around the ACCESS itself (some embedders and Safari-with-cookies-blocked throw on merely touching window.localStorage, so wrapping only getItem is not enough), and per-key boolean filtering so a corrupt or version-skewed blob contributes its valid entries instead of poisoning the map. Every failure degrades to defaults; none throws",
142
+ "Writes are best-effort — setItem throws on quota exhaustion and in Safari private mode, and neither is worth failing a click over",
143
+ "storageKey defaults to a single shared key, so two sidebars on one page MUST be given distinct keys (or null)",
144
+ "Group persistence keys default to the '/'-joined ancestor label chain; a duplicate key DEV-warns and the two groups share one persisted state until one is given an explicit id",
145
+ "With a mounted router, aria-current is owned by the routerLink directive; with no router the component sets it from window.location.pathname and re-syncs on popstate. Exactly one of the two ever writes it",
146
+ "Every anchor is tagged data-router-link / data-prefetch / exact-match unconditionally, so a router that mounts AFTER the sidebar is built still binds the links via setGlobalRouter → bindExistingRouterLinks — and binds them with the same exact-match semantics",
147
+ "destroy() removes every toggle listener, unsubscribes from the router, cleans up router directives on every anchor, and empties the nav; a second call is a no-op"
148
+ ],
149
+ "a11y": [
150
+ "Implements the WAI-ARIA APG *Disclosure Navigation* pattern: native <a href> links and native <button aria-expanded> disclosures, every one of them in the natural tab order",
151
+ "NOT the APG treeview pattern, and that is the non-obvious call: role=\"treeitem\" on an <a href> OVERRIDES the implicit link role, so every entry would drop out of the screen-reader links rotor and stop being announced as a link. APG's own guidance is that site navigation built from real links uses disclosure navigation; treeview is for tree widgets such as file explorers",
152
+ "NOT the aria-activedescendant model <CommandPalette> uses: that pattern requires a single focus-holding widget (a combobox input) that keeps DOM focus while a virtual cursor roves, and a docs sidebar has no such input — focus lives on the links themselves",
153
+ "NO roving tabindex, and therefore no use of createRovingTabindex: roving tabindex means a single tab stop, which is the opposite of this pattern. Every visible link and toggle is its own tab stop, in DOM order",
154
+ "NO keydown handler of any kind. Enter and Space activate links and buttons natively; adding a handler would be a third keyboard state machine in a codebase that already has two",
155
+ "Arrow-key movement is OPTIONAL in APG's disclosure navigation pattern and is a deliberate v1 omission, not an oversight — see the keyboard notes above for why",
156
+ "Renders a <nav> landmark with an always-present accessible name (default 'Docs')",
157
+ "Entries are REAL <ul>/<li> lists with a nested <ul> per group, and EVERY <ul> carries an explicit role=\"list\" — Safari/VoiceOver strips list semantics from a list-style:none / display:flex list, losing the 'list, N items' announcement that is the reason this is a list at all. axe cannot detect this",
158
+ "A collapsed group's <ul> is `hidden`, so its links leave both the accessibility tree and the tab order — a keyboard user never tabs into content they cannot see",
159
+ "Each disclosure button sets aria-expanded and aria-controls pointing at the id of the <ul> it owns",
160
+ "A pure group's button takes its accessible name from its own visible label text. The icon-only button beside a section landing link has no text, so it carries an explicit aria-label of the form '<Label> submenu'",
161
+ "The chevron is a decorative <Icon> (aria-hidden), so it never joins a button's accessible name",
162
+ "aria-current=\"page\" (NOT \"location\") marks the current entry — these links leave the current document, unlike <TableOfContents>'s in-page anchors",
163
+ "At most one link carries aria-current at a time",
164
+ "The current entry is never conveyed by colour alone (WCAG 1.4.1): it gains --font-weight-semibold AND a selected-state tint AND an accent rail. The group containing it gains weight as well, and that marker is supplementary — aria-current on the link is the authoritative state",
165
+ "Links and toggles meet the WCAG 2.5.5 44px floor via --control-min-touch at every container width, not only where a label happens to wrap",
166
+ "CSS transitions are wrapped in prefers-reduced-motion: no-preference via the a11y motion-safe mixin",
167
+ "The chevron rotates a half turn (180deg), which has no horizontal component and so needs no RTL mirroring",
168
+ "Forced colours redraws the guide rail (GrayText) and the current-entry rail and text (Highlight), and restores the focus outline — a11y.focus-ring draws its indicator as a box-shadow over `outline: none`, and forced-colors discards box-shadow without reinstating the suppressed outline (WCAG 2.4.7)",
169
+ "The empty state is <nav hidden data-empty=\"true\">, which removes a nameless, link-less landmark from the accessibility tree instead of leaving a dead focus stop"
170
+ ],
171
+ "ai": {
172
+ "synonyms": [
173
+ "docs sidebar",
174
+ "documentation nav",
175
+ "docs navigation",
176
+ "sidebar tree",
177
+ "nested nav",
178
+ "collapsible navigation",
179
+ "section navigation",
180
+ "left rail",
181
+ "docs menu",
182
+ "disclosure navigation"
183
+ ],
184
+ "avoid": [
185
+ "adding a search or filter field to this component — search is <CommandPalette>, and duplicating it here would mean a second index, a second filter and a second set of combobox semantics",
186
+ "using <TableOfContents> for this job — it links only WITHIN the current document; this component links BETWEEN pages",
187
+ "reaching for role=\"tree\" / role=\"treeitem\" because the markup is a tree — treeitem on an <a href> destroys the link role and the screen-reader links rotor; APG's guidance for link-based site navigation is disclosure navigation",
188
+ "adding arrow-key navigation 'for completeness' — it is optional in the pattern, and this component deliberately adds no keyboard state machine",
189
+ "writing href values with a trailing slash — matching is exact string equality against the router path",
190
+ "mounting two sidebars with the default storageKey — they will overwrite each other's collapse state; give each its own key or pass null"
191
+ ],
192
+ "patterns": [
193
+ "Flat list: DocsSidebar({ items: [{ label: 'Intro', href: '/docs' }, { label: 'API', href: '/docs/api' }] })",
194
+ "Grouped: a group entry with `items` and no `href` renders a disclosure button whose own text is its accessible name",
195
+ "Section landing page: a group entry with BOTH `href` and `items` renders a link plus a separate icon-only disclosure button",
196
+ "Titled: DocsSidebar({ title: 'Guides', titleLevel: 2, items })",
197
+ "No persistence: DocsSidebar({ items, storageKey: null })",
198
+ "Two on a page: give each a distinct storageKey, e.g. 'docs:guides' and 'docs:api'"
199
+ ]
200
+ },
201
+ "designGuidance": {
202
+ "surface": "Transparent — the sidebar sits on whatever surface the docs layout gives it and adds no fill of its own. The only painted areas are the hover tint and the current entry's selected-state tint, both --color-primary washes at design-system state opacities. Text is --color-text-muted at rest so the tree recedes behind the article it navigates, stepping up to --color-text on hover and on the group containing the current page.",
203
+ "depth": "Flat. No shadow, no elevation. A docs sidebar is a map beside the content, not a panel floating over it.",
204
+ "borders": "One border, used as a guide rail: a --border-width-thin inline-start edge on NESTED lists only. The root list has none — a spine down the whole component would imply the top level is itself some group's contents. The current entry's accent rail is a separate --border-width-medium overlay pulled out over that hairline, so activation replaces a colour rather than changing a box size and nothing reflows as the reader navigates.",
205
+ "interactivity": "Two control types and nothing else: links navigate, disclosure buttons expand. Hover raises the text colour and adds the primary tint on both; :focus-visible surfaces the design-system focus ring on both. The current entry is the only persistent state, tripled up (weight + tint + rail) so it never depends on colour.",
206
+ "motion": "Colour, background and a 180deg chevron rotation, all inside prefers-reduced-motion: no-preference. Expansion itself is NOT animated — a height transition on a list of unknown depth either needs a measured max-height (which clips) or `interpolate-size` (not yet broadly available), and a half-animated disclosure reads worse than an instant one.",
207
+ "darkMode": "Nothing to do: every value is a --color-* token, so the muted/rest/current relationship inverts with the theme. The forced-colours block is the separate case — there the rails and the current entry are redrawn with GrayText/Highlight because author colours are discarded outright.",
208
+ "bestPractices": "Reach for it whenever documentation has more than one page. Keep the tree two or three levels deep — a fourth level is a sign the information architecture, not the component, needs work. Give groups explicit `id`s if the labels are likely to be rewritten, so readers keep their collapsed sections across a docs refresh. Leave `defaultOpen` alone unless a group is genuinely reference material the reader rarely wants; opening collapsed hides the navigation the component exists to provide. Pair it with <TableOfContents> on the other side of the article: this one answers 'which page am I on', that one answers 'where in the page am I'. If you want search, add <CommandPalette> to the page header — do not add a field here."
209
+ },
210
+ "examples": [
211
+ {
212
+ "id": "default",
213
+ "title": "Flat tree",
214
+ "pithy": "DocsSidebar({ items: [{ label: 'Introduction', href: '/docs' }] })",
215
+ "assertions": [
216
+ "renders <nav class='ui-docs-sidebar' aria-label='Docs'>",
217
+ "the root <ul> carries an explicit role='list'",
218
+ "each entry is an <a href> inside an <li>"
219
+ ]
220
+ },
221
+ {
222
+ "id": "groups",
223
+ "title": "Nested groups",
224
+ "pithy": "DocsSidebar({ items: [{ label: 'Guides', items: [{ label: 'Install', href: '/docs/install' }] }] })",
225
+ "assertions": [
226
+ "the group renders a <button type='button' aria-expanded aria-controls>",
227
+ "aria-controls resolves to the id of the group's own nested <ul>",
228
+ "the nested <ul> also carries role='list'",
229
+ "a collapsed group's <ul> is hidden, so its links are not tabbable"
230
+ ]
231
+ },
232
+ {
233
+ "id": "landing-page",
234
+ "title": "Group that is also a link",
235
+ "pithy": "DocsSidebar({ items: [{ label: 'Guides', href: '/docs/guides', items: [{ label: 'Install', href: '/docs/guides/install' }] }] })",
236
+ "assertions": [
237
+ "renders an <a> and a <button> as siblings inside one row",
238
+ "the icon-only button carries aria-label='Guides submenu'",
239
+ "the link is navigable and the button toggles, independently"
240
+ ]
241
+ },
242
+ {
243
+ "id": "active-route",
244
+ "title": "Active route and force-expand",
245
+ "pithy": "DocsSidebar({ items })",
246
+ "assertions": [
247
+ "the link whose href equals the router path gains aria-current='page'",
248
+ "at most one link carries aria-current at a time",
249
+ "every ancestor group of that link is expanded on mount, even when the persisted state says collapsed",
250
+ "the force-expand is not written back to storage"
251
+ ]
252
+ },
253
+ {
254
+ "id": "persistence",
255
+ "title": "Persisted collapse",
256
+ "pithy": "DocsSidebar({ items, storageKey: 'docs:guides' })",
257
+ "assertions": [
258
+ "clicking a disclosure writes { key: boolean } to localStorage under the given key",
259
+ "a corrupt or non-object payload degrades to defaults instead of throwing",
260
+ "storageKey: null reads and writes nothing"
261
+ ]
262
+ }
263
+ ]
264
+ }