@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,283 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.card",
4
+ "name": "Card",
5
+ "displayName": "Card",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "content",
9
+ "bidi": true,
10
+ "description": "Content container with named slots (media, header, body, footer), five variants, two orientations, and a pseudo-link overlay for full-card navigation. Semantic <article> root with container-query sizing and paint containment.",
11
+ "props": [
12
+ {
13
+ "name": "variant",
14
+ "label": "Variant",
15
+ "type": "enum",
16
+ "enum": ["default", "outlined", "elevated", "subtle", "ghost"],
17
+ "default": "default",
18
+ "description": "Visual shape. 'default' = surface + hairline + shadow-xs. 'outlined' = surface + hairline, no shadow. 'elevated' = surface + shadow-md (also keeps a hairline for cross-mode edge readability). 'subtle' = surface-variant + hairline, no shadow. 'ghost' = transparent + no border (chrome fades in on hover)."
19
+ },
20
+ {
21
+ "name": "padding",
22
+ "label": "Padding",
23
+ "editor": { "tab": "layout" },
24
+ "type": "enum",
25
+ "enum": ["none", "sm", "md", "lg"],
26
+ "default": "md",
27
+ "description": "Inset padding. Use 'none' to bleed media to the card edges (blog cards, product tiles)."
28
+ },
29
+ {
30
+ "name": "radius",
31
+ "label": "Radius",
32
+ "type": "enum",
33
+ "enum": ["none", "sm", "md", "lg"],
34
+ "default": "md",
35
+ "description": "Corner radius. Use 'none' for full-bleed mobile heroes."
36
+ },
37
+ {
38
+ "name": "orientation",
39
+ "label": "Orientation",
40
+ "editor": { "tab": "layout" },
41
+ "type": "enum",
42
+ "enum": ["vertical", "horizontal"],
43
+ "default": "vertical",
44
+ "description": "'horizontal' places the media slot at the inline-start and runs header/body/footer alongside. Below the 'sm' container breakpoint (30rem / 480px) the layout auto-stacks to vertical via @container query so narrow cards remain readable."
45
+ },
46
+ {
47
+ "name": "mediaAspect",
48
+ "label": "Media aspect",
49
+ "editor": { "tab": "layout" },
50
+ "type": "enum",
51
+ "enum": ["1/1", "4/3", "16/9", "3/2", "21/9"],
52
+ "description": "Aspect ratio for the media slot. Optional in vertical mode (defaults to intrinsic). In horizontal mode the aspect is locked to '16/9' when unset so thumbnails stay rhythmically consistent across lists."
53
+ },
54
+ {
55
+ "name": "mediaOverlay",
56
+ "label": "Media overlay",
57
+ "type": "boolean",
58
+ "default": false,
59
+ "description": "Flips the card topology: the media slot fills the whole card (ignoring padding) and the header / body / footer collapse into a caption group that rides a design-system gradient scrim at the block-end with inverse text colors. Canonical pattern for hero tiles, gallery cards, and marketing listings."
60
+ },
61
+ {
62
+ "name": "clickable",
63
+ "label": "Clickable",
64
+ "type": "boolean",
65
+ "default": false,
66
+ "description": "Renders a stretched <a class='ui-card-link'> overlay so the whole card is a navigation target. Internal buttons/links stay interactive via z-index layering."
67
+ },
68
+ {
69
+ "name": "href",
70
+ "label": "Link URL",
71
+ "type": "string",
72
+ "description": "Target URL for the clickable overlay. Validated via isSafeHref — unsafe schemes (javascript:, data:, vbscript:) are rejected with a DEV warning and the overlay is not rendered. Required when clickable is true."
73
+ },
74
+ {
75
+ "name": "linkLabel",
76
+ "label": "Link label",
77
+ "editor": { "tab": "settings" },
78
+ "type": "string",
79
+ "description": "Accessible label for the stretched link. Defaults to ariaLabel, or the trimmed text of the first heading in the header slot, or 'Open'."
80
+ },
81
+ {
82
+ "name": "disabled",
83
+ "label": "Disabled",
84
+ "editor": { "tab": "settings" },
85
+ "type": "boolean | () => boolean",
86
+ "default": false,
87
+ "reactive": true,
88
+ "description": "Dims the card, sets aria-disabled, and marks the article inert so all descendants (including the link overlay) are removed from focus, hit-testing, and the a11y tree. Accepts a boolean or a reactive getter (() => boolean) — a getter wires an internal effect so the state flips without remounting."
89
+ },
90
+ {
91
+ "name": "selected",
92
+ "label": "Selected",
93
+ "editor": { "tab": "settings" },
94
+ "type": "boolean | () => boolean",
95
+ "default": false,
96
+ "reactive": true,
97
+ "description": "Applies a primary-colored border and inner ring. When combined with clickable (and not disabled), also sets aria-current='true'. Accepts a boolean or a reactive getter (() => boolean) — a getter wires an internal effect so selection flips without remounting. Use for plan pickers, filter card grids, and selectable previews."
98
+ },
99
+ {
100
+ "name": "eyebrow",
101
+ "label": "Eyebrow",
102
+ "type": "string",
103
+ "description": "Short primary-colored uppercase kicker rendered above the heading in the header slot (auto-injected). Use for categories and editorial tags ('Engineering', 'Design', 'Changelog'). Mirrors the Hero / CTA section eyebrow pattern so cards-in-sections read as one editorial voice."
104
+ },
105
+ {
106
+ "name": "ariaLabel",
107
+ "label": "ARIA label",
108
+ "editor": { "tab": "settings" },
109
+ "type": "string",
110
+ "description": "Fallback accessible name for the <article>. Used when there is no heading in the header slot and no ariaLabelledBy is provided."
111
+ },
112
+ {
113
+ "name": "ariaLabelledBy",
114
+ "label": "ARIA labelled by",
115
+ "editor": { "tab": "settings" },
116
+ "type": "string",
117
+ "description": "id of an element outside the card that labels it. Takes precedence over the auto-detected heading id and over ariaLabel."
118
+ },
119
+ {
120
+ "name": "id",
121
+ "label": "Anchor ID",
122
+ "editor": { "tab": "settings" },
123
+ "type": "string",
124
+ "description": "Anchor id on the <article> root. Useful for deep-linking or for wiring a view-transition-name via a utility class."
125
+ }
126
+ ],
127
+ "requiredProps": [],
128
+ "events": [],
129
+ "slots": [
130
+ {
131
+ "name": "media",
132
+ "displayName": "Media",
133
+ "description": "Full-bleed image, icon, or video at the top (vertical) or inline-start (horizontal). Rendered inside .ui-card-media with optional aspect ratio lock.",
134
+ "accepts": ["ui.image", "ui.icon"],
135
+ "max": 1,
136
+ "media": "fill"
137
+ },
138
+ {
139
+ "name": "header",
140
+ "displayName": "Header",
141
+ "description": "Heading, eyebrow, and meta. Rendered inside a semantic <header>. Auto-generates aria-labelledby from the first heading.",
142
+ "accepts": ["ui.heading", "ui.text", "ui.badge"]
143
+ },
144
+ {
145
+ "name": "default",
146
+ "displayName": "Body",
147
+ "description": "Primary content area — paragraphs, lists, form fields. Rendered inside .ui-card-layout as the default projection target."
148
+ },
149
+ {
150
+ "name": "footer",
151
+ "displayName": "Footer",
152
+ "description": "Tags, timestamps, CTAs. Rendered inside a semantic <footer>. Pinned to the block-end via margin-block-start: auto.",
153
+ "accepts": ["ui.text", "ui.button", "ui.badge"]
154
+ }
155
+ ],
156
+ "tokens": {
157
+ "surface.default": "--color-surface + --border-width-hairline --color-border + --shadow-xs",
158
+ "surface.outlined": "--color-surface + --border-width-hairline --color-border, no shadow",
159
+ "surface.elevated": "--color-surface + --border-width-hairline --color-border + --shadow-md (hairline kept for cross-mode edge readability)",
160
+ "surface.subtle": "--color-surface-variant + --border-width-hairline --color-border, no shadow",
161
+ "surface.ghost": "transparent, no border — chrome fades in on hover via surface-variant tint",
162
+ "padding.none": "0 (media bleeds to card edges)",
163
+ "padding.sm": "--space-sm",
164
+ "padding.md": "--space-md (default)",
165
+ "padding.lg": "--space-lg",
166
+ "radius.none": "0",
167
+ "radius.sm": "--radius-sm",
168
+ "radius.md": "--radius-md (default)",
169
+ "radius.lg": "--radius-lg",
170
+ "gap.header-to-body": "--space-sm (Close Friends — heading to body copy)",
171
+ "gap.body-paragraph": "--space-sm (Close Friends — paragraph rhythm inside the body slot)",
172
+ "gap.footer-inline": "--space-sm (Close Friends — tag chips, timestamps, meta)",
173
+ "gap.header-internal": "--space-xs (Lovers / Close Friends — eyebrow ↔ title ↔ meta)",
174
+ "gap.horizontal.media-to-body": "--space-md (Friends — distinct clusters)",
175
+ "border.hover.clickable": "color-mix(in oklch, --color-primary 25%, --color-border) — primary infusion, not a hard swap",
176
+ "shadow.hover.default": "--shadow-md (from --shadow-xs)",
177
+ "shadow.hover.outlined": "--shadow-sm (from none)",
178
+ "shadow.hover.elevated": "--shadow-xl (from --shadow-md)",
179
+ "shadow.hover.subtle": "--shadow-sm (from none) — no translate, keeps list stable",
180
+ "shadow.hover.ghost": "chrome appears: --color-surface-variant bg + --color-border border",
181
+ "motion.translate": "--motion-distance-subtle — clickable cards only (not subtle, never disabled)",
182
+ "motion.duration": "--motion-duration-normal",
183
+ "motion.easing": "--motion-easing-standard",
184
+ "selected.border": "--color-primary",
185
+ "selected.ring": "inset 0 0 0 --border-width-thin --color-primary (belt-and-suspenders emphasis)",
186
+ "disabled.opacity": "--state-disabled-opacity",
187
+ "focus.ring": "a11y.focus-ring — painted on the whole article via :has(.ui-card-link:focus-visible)"
188
+ },
189
+ "spacing": {
190
+ "header-to-body": "--space-sm (Close Friends — tightly coupled meaning)",
191
+ "body-to-footer": "--space-sm (Close Friends — footer is the tail of the same content cluster)",
192
+ "media-to-header.vertical": "--space-md (Friends — the image is a distinct content cluster from the text block below it; flex gap --space-sm + supplemental margin = --space-md total)",
193
+ "media-to-body.horizontal": "--space-md (Friends — distinct visual clusters)",
194
+ "header-internal": "--space-xs (Lovers — eyebrow, title, and meta are one typographic unit)",
195
+ "footer-pinned": "margin-block-start: auto (pins footer to the block-end so cards in a grid line up at the base)",
196
+ "footer-inline": "--space-sm (Close Friends — tag chips, timestamps)",
197
+ "grid-between-cards": "--space-md (Friends — cards in the same grid are peers)"
198
+ },
199
+ "designGuidance": {
200
+ "surface": "Five variants cover the full surface vocabulary: 'default' is the workhorse with a hairline + shadow-xs; 'outlined' reads as a form-group container (no shadow); 'elevated' is the featured/hero card with shadow-md AND a hairline (shadow conveys presence, the hairline keeps the edge readable in dark mode and on tinted backgrounds); 'subtle' is the settings-pane / feature-card style on surface-variant; 'ghost' is for list-row cards that only reveal chrome on hover. Never stack shadows — the tokens encode elevation levels. Never pair elevated with outlined — it reads as uncertainty.",
201
+ "depth": "Static cards never translate on hover — lifting every hover cheapens the motion. Only clickable cards translate by --motion-distance-subtle. 'subtle' is the deliberate exception within clickable: it gains a shadow but stays flat so long lists don't jitter. 'elevated' carries shadow-md at rest; on hover it climbs to shadow-xl without gaining new borders.",
202
+ "borders": "Borders are always a hairline, never thicker. The only exception is the selected state's inner ring (--border-width-thin via inset box-shadow) which pairs with the colored outer border for double emphasis. On hover, borders tint toward primary via color-mix(25%) — a subtle infusion that signals interactivity without swapping values hard. Forced-colors mode always draws a 1px ButtonText border and suppresses shadows.",
203
+ "interactivity": "Clickable cards use the pseudo-link pattern: a stretched <a class='ui-card-link'> fills the article at z-index 1 so mouse clicks anywhere on the card body reach the anchor. The layout wrapper stays at z-index auto (no explicit z-index — any value would create a stacking context and trap descendants below the overlay); interactive descendants (buttons, tag chips, inline links, form controls) are raised to z-index 2 so they still receive their own clicks. Keyboard focus paints the focus ring on the whole <article> via :has(.ui-card-link:focus-visible) — the card *is* the focus indicator, not the invisible overlay. Selected state is a separate axis: combine with clickable for plan pickers (aria-current='true'), or use alone as a static highlight.",
204
+ "bestPractices": [
205
+ "Prefer 'default' for content lists and dashboards. Reach for 'elevated' only for hero/featured cards — too many elevated cards compete for attention.",
206
+ "Use 'subtle' inside sections that already sit on --color-surface (feature grids, settings panes) — it reads as a secondary container.",
207
+ "Use 'ghost' for list rows that need a hit target without competing with the surrounding content (search results, command-palette results).",
208
+ "Pair padding='none' with a media slot when you want the image edge-to-edge (blog cards, product tiles). Pair padding='lg' + no media for quote cards or callouts.",
209
+ "Horizontal orientation is for blog listings and search results. It auto-stacks to vertical below the 'sm' container breakpoint via the card's own container query, so reach for it when that responsive collapse is the desired behavior.",
210
+ "Don't wrap an entire Card in <a> yourself — use clickable+href so internal interactive elements still work. If you need onClick instead of navigation, nest a <Button class='u-absolute u-inset-0'> inside with transparent styling (V0.2 will add a first-class onClick card).",
211
+ "When a heading lives in the header slot of a clickable card, aria-labelledby is auto-wired. If no heading is present, pass ariaLabel for an explicit accessible name.",
212
+ "Don't use variant='elevated' on surface-variant backgrounds — the shadow reads as dirt. Use 'outlined' or 'subtle' instead."
213
+ ]
214
+ },
215
+ "constraints": [
216
+ "Root element is a semantic <article>",
217
+ "All CSS uses logical properties (inline-start/end, block-start/end) — RTL-safe",
218
+ "Design-system-controlled visual values use tokens — every color/space/font-size/radius/shadow/duration uses a design system token or a --_card-* custom property that resolves to one. Intrinsic layout math (aspect ratios, percentages, clamp()/minmax() bounds, and container-query thresholds) is allowed to use dimensional units via named Sass variables scoped to the component stylesheet",
219
+ "Slot wrappers (.ui-card-media, .ui-card-header, .ui-card-body, .ui-card-footer) are only rendered when the slot has content — no empty DOM wrappers",
220
+ "In horizontal orientation, mediaAspect defaults to '16/9' when unset so lists stay visually consistent. Below the 'sm' container breakpoint (30rem) the horizontal layout auto-stacks to vertical via @container query",
221
+ "In clickable mode, the pseudo-link <a class='ui-card-link'> sits at z-index 1 so mouse clicks on the card body reach the anchor; interactive descendants (buttons, inner links, chips) are raised to z-index 2 so they still receive their own clicks",
222
+ "Disabled cards set aria-disabled='true' AND the native inert attribute — descendants are removed from focus, hit-testing, and the a11y tree",
223
+ "Selected cards get aria-current='true' only when also clickable AND not disabled (aria-current has no meaning without a navigation context, and a disabled card is inert)",
224
+ "Focus ring is painted on the whole article via :has(.ui-card-link:focus-visible) — the overlay is never the visible focus indicator",
225
+ "When clickable=true, href is required — the component warns in DEV and omits the overlay link when href is absent or unsafe",
226
+ "Unsafe hrefs (javascript:, data:, vbscript:) are rejected via isSafeHref — DEV warning + the overlay is not rendered",
227
+ "All transitions are gated by @include a11y.motion-safe so prefers-reduced-motion zeroes translate + shadow transitions"
228
+ ],
229
+ "a11y": {
230
+ "landmarks": [
231
+ "Semantic <article> element — a self-contained composition (blog post preview, product card, plan card, etc.)"
232
+ ],
233
+ "keyboard": {
234
+ "clickable": "The stretched <a class='ui-card-link'> is a real anchor — native Tab/Shift+Tab focus, Enter activates navigation.",
235
+ "innerInteractives": "Internal buttons/links receive their own focus stops. In clickable cards, interactive descendants (buttons, inner links, chips, form controls) are raised to z-index 2 so they win click events over the z-index 1 overlay.",
236
+ "disabled": "inert blocks Tab, click, and screen-reader interaction for the whole card and every descendant."
237
+ },
238
+ "focus": {
239
+ "visible": "When the overlay link takes keyboard focus, :has(.ui-card-link:focus-visible) paints the design-system focus ring on the whole article — the card itself is the focus indicator.",
240
+ "innerFocus": "Focus on an inner button/link keeps that element's own focus ring; the card paints a subtle :focus-within border-tint to telegraph 'something inside me has focus'."
241
+ },
242
+ "aria": {
243
+ "root": "<article>",
244
+ "labelledBy": "When clickable + a heading exists in the header slot, an auto-generated id is attached to the heading and the article receives aria-labelledby pointing at it. Explicit ariaLabelledBy always wins.",
245
+ "label": "Accessible name fallback chain: ariaLabelledBy > heading (auto-id) > ariaLabel > linkLabel > DEV warning. linkLabel is a last-resort fallback so clickable cards that set it but forget ariaLabel still get an accessible name on the <article>.",
246
+ "current": "Selected + clickable cards set aria-current='true' (unless also disabled) so AT announce 'current page/item'.",
247
+ "disabled": "data-disabled='true' + aria-disabled='true' + inert attribute"
248
+ },
249
+ "forcedColors": "forced-colors mode renders a 1px ButtonText border (2px Highlight when selected), suppresses shadows, and outlines the overlay with Highlight on focus — WCAG 1.4.1 parity.",
250
+ "motion": {
251
+ "reducedMotion": "All transitions (background, border, shadow, translate) are wrapped in @include a11y.motion-safe — prefers-reduced-motion zeroes duration. WCAG 2.3.3 satisfied.",
252
+ "staticCards": "Non-clickable cards never translate on hover — motion is reserved as a signal of interactivity."
253
+ },
254
+ "contentAssertions": "When clickable, the overlay anchor always has an accessible name. Fallback chain: aria-label from linkLabel > aria-labelledby forwarded from ariaLabelledBy > aria-labelledby pointing at the heading's auto-id (so the name stays in sync with reactive heading text) > aria-label from ariaLabel > visually-hidden 'Open' span."
255
+ },
256
+ "ai": {
257
+ "synonyms": [
258
+ "card",
259
+ "tile",
260
+ "panel",
261
+ "surface",
262
+ "content block",
263
+ "article card",
264
+ "feature card",
265
+ "product card",
266
+ "plan card",
267
+ "blog card"
268
+ ],
269
+ "avoid": [
270
+ "<div class='card'> — use <Card> with a semantic <article> root and slot API",
271
+ "Wrapping the whole card in <a href> — use clickable+href so internal interactive children still work",
272
+ "onClick on the card with role='button' — accessibility-hostile and ambiguous with inner buttons; navigate with clickable+href, or nest a visible <Button>",
273
+ "Stacking shadows manually — the variant tokens encode the right elevation",
274
+ "variant='elevated' on a surface-variant background — the shadow reads as dirt"
275
+ ],
276
+ "patterns": [
277
+ "Blog preview: <Card variant=\"'default'\" padding=\"'none'\" clickable=\"true\" href=\"'/post/hello'\"><img data-slot='media' ...><header data-slot='header'><Heading level='3'>Hello</Heading></header><Text>Excerpt…</Text><footer data-slot='footer'><Text size=\"'sm'\">3 min</Text></footer></Card>",
278
+ "Plan picker: <Card variant=\"'outlined'\" selected=\"isSelected\" clickable=\"true\" href=\"'/pricing?plan=pro'\"><header data-slot='header'><Heading level='3'>Pro</Heading></header><Text>$19/mo</Text></Card>",
279
+ "Horizontal search result: <Card orientation=\"'horizontal'\" variant=\"'ghost'\" clickable=\"true\" href=\"'/result/42'\"><img data-slot='media' ...><header data-slot='header'><Heading level='4'>Result title</Heading></header><Text>Match snippet…</Text></Card>",
280
+ "Feature tile (inside FeaturesSection-style grid): <Card variant=\"'subtle'\" padding=\"'lg'\"><header data-slot='header'><Icon name=\"'zap'\"/><Heading level='3'>Fast</Heading></header><Text>Reactive updates stay O(1).</Text></Card>"
281
+ ]
282
+ }
283
+ }
@@ -0,0 +1,158 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.alert-dialog",
4
+ "name": "AlertDialog",
5
+ "displayName": "Alert Dialog",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "feedback",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Confirmation dialog built on <Modal> with role='alertdialog'. A titled message with a tone-colored confirm action and an optional cancel — for destructive confirms and acknowledgements. Does not dismiss on backdrop click; Escape cancels.",
12
+ "props": [
13
+ {
14
+ "name": "open",
15
+ "label": "Open",
16
+ "type": "boolean | () => boolean",
17
+ "reactive": true,
18
+ "required": true,
19
+ "description": "Visibility state. Static boolean or a () => boolean accessor. The dialog mounts to <body> when true and unmounts when false."
20
+ },
21
+ {
22
+ "name": "title",
23
+ "label": "Title",
24
+ "type": "string",
25
+ "required": true,
26
+ "placeholder": "Clear page",
27
+ "description": "Accessible name and visible heading for the dialog."
28
+ },
29
+ {
30
+ "name": "message",
31
+ "label": "Message",
32
+ "type": "string | () => string",
33
+ "reactive": true,
34
+ "required": true,
35
+ "placeholder": "Remove all blocks from this page? You can undo this.",
36
+ "description": "Body copy explaining the action. Static string or a () => string accessor for reactive copy (e.g. a live count)."
37
+ },
38
+ {
39
+ "name": "tone",
40
+ "label": "Tone",
41
+ "type": "enum",
42
+ "enum": ["info", "warning", "danger"],
43
+ "default": "info",
44
+ "description": "Semantic tone — colors the confirm button and the icon (info, warning, danger)."
45
+ },
46
+ {
47
+ "name": "icon",
48
+ "label": "Icon",
49
+ "editor": { "tab": "settings" },
50
+ "type": "icon",
51
+ "required": false,
52
+ "description": "Optional icon shown beside the title, tinted by tone."
53
+ },
54
+ {
55
+ "name": "confirmLabel",
56
+ "label": "Confirm label",
57
+ "type": "string",
58
+ "required": false,
59
+ "placeholder": "Confirm",
60
+ "description": "Label for the confirm button. Defaults to 'Confirm' when onConfirm is set, else 'OK'."
61
+ },
62
+ {
63
+ "name": "cancelLabel",
64
+ "label": "Cancel label",
65
+ "editor": { "tab": "settings" },
66
+ "type": "string",
67
+ "default": "Cancel",
68
+ "description": "Label for the cancel button (shown only when onConfirm is provided)."
69
+ },
70
+ {
71
+ "name": "dialogId",
72
+ "label": "Dialog id",
73
+ "editor": { "tab": "settings" },
74
+ "type": "string",
75
+ "required": false,
76
+ "description": "Sets id on the dialog element — enables aria-controls on the trigger button."
77
+ }
78
+ ],
79
+ "events": [
80
+ {
81
+ "name": "onClose",
82
+ "description": "Requested close — fired on Escape, Cancel, or after Confirm. The consumer flips open to false."
83
+ },
84
+ {
85
+ "name": "onConfirm",
86
+ "description": "The user confirmed the action. When provided, a Cancel button is shown; onClose runs immediately after."
87
+ }
88
+ ],
89
+ "tokens": {
90
+ "color": "--color-info, --color-warning, --color-danger (tone), --color-text-muted (message)",
91
+ "spacing": "--space-sm (head gap, actions gap, content gap), --space-md (actions offset)",
92
+ "inherits": "surface, backdrop, radius, shadow, and layer from <Modal>"
93
+ },
94
+ "spacing": {
95
+ "head-icon": "--space-sm (close friends — icon ↔ title)",
96
+ "message-actions": "--space-md (actions sit below the message)"
97
+ },
98
+ "constraints": [
99
+ "Built on <Modal role='alertdialog'> — inherits focus trap, scroll lock, and portal-to-body",
100
+ "closeOnBackdrop is forced off: a destructive confirm requires an explicit choice (Escape still cancels)",
101
+ "Initial focus lands on Cancel when present, else the confirm button",
102
+ "Without onConfirm the dialog is acknowledge-only — a single confirm button (default label 'OK')"
103
+ ],
104
+ "a11y": [
105
+ "role='alertdialog' — screen readers announce the content immediately and expect a response",
106
+ "aria-labelledby wired to the visible title heading",
107
+ "Focus trapped inside the dialog and restored to the trigger on close (via Modal)",
108
+ "Backdrop click does not dismiss; Escape cancels"
109
+ ],
110
+ "ai": {
111
+ "synonyms": [
112
+ "alert",
113
+ "confirm",
114
+ "confirmation",
115
+ "are you sure",
116
+ "destructive confirm",
117
+ "prompt"
118
+ ],
119
+ "avoid": [
120
+ "using a plain <Modal role='dialog'> for a destructive confirm — alertdialog is required so the message is announced immediately",
121
+ "omitting onConfirm when the action needs an explicit yes/no — that renders an acknowledge-only OK dialog",
122
+ "long paragraphs in message — keep it to the action and its consequence"
123
+ ],
124
+ "patterns": [
125
+ "Destructive confirm: <AlertDialog open=\"isOpen\" onClose=\"close\" onConfirm=\"clear\" tone=\"'danger'\" icon=\"'trash'\" title=\"'Clear page'\" message=\"'Remove all blocks? You can undo this.'\" confirmLabel=\"'Clear page'\" />",
126
+ "Acknowledge-only: <AlertDialog open=\"isOpen\" onClose=\"close\" title=\"'Saved'\" message=\"'Your changes are saved.'\" />"
127
+ ]
128
+ },
129
+ "designGuidance": {
130
+ "surface": "Inherits the Modal surface and backdrop; the confirm button and icon carry the tone color.",
131
+ "depth": "Elevated as a Modal layer above the page.",
132
+ "borders": "Inherited from Modal (hairline; CanvasText in forced-colors).",
133
+ "interactivity": "Confirm runs the action then closes; Cancel/Escape dismiss; backdrop click is inert.",
134
+ "darkMode": "Adapts via --color-* tokens; tone colors resolve per theme."
135
+ },
136
+ "examples": [
137
+ {
138
+ "id": "confirm",
139
+ "title": "Destructive confirm",
140
+ "pithy": "<AlertDialog open=\"true\" onClose=\"close\" onConfirm=\"clear\" tone=\"'danger'\" title=\"'Clear page'\" message=\"'Remove all blocks?'\" confirmLabel=\"'Clear page'\" />",
141
+ "assertions": [
142
+ "renders role='alertdialog'",
143
+ "confirm button has data-color='danger'",
144
+ "a Cancel button is present"
145
+ ]
146
+ },
147
+ {
148
+ "id": "acknowledge",
149
+ "title": "Acknowledge-only",
150
+ "pithy": "<AlertDialog open=\"true\" onClose=\"close\" title=\"'Saved'\" message=\"'Your changes are saved.'\" />",
151
+ "assertions": [
152
+ "a single 'OK' button",
153
+ "no Cancel button",
154
+ "confirm button data-color='info'"
155
+ ]
156
+ }
157
+ ]
158
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.modal",
4
+ "name": "Modal",
5
+ "displayName": "Modal",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "feedback",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Viewport-level dialog: a centered surface over a dimming backdrop. Portals to <body>, traps focus and locks body scroll via @pithyjs/core's FocusTrap, and closes on Escape or backdrop click. Reactive open state.",
12
+ "props": [
13
+ {
14
+ "name": "open",
15
+ "label": "Open",
16
+ "type": "boolean | () => boolean",
17
+ "reactive": true,
18
+ "required": true,
19
+ "description": "Visibility state. Static boolean or a () => boolean accessor. The dialog mounts to <body> when true and unmounts when false."
20
+ },
21
+ {
22
+ "name": "title",
23
+ "label": "Title",
24
+ "type": "string",
25
+ "required": true,
26
+ "placeholder": "Publish",
27
+ "description": "Accessible name for the dialog. Used as aria-label unless titleId is provided."
28
+ },
29
+ {
30
+ "name": "titleId",
31
+ "label": "Title element id",
32
+ "editor": { "tab": "settings" },
33
+ "type": "string",
34
+ "required": false,
35
+ "description": "Id of a visible title element inside the content — wired as aria-labelledby (takes precedence over title)."
36
+ },
37
+ {
38
+ "name": "dialogId",
39
+ "label": "Dialog id",
40
+ "editor": { "tab": "settings" },
41
+ "type": "string",
42
+ "required": false,
43
+ "description": "Sets id on the dialog element — enables aria-controls on the trigger button."
44
+ },
45
+ {
46
+ "name": "closeOnBackdrop",
47
+ "label": "Close on backdrop click",
48
+ "editor": { "tab": "settings" },
49
+ "type": "boolean",
50
+ "default": true,
51
+ "description": "Close when the backdrop (outside the dialog) is clicked."
52
+ },
53
+ {
54
+ "name": "closeOnEscape",
55
+ "label": "Close on Escape",
56
+ "editor": { "tab": "settings" },
57
+ "type": "boolean",
58
+ "default": true,
59
+ "description": "Close when Escape is pressed. Consumed in the capture phase so the dialog wins over global handlers."
60
+ }
61
+ ],
62
+ "events": [
63
+ {
64
+ "name": "onClose",
65
+ "description": "Requested close — fired on Escape, backdrop click, or any programmatic dismiss. The consumer flips open to false."
66
+ }
67
+ ],
68
+ "slots": [{ "name": "default", "displayName": "Content" }],
69
+ "tokens": {
70
+ "color": "--color-surface, --color-text, --color-border, --color-overlay-rgb (scrim)",
71
+ "spacing": "--space-lg (dialog padding), --space-md (backdrop padding, content gap)",
72
+ "radius": "--radius-lg (dialog)",
73
+ "shadow": "--shadow-xl (dialog)",
74
+ "border": "--border-width-hairline (idle), --border-width-thin (forced-colors)",
75
+ "layer": "--z-modal (backdrop stacking)",
76
+ "motion": "--motion-duration-fast, --motion-duration-normal, --motion-easing-standard, --motion-easing-emphasized"
77
+ },
78
+ "spacing": {
79
+ "backdrop-inset": "--space-md (gutter so the dialog never touches the viewport edge)",
80
+ "dialog-content": "--space-md (friends — stacked content regions inside the dialog)"
81
+ },
82
+ "constraints": [
83
+ "title (or titleId) is required — DEV warning when both are missing, to prevent an unnamed dialog",
84
+ "Portals to <body>; the returned el is a hidden anchor until open, so template placement never shows a stray scrim",
85
+ "Escape is consumed in the capture phase (stopPropagation) so the dialog closes before global keydown handlers fire",
86
+ "Backdrop click only closes when the target is the backdrop itself — clicks inside the dialog do not dismiss"
87
+ ],
88
+ "a11y": [
89
+ "role='dialog' + aria-modal='true' on the dialog surface",
90
+ "aria-labelledby (titleId) when a visible title is present, else aria-label (title)",
91
+ "Focus is trapped inside the dialog and restored to the trigger on close (FocusTrap)",
92
+ "Body scroll is locked while open (FocusTrap preventScroll)",
93
+ "Backdrop scrim and entrance animation respect prefers-reduced-motion",
94
+ "Forced-colors: explicit CanvasText border on the dialog surface"
95
+ ],
96
+ "ai": {
97
+ "synonyms": ["modal", "dialog", "overlay", "popup", "sheet", "lightbox"],
98
+ "avoid": [
99
+ "a hand-rolled fixed-position div with a backdrop — use <Modal> so focus trap, scroll lock, Escape, and aria-modal are correct",
100
+ "leaving title empty — every dialog needs an accessible name",
101
+ "putting the trigger button inside the Modal content — the trigger lives outside and drives the open prop"
102
+ ],
103
+ "patterns": [
104
+ "Confirm dialog: <Modal open=\"isOpen\" onClose=\"close\" title=\"'Delete page?'\">…content…</Modal>",
105
+ "Labelled by a visible heading: <Modal open=\"isOpen\" onClose=\"close\" title=\"'Publish'\" titleId=\"'publish-title'\"><Heading id=\"'publish-title'\">Publish</Heading>…</Modal>"
106
+ ]
107
+ },
108
+ "designGuidance": {
109
+ "surface": "Dialog uses --color-surface with a hairline border; the backdrop is a translucent scrim over --color-overlay-rgb.",
110
+ "depth": "Elevated with --shadow-xl to read as a layer above the page.",
111
+ "borders": "Hairline border on the dialog; forced-colors swaps to a CanvasText border.",
112
+ "interactivity": "Backdrop click and Escape dismiss; content inside is fully interactive and focus-trapped.",
113
+ "darkMode": "Adapts automatically via --color-* tokens; the scrim uses the theme's overlay color."
114
+ },
115
+ "examples": [
116
+ {
117
+ "id": "open",
118
+ "title": "Open modal",
119
+ "pithy": "<Modal open=\"true\" onClose=\"close\" title=\"'Publish'\"><Text>Ready to ship.</Text></Modal>",
120
+ "assertions": [
121
+ "renders a <div class='ui-modal' role='dialog'>",
122
+ "aria-modal='true'",
123
+ "aria-label='Publish'"
124
+ ]
125
+ },
126
+ {
127
+ "id": "labelledby",
128
+ "title": "Labelled by a visible title",
129
+ "pithy": "<Modal open=\"true\" onClose=\"close\" title=\"'Publish'\" titleId=\"'publish-title'\"><Heading id=\"'publish-title'\">Publish</Heading></Modal>",
130
+ "assertions": ["aria-labelledby='publish-title'", "aria-label not set"]
131
+ }
132
+ ]
133
+ }