@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PithyJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,289 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.button",
4
+ "name": "Button",
5
+ "displayName": "Button",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "actions",
9
+ "bidi": true,
10
+ "description": "Accessible, reactive button with variants, color schemes, loading state, and anchor mode. Keyboard and ARIA correct by default. Uses aria-disabled to keep button focusable for assistive tech.",
11
+ "props": [
12
+ {
13
+ "name": "text",
14
+ "reactive": true,
15
+ "label": "Label",
16
+ "type": "string | Signal<string>",
17
+ "placeholder": "Click me",
18
+ "description": "Visible button label. Rendered as a text node inside the button. When omitted, the component falls back to slot content (`<Button>Click me</Button>` in templates)."
19
+ },
20
+ {
21
+ "name": "iconStart",
22
+ "reactive": true,
23
+ "label": "Leading icon",
24
+ "type": "icon",
25
+ "description": "Icon shown before the label. Picked from the registered sprite (see @pithyjs/vite-plugin/icons). Pair with `ariaLabel` for icon-only buttons."
26
+ },
27
+ {
28
+ "name": "iconEnd",
29
+ "reactive": true,
30
+ "label": "Trailing icon",
31
+ "type": "icon",
32
+ "description": "Icon shown after the label."
33
+ },
34
+ {
35
+ "name": "variant",
36
+ "label": "Variant",
37
+ "type": "enum",
38
+ "enum": ["solid", "outline", "ghost", "link"],
39
+ "default": "solid",
40
+ "description": "Visual style — solid (filled), outline (bordered), ghost (no background), link (text-only underlined)"
41
+ },
42
+ {
43
+ "name": "color",
44
+ "label": "Color",
45
+ "editor": { "type": "token-color" },
46
+ "type": "enum",
47
+ "enum": [
48
+ "primary",
49
+ "secondary",
50
+ "tertiary",
51
+ "success",
52
+ "info",
53
+ "warning",
54
+ "danger"
55
+ ],
56
+ "default": "primary",
57
+ "description": "Semantic color scheme — maps to design-system --color-* tokens."
58
+ },
59
+ {
60
+ "name": "size",
61
+ "label": "Size",
62
+ "type": "enum",
63
+ "enum": ["sm", "md", "lg"],
64
+ "default": "md",
65
+ "description": "Control size — maps to u-control-* utilities (padding, min-height, gap)"
66
+ },
67
+ {
68
+ "name": "shape",
69
+ "label": "Shape",
70
+ "type": "enum",
71
+ "enum": ["none", "xs", "sm", "md", "lg", "xl"],
72
+ "default": "md",
73
+ "description": "Border-radius shape — maps to design-system --radius-* tokens via data-shape attribute. 'xl' reads as pill, 'none' as square."
74
+ },
75
+ {
76
+ "name": "disabled",
77
+ "label": "Disabled",
78
+ "editor": { "tab": "settings" },
79
+ "type": "boolean | Signal<boolean>",
80
+ "default": false,
81
+ "description": "Disabled state — accepts static boolean or signal accessor. Uses aria-disabled (not native disabled) to keep element focusable."
82
+ },
83
+ {
84
+ "name": "loading",
85
+ "label": "Loading",
86
+ "editor": { "tab": "settings" },
87
+ "type": "boolean | Signal<boolean>",
88
+ "default": false,
89
+ "description": "Loading state — shows spinner, sets aria-busy, announces loading text to screen readers"
90
+ },
91
+ {
92
+ "name": "type",
93
+ "label": "Type",
94
+ "editor": { "tab": "settings" },
95
+ "type": "enum",
96
+ "enum": ["button", "submit", "reset"],
97
+ "default": "button",
98
+ "description": "HTML button type attribute — only used when as=\"button\""
99
+ },
100
+ {
101
+ "name": "loadingText",
102
+ "label": "Loading text",
103
+ "editor": { "tab": "settings" },
104
+ "type": "string",
105
+ "default": "Loading\u2026",
106
+ "description": "Screen-reader announcement during loading state"
107
+ },
108
+ {
109
+ "name": "fullWidth",
110
+ "label": "Full width",
111
+ "editor": { "tab": "layout" },
112
+ "type": "boolean",
113
+ "default": false,
114
+ "description": "Stretch to 100% container width"
115
+ },
116
+ {
117
+ "name": "as",
118
+ "label": "Element",
119
+ "editor": { "tab": "settings" },
120
+ "type": "enum",
121
+ "enum": ["button", "a"],
122
+ "default": "button",
123
+ "description": "Render as <a> for anchor semantics — with href keeps native link, without href becomes role=button"
124
+ },
125
+ {
126
+ "name": "href",
127
+ "label": "Link URL",
128
+ "type": "string",
129
+ "description": "Link URL — only used when as=\"a\". Preserves native link semantics (no role override)."
130
+ },
131
+ {
132
+ "name": "onClick",
133
+ "type": "(e: MouseEvent) => void",
134
+ "description": "Click handler — guarded against disabled and loading states"
135
+ },
136
+ {
137
+ "name": "ariaLabel",
138
+ "reactive": true,
139
+ "label": "ARIA label",
140
+ "editor": { "tab": "settings" },
141
+ "type": "string | Signal<string>",
142
+ "description": "Accessible label — forwarded as aria-label on the root element. Static string or signal accessor. Use when the button has no visible text (e.g. icon-only buttons)."
143
+ }
144
+ ],
145
+ "events": ["click"],
146
+ "slots": [
147
+ {
148
+ "name": "default",
149
+ "displayName": "Content",
150
+ "description": "Rich label content (icon, text). Interactive or container blocks are not allowed inside a button.",
151
+ "accepts": ["ui.icon", "ui.text"]
152
+ }
153
+ ],
154
+ "tokens": {
155
+ "solid.bg": "--color-{color}",
156
+ "solid.text": "--color-on-{color}",
157
+ "solid.bg-hover": "--color-{color}-hover",
158
+ "solid.bg-active": "--color-{color}-active",
159
+ "outline.border": "--border-width-hairline",
160
+ "outline.hover": "a11y.state-background(hover)",
161
+ "outline.active": "a11y.state-background(pressed)",
162
+ "ghost.hover": "color-mix(--_btn-color, --state-hover-opacity * 200%)",
163
+ "ghost.active": "a11y.state-background(pressed)",
164
+ "link.text-hover": "--color-{color}-hover",
165
+ "link.text-active": "--color-{color}-active",
166
+ "toggle.solid.bg-pressed": "--color-{color}-pressed (when aria-pressed=true or aria-expanded=true)",
167
+ "toggle.outline.bg-pressed": "a11y.state-background(selected)",
168
+ "toggle.ghost.bg-pressed": "a11y.state-background(selected)",
169
+ "radius": "--radius-{shape} (via data-shape attribute, mapped from radius.$radius-sizes)",
170
+ "focus": "--focus-ring-* (via u-focus-ring)",
171
+ "disabled.opacity": "--state-disabled-opacity",
172
+ "spinner.border": "--border-width-thin",
173
+ "spinner.animation": "--motion-animation-spin",
174
+ "font-weight": "--font-weight-medium"
175
+ },
176
+ "spacing": {
177
+ "internal": "Controlled by u-control-{size} utility — uses --control-padding-* and --control-gap-* tokens",
178
+ "icon-to-label": "--space-xs (lovers — icon and label are inseparable)"
179
+ },
180
+ "constraints": [
181
+ "Never use raw hex colors — use design system tokens via --_btn-* custom properties",
182
+ "Never use native disabled attribute — use aria-disabled to keep button focusable for assistive tech",
183
+ "Link variant skips u-control-* class — it's inline text, not a sized control",
184
+ "Radius is driven by data-shape attribute (mirrors variant/color/size pattern), not a u-radius-* class",
185
+ "Use logical CSS properties only (margin-inline-start, not margin-left)",
186
+ "All transitions wrapped in @include a11y.motion-safe",
187
+ "Must handle forced-colors mode — add visible border for ButtonText, Highlight border when toggled on"
188
+ ],
189
+ "a11y": [
190
+ "Provides role='button' and accessible name from slot content",
191
+ "When as='a' without href, adds role='button' with full keyboard emulation (Space + Enter)",
192
+ "When as='a' with href, keeps native link semantics (screen reader announces 'link')",
193
+ "Focus ring via u-focus-ring utility — meets 3:1 contrast with surrounding",
194
+ "Loading state: sets aria-busy='true', announces loadingText via aria-live region",
195
+ "Disabled state: uses aria-disabled (not native disabled) — element stays in tab order",
196
+ "Toggle / disclosure: aria-pressed='true' (toggles) or aria-expanded='true' (disclosures) renders a persistent on-state distinct from transient :active",
197
+ "Forced-colors: visible border for all variants, Highlight border when aria-pressed/expanded, GrayText for disabled"
198
+ ],
199
+ "ai": {
200
+ "synonyms": [
201
+ "cta",
202
+ "primary action",
203
+ "submit button",
204
+ "call to action",
205
+ "action button"
206
+ ],
207
+ "avoid": [
208
+ "div as button — use <Button> component",
209
+ "anchor without role when not a link — use as='a' without href",
210
+ "hardcoded colors — use color prop with semantic values",
211
+ "outline: none on focus — focus ring is handled by u-focus-ring"
212
+ ],
213
+ "patterns": [
214
+ "Primary solid button for main CTA: <Button color=\"'primary'\">Save</Button>",
215
+ "Danger outline for destructive actions: <Button variant=\"'outline'\" color=\"'danger'\">Delete</Button>",
216
+ "Ghost button in card footer: <Button variant=\"'ghost'\" size=\"'sm'\">Learn More</Button>",
217
+ "Full-width submit: <Button type=\"'submit'\" fullWidth>Sign Up</Button>",
218
+ "Loading state: <Button loading=\"isSaving\">Saving...</Button>",
219
+ "Pill shape for tags/filters: <Button shape=\"'xl'\" variant=\"'outline'\">All</Button>",
220
+ "Square chrome button: <Button color=\"'neutral'\" variant=\"'ghost'\" shape=\"'none'\">⌘K</Button>",
221
+ "Toggle button: <Button variant=\"'ghost'\" :aria-pressed=\"isOn\" @click=\"toggle()\">Bold</Button>",
222
+ "Disclosure trigger: <Button :aria-expanded=\"isOpen\" @click=\"toggle()\">Filters</Button>"
223
+ ]
224
+ },
225
+ "examples": [
226
+ {
227
+ "id": "basic-solid",
228
+ "title": "Basic solid button",
229
+ "pithy": "<Button @click=\"save()\">Save</Button>",
230
+ "assertions": ["role=button has name 'Save'", "no violations in axe-core"]
231
+ },
232
+ {
233
+ "id": "outline-danger",
234
+ "title": "Danger outline button",
235
+ "pithy": "<Button variant=\"'outline'\" color=\"'danger'\" @click=\"remove()\">Delete</Button>",
236
+ "assertions": [
237
+ "role=button has name 'Delete'",
238
+ "data-variant='outline'",
239
+ "data-color='danger'"
240
+ ]
241
+ },
242
+ {
243
+ "id": "loading-state",
244
+ "title": "Button with reactive loading",
245
+ "pithy": "<Button loading=\"isLoading\" @click=\"submit()\">Submit</Button>",
246
+ "assertions": [
247
+ "aria-busy='true' when loading",
248
+ "spinner visible",
249
+ "screen reader announces 'Loading\u2026'"
250
+ ]
251
+ },
252
+ {
253
+ "id": "anchor-button",
254
+ "title": "Anchor rendered as button",
255
+ "pithy": "<Button as=\"'a'\" variant=\"'ghost'\">Open Menu</Button>",
256
+ "assertions": ["role='button'", "tabindex='0'", "Space + Enter activate"]
257
+ },
258
+ {
259
+ "id": "link-nav",
260
+ "title": "Anchor with href (native link)",
261
+ "pithy": "<Button as=\"'a'\" href=\"'/docs'\" variant=\"'link'\">Read Docs</Button>",
262
+ "assertions": [
263
+ "no role override",
264
+ "href='/docs'",
265
+ "screen reader announces 'link'"
266
+ ]
267
+ },
268
+ {
269
+ "id": "pill-shape",
270
+ "title": "Pill-shaped filter button",
271
+ "pithy": "<Button shape=\"'xl'\" variant=\"'outline'\" color=\"'neutral'\">All</Button>",
272
+ "assertions": [
273
+ "data-shape='xl'",
274
+ "border-radius resolves from --radius-xl",
275
+ "data-color='neutral'"
276
+ ]
277
+ },
278
+ {
279
+ "id": "toggle-pressed",
280
+ "title": "Toggle button with aria-pressed",
281
+ "pithy": "<Button variant=\"'ghost'\" :aria-pressed=\"isBold\" @click=\"toggle()\">B</Button>",
282
+ "assertions": [
283
+ "aria-pressed reflects state",
284
+ "persistent on-state styling distinct from :active",
285
+ "no violations in axe-core"
286
+ ]
287
+ }
288
+ ]
289
+ }
@@ -0,0 +1,141 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.icon-group",
4
+ "name": "IconGroup",
5
+ "displayName": "Icon Group",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "actions",
9
+ "bidi": true,
10
+ "description": "Single-select icon-only segmented pill. Renders role='radiogroup' with one role='radio' button per item; the active option gets a circular indicator behind its icon.",
11
+ "props": [
12
+ {
13
+ "name": "items",
14
+ "label": "Items",
15
+ "type": "array",
16
+ "required": true,
17
+ "placeholder": [
18
+ { "value": "desktop", "icon": "desktop", "ariaLabel": "Desktop" },
19
+ { "value": "tablet", "icon": "tablet", "ariaLabel": "Tablet" },
20
+ { "value": "mobile", "icon": "mobile", "ariaLabel": "Mobile" }
21
+ ],
22
+ "description": "Options to choose from in display order. Each item is { value, icon, ariaLabel }.",
23
+ "itemSchema": {
24
+ "value": {
25
+ "type": "string",
26
+ "required": true,
27
+ "description": "Unique value identifier"
28
+ },
29
+ "icon": {
30
+ "type": "string",
31
+ "required": true,
32
+ "editor": { "type": "icon" },
33
+ "description": "Icon name passed to <Icon name=...>"
34
+ },
35
+ "ariaLabel": {
36
+ "type": "string",
37
+ "required": true,
38
+ "description": "Accessible label for the icon-only option"
39
+ }
40
+ }
41
+ },
42
+ {
43
+ "name": "value",
44
+ "label": "Value",
45
+ "type": "string | () => string",
46
+ "required": true,
47
+ "description": "Currently selected value. Must be a signal accessor () => string for interactive use — a static string sets the initial state only and will not update after user interaction."
48
+ },
49
+ {
50
+ "name": "ariaLabel",
51
+ "label": "ARIA label",
52
+ "editor": { "tab": "settings" },
53
+ "type": "string",
54
+ "required": true,
55
+ "placeholder": "View mode",
56
+ "description": "Accessible label for the radiogroup itself."
57
+ },
58
+ {
59
+ "name": "size",
60
+ "label": "Size",
61
+ "type": "enum",
62
+ "enum": ["sm", "md", "lg"],
63
+ "default": "md",
64
+ "description": "Control size — maps option hit area to --control-min-height-{size}."
65
+ },
66
+ {
67
+ "name": "iconSize",
68
+ "label": "Icon size",
69
+ "type": "enum",
70
+ "enum": ["xs", "sm", "md", "lg", "xl", "xxl"],
71
+ "default": "sm",
72
+ "description": "Icon size — passed to <Icon size=...>."
73
+ }
74
+ ],
75
+ "events": [
76
+ {
77
+ "name": "onChange",
78
+ "description": "Fires with the new value on click or keyboard activation."
79
+ }
80
+ ],
81
+ "slots": [],
82
+ "tokens": {
83
+ "color": "--color-surface (track), --color-text, --color-text-muted, --color-border",
84
+ "spacing": "--space-xs (track padding + gap)",
85
+ "radius": "--radius-pill (track), 50% (option indicator)",
86
+ "size": "--control-min-height-{size} (option hit area)",
87
+ "border": "--border-width-hairline (track)",
88
+ "motion": "--motion-transition-background, --motion-transition-color"
89
+ },
90
+ "spacing": {
91
+ "options-internal": "--space-xs (lovers — adjacent options)"
92
+ },
93
+ "constraints": [
94
+ "ariaLabel on the group is required — DEV warning when missing",
95
+ "ariaLabel on each item is required — icon-only buttons have no visible text",
96
+ "items length should be ≥ 2 — DEV warning otherwise",
97
+ "Keyboard: Left/Up = prev (wrap), Right/Down = next (wrap), Home = first, End = last — focus + activate per radiogroup spec"
98
+ ],
99
+ "a11y": [
100
+ "Container role='radiogroup' with required aria-label",
101
+ "Each option role='radio' with aria-checked + aria-label (per-item label drives the announce)",
102
+ "Roving tabindex: only the active option is in the Tab sequence",
103
+ "Forced-colors: selected option uses Highlight + HighlightText system colors with forced-color-adjust:none"
104
+ ],
105
+ "ai": {
106
+ "synonyms": [
107
+ "icon group",
108
+ "icon toggle",
109
+ "icon segmented",
110
+ "view selector",
111
+ "device selector"
112
+ ],
113
+ "avoid": [
114
+ "div with click handlers — use IconGroup for keyboard + ARIA semantics",
115
+ "checkbox group — IconGroup is single-select",
116
+ "Buttons row — that's a button group, not a radiogroup; semantics differ"
117
+ ],
118
+ "patterns": [
119
+ "Viewport selector: <IconGroup items=\"viewportItems\" value=\"viewport\" onChange=\"setViewport\" ariaLabel=\"'Viewport size'\" />",
120
+ "View density: <IconGroup items=\"densityItems\" value=\"density\" onChange=\"setDensity\" ariaLabel=\"'View density'\" />"
121
+ ]
122
+ },
123
+ "designGuidance": {
124
+ "surface": "Pill-shaped track on --color-surface with a hairline border. Each option is a square hit area with a circular active indicator (selected-state background overlay).",
125
+ "depth": "Flat — the active indicator carries the affordance, no shadow.",
126
+ "borders": "Hairline track border for definition.",
127
+ "interactivity": "Hover lightens text + tints the option background subtly. Focus-visible draws the design-system focus ring. Active state uses the design-system selected overlay for consistency with checkboxes/radios.",
128
+ "darkMode": "Adapts via --color-* tokens; no manual overrides needed."
129
+ },
130
+ "examples": [
131
+ {
132
+ "id": "viewport",
133
+ "title": "Viewport selector (desktop / tablet / mobile)",
134
+ "pithy": "<IconGroup items=\"viewportItems\" value=\"viewport\" onChange=\"setViewport\" ariaLabel=\"'Viewport size'\" />",
135
+ "assertions": [
136
+ "role='radiogroup'",
137
+ "three role='radio' children with aria-label per item"
138
+ ]
139
+ }
140
+ ]
141
+ }
@@ -0,0 +1,137 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.segmented-control",
4
+ "name": "SegmentedControl",
5
+ "displayName": "Segmented Control",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "actions",
9
+ "bidi": true,
10
+ "description": "Single-select segmented filter. Renders a role='radiogroup' container with one role='radio' button per item. Supports arrow-key navigation, Home/End jump, and roving tabindex.",
11
+ "props": [
12
+ {
13
+ "name": "items",
14
+ "label": "Items",
15
+ "type": "array",
16
+ "required": true,
17
+ "placeholder": [
18
+ { "value": "all", "label": "All" },
19
+ { "value": "active", "label": "Active" },
20
+ { "value": "archived", "label": "Archived" }
21
+ ],
22
+ "description": "Options to choose from in display order. Each item is { value, label }.",
23
+ "itemSchema": {
24
+ "value": {
25
+ "type": "string",
26
+ "required": true,
27
+ "description": "Unique value identifier"
28
+ },
29
+ "label": {
30
+ "type": "string",
31
+ "required": true,
32
+ "description": "Visible text"
33
+ }
34
+ }
35
+ },
36
+ {
37
+ "name": "value",
38
+ "label": "Value",
39
+ "type": "string | () => string",
40
+ "required": true,
41
+ "description": "Currently selected value. Must be a signal accessor () => string for interactive use — a static string sets the initial state only and will not update after user interaction."
42
+ },
43
+ {
44
+ "name": "ariaLabel",
45
+ "label": "ARIA label",
46
+ "editor": { "tab": "settings" },
47
+ "type": "string",
48
+ "required": true,
49
+ "placeholder": "Filter scope",
50
+ "description": "Accessible label for the radiogroup — required by WAI-ARIA."
51
+ },
52
+ {
53
+ "name": "size",
54
+ "label": "Size",
55
+ "type": "enum",
56
+ "enum": ["sm", "md", "lg"],
57
+ "default": "md",
58
+ "description": "Control size — drives font-size and padding."
59
+ }
60
+ ],
61
+ "events": [
62
+ {
63
+ "name": "onChange",
64
+ "description": "Fires with the new value on click or keyboard activation."
65
+ }
66
+ ],
67
+ "slots": [],
68
+ "tokens": {
69
+ "color": "--color-surface-variant (track), --color-surface (selected), --color-text, --color-text-muted",
70
+ "spacing": "--space-xs/sm/md/lg (size-driven padding)",
71
+ "radius": "--radius-sm",
72
+ "shadow": "--shadow-xs (selected lift)",
73
+ "border": "--border-width-thin (track inset)",
74
+ "size": "--font-size-{xs|sm|body} (size-driven font-size)",
75
+ "motion": "--motion-transition-background, --motion-transition-color, --motion-duration-normal, --motion-easing-standard"
76
+ },
77
+ "spacing": {
78
+ "options-internal": "--space-xs (lovers — text content inside an option)"
79
+ },
80
+ "constraints": [
81
+ "ariaLabel is required — DEV warning when missing",
82
+ "items length should be ≥ 2 — DEV warning otherwise (single-option radiogroup is a button)",
83
+ "value must match one of items[].value to render with a checked option — otherwise the first option becomes tabbable but no option is checked",
84
+ "Keyboard: Left/Up = prev (wrap), Right/Down = next (wrap), Home = first, End = last — focus + activate per radiogroup spec"
85
+ ],
86
+ "a11y": [
87
+ "Container role='radiogroup' with required aria-label",
88
+ "Each option role='radio' with aria-checked reflecting active state",
89
+ "Roving tabindex: only the active option is in the Tab sequence (tabindex=0)",
90
+ "Forced-colors: selected option uses Highlight + HighlightText system colors with forced-color-adjust:none"
91
+ ],
92
+ "ai": {
93
+ "synonyms": [
94
+ "segmented",
95
+ "filter",
96
+ "toggle",
97
+ "switch",
98
+ "tab bar",
99
+ "pill toggle"
100
+ ],
101
+ "avoid": [
102
+ "div with click handlers — use SegmentedControl for keyboard + ARIA semantics",
103
+ "checkbox group — segmented is single-select, not multi",
104
+ "Tabs — these are filters / mode switches, not navigation between content panels"
105
+ ],
106
+ "patterns": [
107
+ "Filter scope: <SegmentedControl items=\"options\" value=\"current\" onChange=\"set\" ariaLabel=\"'Filter scope'\" />",
108
+ "Sort order: <SegmentedControl items=\"sortOptions\" value=\"sort\" onChange=\"setSort\" ariaLabel=\"'Sort order'\" />",
109
+ "View density: <SegmentedControl items=\"densities\" value=\"density\" onChange=\"setDensity\" ariaLabel=\"'View density'\" size=\"'sm'\" />"
110
+ ]
111
+ },
112
+ "designGuidance": {
113
+ "surface": "Track uses --color-surface-variant; the selected option lifts onto --color-surface with shadow-xs for tactile depth.",
114
+ "depth": "Selected option only — flat track, raised pill.",
115
+ "borders": "Hairline track inset via padding (no border) lets the selected pill align with the container edge.",
116
+ "interactivity": "Hover lightens unselected option text; active option keeps full text contrast. Focus-visible draws the design-system focus ring on whichever option has keyboard focus.",
117
+ "darkMode": "Adapts via --color-* tokens; no manual overrides needed."
118
+ },
119
+ "examples": [
120
+ {
121
+ "id": "basic",
122
+ "title": "All / Sections filter",
123
+ "pithy": "<SegmentedControl items=\"levelOptions\" value=\"levelFilter\" onChange=\"setLevel\" ariaLabel=\"'Component scope'\" />",
124
+ "assertions": [
125
+ "role='radiogroup'",
126
+ "aria-label='Component scope'",
127
+ "two role='radio' children"
128
+ ]
129
+ },
130
+ {
131
+ "id": "small",
132
+ "title": "Compact density toggle",
133
+ "pithy": "<SegmentedControl items=\"densityOptions\" value=\"density\" onChange=\"setDensity\" ariaLabel=\"'Density'\" size=\"'sm'\" />",
134
+ "assertions": ["data-size='sm'"]
135
+ }
136
+ ]
137
+ }