@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,227 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.image",
4
+ "name": "Image",
5
+ "displayName": "Image",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "content",
9
+ "bidi": true,
10
+ "description": "Accessible, optimized image component. Mobile-first with lazy loading and async decoding by default. Enforces alt text for accessibility. Supports <picture> with multiple <source> elements for format negotiation (AVIF, WebP, fallback). Use width/height to prevent layout shift (CLS = 0).",
11
+ "props": [
12
+ {
13
+ "name": "src",
14
+ "reactive": true,
15
+ "label": "Image source",
16
+ "type": "string | Signal<string>",
17
+ "required": true,
18
+ "placeholder": "https://placehold.co/600x400?text=Replace+me",
19
+ "description": "Image source URL (required)",
20
+ "editor": { "type": "image" }
21
+ },
22
+ {
23
+ "name": "alt",
24
+ "reactive": true,
25
+ "label": "Alt text",
26
+ "type": "string | Signal<string>",
27
+ "kind": "alt-text",
28
+ "required": true,
29
+ "placeholder": "Replace this image and update the alt text",
30
+ "description": "Alt text for accessibility — REQUIRED. Use \"\" only for purely decorative images."
31
+ },
32
+ {
33
+ "name": "width",
34
+ "label": "Width",
35
+ "editor": { "tab": "layout" },
36
+ "type": "number",
37
+ "description": "Intrinsic width in pixels — prevents layout shift (CLS). Provide both width and height."
38
+ },
39
+ {
40
+ "name": "height",
41
+ "label": "Height",
42
+ "editor": { "tab": "layout" },
43
+ "type": "number",
44
+ "description": "Intrinsic height in pixels — prevents layout shift (CLS). Provide both width and height."
45
+ },
46
+ {
47
+ "name": "loading",
48
+ "label": "Loading",
49
+ "editor": { "tab": "settings" },
50
+ "type": "enum",
51
+ "enum": ["lazy", "eager"],
52
+ "default": "lazy",
53
+ "description": "Loading strategy — use 'eager' for above-the-fold / LCP images"
54
+ },
55
+ {
56
+ "name": "decoding",
57
+ "label": "Decoding",
58
+ "editor": { "tab": "settings" },
59
+ "type": "enum",
60
+ "enum": ["async", "sync", "auto"],
61
+ "default": "async",
62
+ "description": "Decoding strategy — async by default for non-blocking rendering"
63
+ },
64
+ {
65
+ "name": "fetchPriority",
66
+ "label": "Fetch priority",
67
+ "editor": { "tab": "settings" },
68
+ "type": "enum",
69
+ "enum": ["high", "low", "auto"],
70
+ "default": "auto",
71
+ "description": "Fetch priority hint — use 'high' for LCP images, 'low' for below-fold images"
72
+ },
73
+ {
74
+ "name": "aspectRatio",
75
+ "label": "Aspect ratio",
76
+ "editor": { "tab": "layout" },
77
+ "type": "enum",
78
+ "enum": ["16/9", "4/3", "1/1", "3/2", "21/9", "3/4", "2/3", "9/16"],
79
+ "description": "Aspect ratio of the image box. Leave empty for the image's natural ratio."
80
+ },
81
+ {
82
+ "name": "sizing",
83
+ "label": "Sizing",
84
+ "editor": { "tab": "layout" },
85
+ "type": "enum",
86
+ "enum": ["auto", "fill"],
87
+ "default": "auto",
88
+ "description": "Width behaviour — 'auto' (natural size, capped at container) or 'fill' (full container width)"
89
+ },
90
+ {
91
+ "name": "fit",
92
+ "label": "Fit",
93
+ "editor": { "tab": "layout" },
94
+ "type": "enum",
95
+ "enum": ["contain", "cover", "fill", "none", "scale-down"],
96
+ "default": "cover",
97
+ "description": "Object-fit — how the image fills its container"
98
+ },
99
+ {
100
+ "name": "sizes",
101
+ "label": "Sizes",
102
+ "editor": { "tab": "settings" },
103
+ "type": "string",
104
+ "description": "Responsive sizes attribute (e.g. '(max-width: 600px) 100vw, 50vw')"
105
+ },
106
+ {
107
+ "name": "srcset",
108
+ "label": "Srcset",
109
+ "editor": { "tab": "settings" },
110
+ "type": "string",
111
+ "description": "Srcset for responsive resolution switching (e.g. 'img-400.jpg 400w, img-800.jpg 800w')"
112
+ },
113
+ {
114
+ "name": "sources",
115
+ "label": "Sources",
116
+ "editor": { "tab": "settings" },
117
+ "type": "ImageSource[]",
118
+ "description": "Additional <source> elements for <picture> — enables AVIF/WebP with fallback. Each source has src, type, media, and srcset."
119
+ },
120
+ {
121
+ "name": "rounded",
122
+ "label": "Rounded",
123
+ "editor": { "tab": "effects" },
124
+ "type": "boolean | RadiusLevel",
125
+ "default": "none",
126
+ "description": "Border radius — true for 'md', or explicit level: 'xs', 'sm', 'md', 'lg', 'xl', 'full'"
127
+ }
128
+ ],
129
+ "events": [],
130
+ "tokens": {
131
+ "border.forced-colors": "--border-width-hairline solid CanvasText",
132
+ "radius": "--radius-{rounded} (via u-radius-* utility)",
133
+ "max-inline-size": "100% (logical property, not max-width)"
134
+ },
135
+ "spacing": {
136
+ "internal": "No internal spacing — image fills wrapper",
137
+ "image-to-caption": "--space-sm (close friends — image and caption are tightly coupled)",
138
+ "image-in-card": "--space-md (friends — image and card content are related)"
139
+ },
140
+ "constraints": [
141
+ "alt prop is REQUIRED — TypeScript enforces this at build time",
142
+ "Use alt='' ONLY for purely decorative images (sets role='presentation')",
143
+ "Always provide width + height to prevent layout shift (CLS)",
144
+ "Use loading='eager' + fetchPriority='high' for LCP images — never lazy+high (contradictory)",
145
+ "Use logical CSS properties only (max-inline-size, not max-width; inline-size, not width; block-size, not height)",
146
+ "Must handle forced-colors mode — thin CanvasText border for visibility",
147
+ "Must handle reduced motion — pause CSS animations on images"
148
+ ],
149
+ "a11y": [
150
+ "alt text is required via TypeScript — build error if omitted",
151
+ "Decorative images (alt='') get role='presentation' for older screen readers",
152
+ "width/height prevent layout shift for sighted users and magnification users",
153
+ "Forced-colors: thin border ensures image boundaries are visible",
154
+ "Reduced motion: CSS animations on images are paused (does not stop GIF/APNG playback)"
155
+ ],
156
+ "ai": {
157
+ "synonyms": [
158
+ "photo",
159
+ "picture",
160
+ "img",
161
+ "hero image",
162
+ "thumbnail",
163
+ "avatar image",
164
+ "banner"
165
+ ],
166
+ "avoid": [
167
+ "raw <img> tag — use <Image> component for optimization and a11y",
168
+ "missing alt text — always provide descriptive alt or alt='' for decorative",
169
+ "missing width/height — always provide both to prevent CLS",
170
+ "loading='lazy' with fetchPriority='high' — contradictory, use loading='eager'",
171
+ "hardcoded border-radius — use rounded prop with design system level"
172
+ ],
173
+ "patterns": [
174
+ "Hero image (LCP): <Image src=\"'/hero.jpg'\" alt=\"'Hero banner'\" width=\"1200\" height=\"600\" loading=\"'eager'\" fetchPriority=\"'high'\" aspectRatio=\"'16/9'\" />",
175
+ "Card thumbnail: <Image src=\"'/thumb.jpg'\" alt=\"'Product'\" width=\"400\" height=\"300\" rounded=\"'md'\" />",
176
+ "Avatar: <Image src=\"'/avatar.jpg'\" alt=\"'User photo'\" width=\"64\" height=\"64\" rounded=\"'full'\" />",
177
+ "Multi-format (AVIF/WebP): <Image src=\"'/photo.jpg'\" alt=\"'Photo'\" width=\"800\" height=\"600\" :sources=\"[{src:'/photo.avif',type:'image/avif'},{src:'/photo.webp',type:'image/webp'}]\" />",
178
+ "Decorative: <Image src=\"'/pattern.svg'\" alt=\"''\" />"
179
+ ]
180
+ },
181
+ "examples": [
182
+ {
183
+ "id": "basic",
184
+ "title": "Basic image with dimensions",
185
+ "pithy": "<Image src=\"'/photo.jpg'\" alt=\"'A sunset over the ocean'\" width=\"800\" height=\"600\" />",
186
+ "assertions": [
187
+ "has alt text",
188
+ "has width/height attributes",
189
+ "loading='lazy'",
190
+ "decoding='async'"
191
+ ]
192
+ },
193
+ {
194
+ "id": "lcp-hero",
195
+ "title": "LCP hero image (eager + high priority)",
196
+ "pithy": "<Image src=\"'/hero.jpg'\" alt=\"'Welcome banner'\" width=\"1200\" height=\"600\" loading=\"'eager'\" fetchPriority=\"'high'\" aspectRatio=\"'16/9'\" />",
197
+ "assertions": [
198
+ "loading='eager'",
199
+ "fetchpriority='high'",
200
+ "aspect-ratio set on wrapper"
201
+ ]
202
+ },
203
+ {
204
+ "id": "rounded-avatar",
205
+ "title": "Rounded avatar image",
206
+ "pithy": "<Image src=\"'/avatar.jpg'\" alt=\"'Jane Doe'\" width=\"100\" height=\"100\" rounded=\"'full'\" />",
207
+ "assertions": ["has u-radius-full class", "width=100", "height=100"]
208
+ },
209
+ {
210
+ "id": "picture-sources",
211
+ "title": "Multi-format with AVIF/WebP fallback",
212
+ "pithy": "<Image src=\"'/photo.jpg'\" alt=\"'Nature scene'\" width=\"800\" height=\"600\" :sources=\"[{src:'/photo.avif',type:'image/avif'},{src:'/photo.webp',type:'image/webp'}]\" />",
213
+ "assertions": [
214
+ "renders <picture> element",
215
+ "has <source> for AVIF",
216
+ "has <source> for WebP",
217
+ "fallback <img> for JPEG"
218
+ ]
219
+ },
220
+ {
221
+ "id": "decorative",
222
+ "title": "Decorative image (hidden from AT)",
223
+ "pithy": "<Image src=\"'/pattern.svg'\" alt=\"''\" />",
224
+ "assertions": ["alt=''", "role='presentation'"]
225
+ }
226
+ ]
227
+ }
@@ -0,0 +1,183 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.prose",
4
+ "name": "Prose",
5
+ "displayName": "Prose",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "content",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Long-form content wrapper for docs and articles. A thin semantic container that applies the design-system .u-prose utility (plus the .u-prose-{size} measure variant) and projects its children. All element-level typographic treatment — vertical rhythm, links, inline code, blockquote, tables, figures, description lists — lives in .u-prose in @pithyjs/design-system, not in the component, so the same treatment is available to consumers who use the utility class without the component.",
12
+ "props": [
13
+ {
14
+ "name": "content",
15
+ "label": "Content",
16
+ "editor": {
17
+ "tab": "content",
18
+ "multiline": true
19
+ },
20
+ "type": "string",
21
+ "required": false,
22
+ "placeholder": "Long-form body copy.",
23
+ "description": "Optional body, for callers that build the document in TypeScript rather than as projected children. The TypeScript API also accepts a Node or a { el, destroy } ui-kit component. Strings are inserted as text (never HTML) — avoids injection and stray {{ }} interpolation. Rendered AFTER any projected children; both are rendered, nothing is dropped. NOTE: `content` is plain text, so it cannot produce headings, paragraphs or lists — authoring a document through this field is not possible. That, plus a `slots.accepts` list that admits only CodeBlock/Callout/Image, is why `studioReady` is false: Prose is a developer-facing wrapper until it has a real builder authoring path (see BACKLOG #12)."
24
+ },
25
+ {
26
+ "name": "size",
27
+ "label": "Size",
28
+ "type": "enum",
29
+ "enum": ["sm", "md", "lg", "xl"],
30
+ "default": "md",
31
+ "description": "Reading measure and type size. 'md' (default) is the base .u-prose measure (65ch at --font-size-body) and emits NO variant class — .u-prose-md does not exist. 'sm' = 60ch/0.875rem, 'lg' = 70ch/1.125rem, 'xl' = 75ch/1.25rem."
32
+ },
33
+ {
34
+ "name": "as",
35
+ "label": "Element",
36
+ "editor": {
37
+ "tab": "settings"
38
+ },
39
+ "type": "enum",
40
+ "enum": ["div", "article", "section", "main", "aside"],
41
+ "default": "div",
42
+ "description": "Semantic element to render. Default 'div' contributes no implicit role, so a generic prose wrapper can never invent a spurious landmark or article region. Use 'article' when the content genuinely is a self-contained composition. Values outside this whitelist fall back to 'div' (a persisted document must not be able to choose the tag name)."
43
+ },
44
+ {
45
+ "name": "ariaLabel",
46
+ "label": "ARIA label",
47
+ "editor": {
48
+ "tab": "settings",
49
+ "visibleWhen": {
50
+ "prop": "as",
51
+ "in": ["article", "section", "main", "aside"]
52
+ }
53
+ },
54
+ "type": "string",
55
+ "required": false,
56
+ "description": "Accessible name for the container. ONLY applied when as is article | section | main | aside — the editor hides the field at any other `as` (visibleWhen), because the component drops it there and a saved value would persist in the document while the published page has no accessible name. At the default as='div' the element is role=generic, where ARIA-in-HTML prohibits aria-label and browsers/AT ignore it — the component drops it with a DEV warning rather than implying the region is named. Pair it with as='section' or as='aside', where a name is what promotes the element to a navigable landmark."
57
+ }
58
+ ],
59
+ "slots": [
60
+ {
61
+ "name": "default",
62
+ "displayName": "Document",
63
+ "description": "The primary authoring path: raw semantic HTML (h1-h6, p, ul/ol, blockquote, pre, table, figure, dl, hr, a, code, strong/em) and nested ui-kit components. MUST be named 'default': the block renderer only inlines the 'default' slot into the construction-time injection that <Prose> consumes via captureChildren() — a differently-named slot is wrapped in an inert <template data-slot=\"…\"> and, because the injection is still consumed, the projectRenderedSlots() fallback never runs and the content renders as nothing. Children render before the `content` prop. ui.heading and ui.text are deliberately NOT accepted: prose IS raw HTML, and routing headings through <Heading> would inject document hierarchy the author did not write — see ai.avoid, which this field must not contradict.",
64
+ "accepts": ["ui.code-block", "ui.callout", "ui.image"]
65
+ }
66
+ ],
67
+ "tokens": {
68
+ "measure": "65ch base; 60ch (sm) / 70ch (lg) / 75ch (xl) via .u-prose-{size} — set as max-inline-size",
69
+ "typography": "--font-size-body + --line-height-body (base), --font-weight-semibold (strong, dt, thead th), --font-family-mono (code, pre), --inline-code-font-size / --inline-code-padding-block / --inline-code-padding-inline / --inline-code-margin-inline (inline code chrome)",
70
+ "color": "--color-text (body, strong, dt, thead th), --color-text-muted (blockquote, figcaption, caption, dd), --color-primary + --color-primary-hover (links, blockquote accent), --color-surface-variant (inline code + pre fill), --color-border (code/pre/table rules), --color-divider (hr)",
71
+ "border": "--border-width-hairline (code, pre, table cells, hr), --border-width-thin (thead rule), --border-width-medium (blockquote accent bar)",
72
+ "radius": "--radius-xs (inline code), --radius-md (pre, images)",
73
+ "focus": "--focus-ring-width / --focus-ring-offset / --ring-color via the a11y focus-ring mixin on link :focus-visible",
74
+ "motion": "--motion-transition-color, --motion-duration-normal, --motion-easing-standard on link color/underline, inside prefers-reduced-motion: no-preference"
75
+ },
76
+ "spacing": {
77
+ "rhythm-unit": "em, not --space-* — every rhythm value is relative to the container's own font-size, so the sm/lg/xl measures get proportionally tighter/looser spacing for free. A rem/token scale would hold the rhythm fixed while the type grew.",
78
+ "heading-block-start": "1.5em (h2) / 1.25em (h3) / 1em (h4-h6) — a heading binds to the section it opens, not to the paragraph above it",
79
+ "heading-block-end": "0.75em (h1, h2) / 0.5em (h3-h6) — close friends, heading to its first line",
80
+ "paragraph-block-end": "1.25em (friends — sibling paragraphs in one flow)",
81
+ "list-item-block-end": "0.5em; nested lists 0.5em block (lovers — a sub-list belongs to its own bullet)",
82
+ "block-elements": "1.5em block margin on pre, blockquote, figure, img/video, table; 2.5em on hr (strangers — a rule separates whole passages)",
83
+ "container-edges": "the first/last flow child has its outer block margin zeroed, so the container's own box owns the outer spacing"
84
+ },
85
+ "constraints": [
86
+ "Purely presentational container — it adds no interactivity of its own; any links/controls come from the projected content",
87
+ "Element-level treatment lives in the design-system .u-prose utility, NOT in the component's SCSS — do not fork it into ui-kit",
88
+ "All descendant rules are wrapped in :where(), so they carry zero added specificity — a component's own rule wins on any property it declares",
89
+ "Descendant rules additionally skip any nested ui-kit component ROOT and its internals (matched prefix-anchored as [class^='ui-'] / [class*=' ui-'], scoped under .u-prose, with prose's own .ui-prose hook excepted): a component owns its own box, including the properties it never declares (border, radius, block margin, overflow). Both mechanisms are load-bearing — :where() alone does NOT prevent that bleed",
90
+ "A component that HOSTS a nested <Prose> does not exclude that prose's content (the descendant terms carry :not(:has(.u-prose))), so <Prose><Tabs><Prose> keeps the inner document's element rules; a component inside that inner prose still excludes its own internals",
91
+ "Add the .u-not-prose class to opt an arbitrary subtree out of prose treatment entirely. The opt-out is scoped UNDER .u-prose, so a <Prose> rendered inside a .u-not-prose region (a shared layout that knows nothing about the marker) is unaffected",
92
+ "pre code is explicitly reset so a code block never gets inline-code chrome on top of its own",
93
+ "String content is inserted as text (textContent/createTextNode), never HTML",
94
+ "as is whitelisted to div | article | section | main | aside; anything else falls back to div and DEV-warns",
95
+ "size='md' (and an omitted size) emit no .u-prose-* class — .u-prose-md does not exist in the design system",
96
+ "Children render before the content prop; both are rendered, neither is dropped",
97
+ "destroy() drains the projected-children disposer and any content component's destroy, then empties the element; a second call is a no-op"
98
+ ],
99
+ "a11y": [
100
+ "Default element is div — no implicit role, so a generic wrapper cannot invent a landmark or an article region inside a page that already has real ones",
101
+ "as='section' / 'aside' / 'main' / 'article' create nameable roles — pair them with ariaLabel so the landmark is named and navigable. ariaLabel is applied ONLY on those; on the default role=generic <div> it is dropped with a DEV warning, because ARIA-in-HTML prohibits it there",
102
+ "ariaLabel is trimmed; a whitespace-only value is ignored rather than producing an empty accessible name",
103
+ "Heading hierarchy is the author's: Prose never injects a heading and never renders <Heading>, so projected h1-h6 keep their document order",
104
+ "Links use the design-system link-underline treatment: a real text-decoration underline (never color alone, WCAG 1.4.1) plus the design-system focus ring on :focus-visible",
105
+ "Link color/underline transitions are wrapped in prefers-reduced-motion: no-preference",
106
+ "Forced-colors needs no override for prose's STATIC chrome: the mode substitutes system colors for every color/background-color/border-color prose sets, and every block (inline code, pre, blockquote, table cells) is drawn with a real border rather than fill alone, so each stays perceivable once fills are dropped. The link FOCUS RING is the exception and does need one — the a11y focus-ring mixin draws it as a box-shadow over `outline: none`, and forced-colors drops box-shadow without restoring the suppressed outline, so the link rule adds an explicit `outline: … solid Highlight` under (forced-colors: active) (WCAG 2.4.7). The prose rule cannot fall back to the global a:focus-visible outline: at (0,2,0) it outranks both the element-level a:focus-visible (0,1,1) and the global :focus-visible (0,1,0)"
107
+ ],
108
+ "ai": {
109
+ "synonyms": [
110
+ "prose",
111
+ "article body",
112
+ "long-form",
113
+ "rich text",
114
+ "markdown body",
115
+ "typography wrapper",
116
+ "readable content",
117
+ "docs body",
118
+ "measure",
119
+ "reading column"
120
+ ],
121
+ "avoid": [
122
+ "using <Prose> for a short label, caption or single line — use <Text>; Prose exists to set a reading measure and vertical rhythm over a whole document",
123
+ "wrapping app chrome (nav, toolbars, forms) in <Prose> — the element-level rhythm will fight the components' own spacing",
124
+ "re-styling elements inside prose from the page's own CSS — add .u-not-prose to the subtree instead",
125
+ "using <Heading> for the document's headings inside Prose — raw h1-h6 are correct here; prose IS raw HTML"
126
+ ],
127
+ "patterns": [
128
+ "Basic: <Prose><h2>Title</h2><p>Body copy.</p></Prose>",
129
+ "Semantic article: <Prose as=\"'article'\"><h1>Post title</h1><p>…</p></Prose>",
130
+ "Wider measure: <Prose size=\"'lg'\"><p>Longer reading column.</p></Prose>",
131
+ "Named landmark: <Prose as=\"'section'\" ariaLabel=\"'Release notes'\">…</Prose>",
132
+ "Opt a subtree out: <Prose><div class=\"u-not-prose\">…custom layout…</div></Prose>"
133
+ ]
134
+ },
135
+ "designGuidance": {
136
+ "surface": "The container itself is transparent — it inherits the page surface and sets only measure, base type size, line-height and text color. Visual weight comes from the elements inside it: inline code and pre sit on --color-surface-variant, blockquote is a muted, italic passage behind a --color-primary accent bar, tables are ruled with hairline --color-border cell borders.",
137
+ "depth": "Flat — no shadow anywhere. Prose is a reading surface; depth would compete with the text.",
138
+ "borders": "Hairline rules only, except two deliberate accents: the blockquote's --border-width-medium inline-start bar and the --border-width-thin rule under a thead. Images and pre share --radius-md; inline code takes --radius-xs so it stays a chip inside a text line.",
139
+ "interactivity": "Only links. They carry a real underline at rest (never color alone), shift to --color-primary-hover on hover/focus, and surface the design-system focus ring on :focus-visible. Everything else in prose is inert.",
140
+ "motion": "Static except the link color/underline transition, which is gated behind prefers-reduced-motion: no-preference.",
141
+ "bestPractices": "Reach for Prose whenever a block of authored content is longer than a couple of sentences: docs pages, blog posts, changelogs, rendered markdown, CMS bodies. Keep the default measure — 65ch is the readable band; go 'lg'/'xl' only for hero-weight editorial, 'sm' for dense reference material in a sidebar. Author the document with raw semantic HTML and let .u-prose do the styling; drop in ui-kit components (CodeBlock, Callout) freely — prose both loses on specificity (:where()) and skips their subtrees outright, so they keep their own chrome. If a subtree needs bespoke layout, mark it .u-not-prose rather than fighting the rhythm with page CSS."
142
+ },
143
+ "examples": [
144
+ {
145
+ "id": "default",
146
+ "title": "Document body",
147
+ "pithy": "<Prose><h2>Reactive signals</h2><p>Components never re-render.</p></Prose>",
148
+ "assertions": [
149
+ "renders <div class='ui-prose u-prose'>",
150
+ "no u-prose-* variant class at the default size",
151
+ "projected children are rendered inside the container"
152
+ ]
153
+ },
154
+ {
155
+ "id": "sizes",
156
+ "title": "Measure variants",
157
+ "pithy": "<Prose size=\"'lg'\"><p>A wider reading column.</p></Prose>",
158
+ "assertions": [
159
+ "sm/lg/xl add .u-prose-sm / .u-prose-lg / .u-prose-xl",
160
+ "md (and an omitted size) add no variant class"
161
+ ]
162
+ },
163
+ {
164
+ "id": "semantic-element",
165
+ "title": "Semantic element",
166
+ "pithy": "<Prose as=\"'article'\" ariaLabel=\"'Release notes'\"><h1>0.1.0</h1></Prose>",
167
+ "assertions": [
168
+ "renders an <article> element",
169
+ "aria-label is set from the trimmed ariaLabel",
170
+ "an unknown `as` falls back to <div>"
171
+ ]
172
+ },
173
+ {
174
+ "id": "text-content",
175
+ "title": "Text content prop",
176
+ "pithy": "<Prose content=\"'Plain body copy, inserted as text.'\" />",
177
+ "assertions": [
178
+ "content is inserted with textContent — <b>markup</b> stays literal",
179
+ "a { el, destroy } content handle mounts and its destroy is chained"
180
+ ]
181
+ }
182
+ ]
183
+ }
@@ -0,0 +1,158 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.stat",
4
+ "name": "Stat",
5
+ "displayName": "Stat",
6
+ "version": "0.1.0",
7
+ "level": "molecule",
8
+ "domain": "content",
9
+ "bidi": true,
10
+ "description": "Display statistic — a large value over a caption/label, with optional smaller supporting description. Non-interactive display primitive reusable across landing pages, dashboards, and marketing sections.",
11
+ "props": [
12
+ {
13
+ "name": "value",
14
+ "reactive": true,
15
+ "label": "Value",
16
+ "type": "string | Signal<string>",
17
+ "required": true,
18
+ "placeholder": "100",
19
+ "description": "The large display value (e.g. \"100\", \"0.8 KB\", \"AAA\"). Rendered prominently at --font-size-2xl."
20
+ },
21
+ {
22
+ "name": "label",
23
+ "reactive": true,
24
+ "label": "Label",
25
+ "type": "string | Signal<string>",
26
+ "required": true,
27
+ "placeholder": "Components",
28
+ "description": "Caption shown directly under the value. Body-sized text."
29
+ },
30
+ {
31
+ "name": "description",
32
+ "reactive": true,
33
+ "label": "Description",
34
+ "type": "string | Signal<string>",
35
+ "placeholder": "up 12% this month",
36
+ "description": "Optional smaller supporting text below the label."
37
+ },
38
+ {
39
+ "name": "color",
40
+ "label": "Color",
41
+ "editor": { "type": "token-color" },
42
+ "type": "enum",
43
+ "enum": [
44
+ "primary",
45
+ "secondary",
46
+ "tertiary",
47
+ "success",
48
+ "info",
49
+ "warning",
50
+ "danger"
51
+ ],
52
+ "description": "Semantic color for the value — maps to --color-* tokens via --_stat-value-color. Omit for the normal text color."
53
+ },
54
+ {
55
+ "name": "align",
56
+ "label": "Alignment",
57
+ "editor": { "tab": "layout" },
58
+ "type": "enum",
59
+ "enum": ["start", "center", "end"],
60
+ "default": "start",
61
+ "description": "Text/box alignment — logical values (start/center/end) for RTL support."
62
+ },
63
+ {
64
+ "name": "id",
65
+ "label": "Anchor ID",
66
+ "editor": { "tab": "settings" },
67
+ "type": "string",
68
+ "description": "Optional element id."
69
+ }
70
+ ],
71
+ "events": [],
72
+ "tokens": {
73
+ "value.font-size": "--font-size-2xl",
74
+ "value.font-weight": "--font-weight-bold",
75
+ "value.color": "--color-{color} (via --_stat-value-color; defaults to --color-text)",
76
+ "label.font-size": "--font-size-body",
77
+ "label.font-weight": "--font-weight-medium",
78
+ "description.font-size": "--font-size-sm",
79
+ "description.color": "--color-text-muted"
80
+ },
81
+ "spacing": {
82
+ "value-to-label": "--space-sm (close friends — value and label read as one unit)",
83
+ "label-to-description": "--space-xs (lovers — the caption's supporting note)"
84
+ },
85
+ "constraints": [
86
+ "value and label are required",
87
+ "Colors are semantic only (primary/secondary/tertiary/success/info/warning/danger) — omit for normal text color",
88
+ "Alignment uses logical values (start/center/end), never physical left/right",
89
+ "Reading order is fixed: value, then label, then description",
90
+ "Non-interactive — no hover/focus/click behavior"
91
+ ],
92
+ "a11y": [
93
+ "Renders plain display text in logical reading order (value → label → description)",
94
+ "No ARIA roles needed — it is static content, not a control",
95
+ "Forced-colors: value falls back to CanvasText for visibility",
96
+ "Color is decorative emphasis — the label carries the meaning, so semantic color is not the only signal"
97
+ ],
98
+ "ai": {
99
+ "synonyms": [
100
+ "stat",
101
+ "statistic",
102
+ "metric",
103
+ "kpi",
104
+ "number",
105
+ "figure",
106
+ "count",
107
+ "big number",
108
+ "data point"
109
+ ],
110
+ "avoid": [
111
+ "Badge for a statistic — Badge is a nowrap inline pill that clips long values; use Stat for a value-over-label block",
112
+ "raw font sizes — the value size comes from --font-size-2xl",
113
+ "hardcoded colors — use the color prop",
114
+ "physical text-align left/right — use align start/center/end"
115
+ ],
116
+ "patterns": [
117
+ "Plain stat: <Stat value=\"'100'\" label=\"'Components'\" />",
118
+ "With supporting text: <Stat value=\"'0.8 KB'\" label=\"'Core size'\" description=\"'min + gzip'\" />",
119
+ "Semantic color + centered: <Stat value=\"'AAA'\" label=\"'Accessibility'\" color=\"'success'\" align=\"'center'\" />"
120
+ ]
121
+ },
122
+ "designGuidance": {
123
+ "surface": "Transparent by default — Stat is a text block, not a card. Place several in a Grid/Stack for a stats row, or inside a Card for a contained metric.",
124
+ "depth": "Flat. No shadow or border; the value's size and optional color carry the emphasis.",
125
+ "borders": "None.",
126
+ "interactivity": "Non-interactive display content — no hover, focus, or click.",
127
+ "bestPractices": [
128
+ "Use for a single headline number with a short caption; keep the value terse (a number, size, grade)",
129
+ "Reach for the description only for a small qualifier (delta, unit, timeframe) — full sentences belong in Text",
130
+ "Semantic color is decorative emphasis; never rely on it alone to convey meaning — the label must stand on its own",
131
+ "In a row of stats, keep alignment consistent and lay them out with Grid/Stack, not manual margins"
132
+ ]
133
+ },
134
+ "examples": [
135
+ {
136
+ "id": "plain",
137
+ "title": "Plain stat",
138
+ "pithy": "<Stat value=\"'100'\" label=\"'Components'\" />",
139
+ "assertions": [
140
+ "has ui-stat class",
141
+ "value in .ui-stat-value",
142
+ "label in .ui-stat-label"
143
+ ]
144
+ },
145
+ {
146
+ "id": "with-description",
147
+ "title": "Stat with supporting description",
148
+ "pithy": "<Stat value=\"'0.8 KB'\" label=\"'Core size'\" description=\"'min + gzip'\" />",
149
+ "assertions": ["renders .ui-stat-description"]
150
+ },
151
+ {
152
+ "id": "colored-centered",
153
+ "title": "Semantic color, centered",
154
+ "pithy": "<Stat value=\"'AAA'\" label=\"'Accessibility'\" color=\"'success'\" align=\"'center'\" />",
155
+ "assertions": ["data-color='success'", "data-align='center'"]
156
+ }
157
+ ]
158
+ }