@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,150 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.toast",
4
+ "name": "Toast",
5
+ "displayName": "Toast",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "feedback",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Non-blocking notification. Imperative API (toast.show / success / error / warning / info) that stacks transient messages in a fixed corner region, portaled to <body>. Auto-dismisses after a duration, pauses on hover/focus, and supports an optional action button and close control.",
12
+ "props": [
13
+ {
14
+ "name": "message",
15
+ "label": "Message",
16
+ "type": "string",
17
+ "required": true,
18
+ "placeholder": "Changes saved",
19
+ "description": "The notification text. Conveyed with a variant icon so meaning never relies on color alone."
20
+ },
21
+ {
22
+ "name": "variant",
23
+ "label": "Variant",
24
+ "type": "enum",
25
+ "enum": ["info", "success", "warning", "danger"],
26
+ "default": "info",
27
+ "description": "Semantic variant — drives the leading icon, accent color, and live-region politeness (info/success are polite status; warning/danger are assertive alerts)."
28
+ },
29
+ {
30
+ "name": "duration",
31
+ "label": "Duration (ms)",
32
+ "editor": { "tab": "settings" },
33
+ "type": "number",
34
+ "description": "Auto-dismiss delay in milliseconds. 0 makes the toast sticky (never auto-dismisses). Intentionally has NO static default: omitted, it resolves to 5000 for a plain toast but to 0 (sticky) when `action` is set, so an Undo/Retry control cannot time out before a keyboard or screen-reader user reaches it (WCAG 2.2.1 Timing Adjustable). Do not seed an editor value of 5000 — writing an explicit duration defeats the sticky-with-action rule, since an explicit value is always honoured as given (including an explicit 5000 alongside an action). Leave it unset unless the author deliberately wants a fixed timeout. The timer pauses while the pointer is over the stack or a toast has focus."
35
+ },
36
+ {
37
+ "name": "dismissible",
38
+ "label": "Show close button",
39
+ "editor": { "tab": "settings" },
40
+ "type": "boolean",
41
+ "default": true,
42
+ "description": "Render the X close button that dismisses the toast on click."
43
+ },
44
+ {
45
+ "name": "action",
46
+ "label": "Action",
47
+ "editor": { "tab": "settings" },
48
+ "type": "{ label: string; onClick: () => void }",
49
+ "required": false,
50
+ "description": "Optional ghost action button (e.g. Undo). Runs onClick then dismisses the toast. Setting it without a duration makes the toast sticky by default — see `duration`."
51
+ }
52
+ ],
53
+ "tokens": {
54
+ "color": "--color-surface-bright (surface), --color-text / --color-text-muted, --color-border, --color-info / --color-success / --color-warning / --color-danger (accent; the icon pill tints it via color-mix at --state-selected-opacity)",
55
+ "spacing": "--space-md (padding), --space-sm (stack gap, icon gap), --space-lg (region insets, icon/close size)",
56
+ "radius": "--radius-lg (toast), --radius-md (icon pill, close button)",
57
+ "shadow": "--shadow-xl",
58
+ "border": "--border-width-hairline (idle), --border-width-thin (forced-colors)",
59
+ "layer": "--z-toast (stacking)",
60
+ "motion": "--motion-duration-normal, --motion-easing-entrance, --motion-easing-exit"
61
+ },
62
+ "spacing": {
63
+ "stack-gap": "--space-sm (friends — stacked toasts)",
64
+ "icon-message": "--space-sm (icon ↔ message)",
65
+ "region-inset": "--space-lg (gutter from the viewport corner)"
66
+ },
67
+ "constraints": [
68
+ "Imperative-only — created via toast.show()/sugar, not placed on the studio canvas (studioReady: false)",
69
+ "Portals to <body> in a single fixed region per toaster; the region is created lazily on first show()",
70
+ "SSR-safe: show() is a no-op that still returns an id when document is unavailable",
71
+ "duration 0 is sticky; otherwise the auto-dismiss timer pauses on hover/focus and resumes on leave/blur",
72
+ "An omitted duration resolves to sticky when action is set, and to 5000 otherwise; an explicitly passed duration always wins (an invalid negative one still falls back to 5000 rather than becoming sticky)",
73
+ "F6 is registered on the document only while at least one toast is live, once for the whole module regardless of how many toasters exist, and removed on the last dismissal and on destroy(). A region torn out of the DOM without destroy() is pruned on the next press, so the listener cannot outlive its regions",
74
+ "Meaning is carried by icon + text, never color alone",
75
+ "Variant glyphs resolve through the normal icon pipeline, so an app sprite registering the same generic names (info/check/alert/octagon-alert) overrides them. The visually-hidden severity prefix keeps the non-color-alone guarantee intact regardless, but an app overriding these names should keep the shapes visually distinct"
76
+ ],
77
+ "a11y": [
78
+ "Container is role='region' aria-label='Notifications' and is not itself a live region",
79
+ "The live region is scoped to the message body (.ui-toast-body), NOT the toast root, so the action/close controls are not swept into the announcement (role=status/alert imply aria-atomic)",
80
+ "The message body is populated on the next task (after it is in the DOM) so screen readers observe a live-region mutation and announce it — a region attached with content already inside is frequently not announced",
81
+ "danger/warning use role='alert' + aria-live='assertive'; info/success use role='status' + aria-live='polite'",
82
+ "Severity is spoken via a visually-hidden prefix (Information/Success/Warning/Error) inside the live region, since the icon is decorative and info/success (and warning/danger) otherwise share one role. The labels come from the shared feedback-tone foundation except danger, which Toast overrides to 'Error' so the announcement matches the toast.error() API rather than the colour token name",
83
+ "The leading variant icon is aria-hidden; the close button has an aria-label",
84
+ "Each close button is uniquely named after its own message — Dismiss “Upload failed” — so a screen-reader user can tell a stack of toasts apart. The message is whitespace-collapsed and clipped to 40 code points with a single-character ellipsis; double quotes inside it are folded to a typographic apostrophe so they can never be confused with the “…” delimiters. An empty message falls back to “Dismiss notification”",
85
+ "Two toasts carrying the SAME message are disambiguated with a trailing ordinal — Dismiss “Upload failed” (2) — resolved against the labels currently live. A dismissed toast frees its ordinal for reuse, and toasts already on screen are never renamed (renaming a button mid-read would be worse than the clash). Ordinals are per-toaster, matching the per-toaster region landmarks",
86
+ "F6 moves focus into the notifications region (the first control of a toast that is not leaving, else the region itself, which is tabindex=-1). The region is portaled to the end of <body>, so without the shortcut a keyboard or screen-reader user would have to Tab across the whole page — and an auto-dismissing toast would be gone before they arrived. When focus is already inside a region, F6 advances to the next region with live toasts (ARIA APG “cycle panes”), wrapping onto itself when it is the only one. Regions are visited in DOM order, not registration order",
87
+ "The region carries aria-keyshortcuts='F6', so the shortcut is exposed programmatically to assistive tech rather than living only in prose docs",
88
+ "F6 is consumed (preventDefault) only once focus has actually moved — a press that finds nothing to focus leaves the browser's native F6 behaviour intact",
89
+ "Known interaction — FocusTrap: @pithyjs/core's focus trap intercepts Tab on keydown and has no focusin re-capture, so once F6 has moved focus to a toast control outside an open dialog, a subsequent Tab can walk out of the modal. F6 is deliberately NOT suppressed while a trap is active — a user who gets an error toast while inside a modal is exactly who needs to reach it. The fix belongs in FocusTrap (tracked in docs/BACKLOG.md), not in a Toast-side suppression",
90
+ "F6 is deliberately NOT suppressed while a text field has focus: it is not a text-entry key, and skipping it there would defeat the primary use case — someone typing in a form who needs to reach an error toast",
91
+ "WCAG 2.2.1 (Timing Adjustable): an actionable toast defaults to no time limit rather than a longer floor. “Turn off” is the only 2.2.1 exception a toast can satisfy without extra UI — the “Extend” path requires warning the user and offering an extension, which a toast has no affordance for. Pause-on-hover/focus is a mitigation, not conformance",
92
+ "Enter/exit motion respects prefers-reduced-motion (instant baseline)",
93
+ "Forced-colors: explicit CanvasText border on the toast surface",
94
+ "Logical properties throughout so the region flips correctly in RTL"
95
+ ],
96
+ "ai": {
97
+ "synonyms": [
98
+ "toast",
99
+ "notification",
100
+ "snackbar",
101
+ "flash message",
102
+ "notice",
103
+ "alert banner"
104
+ ],
105
+ "avoid": [
106
+ "using a Toast for a blocking confirmation — use <AlertDialog> when the user must respond",
107
+ "long paragraphs — keep the message to a short, scannable line",
108
+ "relying on color alone to signal severity — the variant icon carries meaning",
109
+ "placing a Toast on the canvas — it is an imperative API, not a droppable block",
110
+ "pairing an action with a short duration — an Undo/Retry that auto-dismisses in a few seconds fails WCAG 2.2.1 (Timing Adjustable); omit duration so the toast stays sticky until the user acts"
111
+ ],
112
+ "patterns": [
113
+ "Success: toast.success('Changes saved')",
114
+ "Error with retry: toast.error('Upload failed', { action: { label: 'Retry', onClick: retry } }) — sticky by default because it carries an action",
115
+ "Undo pattern: toast.info('Page cleared', { action: { label: 'Undo', onClick: restore } })",
116
+ "Sticky: toast.warning('You are offline', { duration: 0 })"
117
+ ]
118
+ },
119
+ "designGuidance": {
120
+ "surface": "Raised --color-surface-bright surface with a hairline border, offset from the viewport corner.",
121
+ "depth": "Elevated with --shadow-xl to read as a transient layer above the page.",
122
+ "borders": "Hairline border; forced-colors swaps to a CanvasText border.",
123
+ "interactivity": "Auto-dismisses on a timer that pauses on hover/focus; optional action and close controls dismiss on click. A toast with an action stays put until the user acts, and F6 jumps focus into the region.",
124
+ "darkMode": "Adapts automatically via --color-* tokens; variant accents resolve per theme."
125
+ },
126
+ "examples": [
127
+ {
128
+ "id": "success",
129
+ "title": "Success toast",
130
+ "pithy": "toast.success('Changes saved')",
131
+ "assertions": [
132
+ "renders a <div class='ui-toast' data-variant='success'>",
133
+ "role='status'",
134
+ "aria-live='polite'"
135
+ ]
136
+ },
137
+ {
138
+ "id": "error-action",
139
+ "title": "Error toast with a retry action",
140
+ "pithy": "toast.error('Upload failed', { action: { label: 'Retry', onClick: retry } })",
141
+ "assertions": [
142
+ "data-variant='danger'",
143
+ "role='alert' and aria-live='assertive'",
144
+ "a ghost .ui-button action labelled 'Retry'",
145
+ "sticky — no duration was given and an action is set (WCAG 2.2.1)",
146
+ "the close button is named Dismiss “Upload failed”"
147
+ ]
148
+ }
149
+ ]
150
+ }
@@ -0,0 +1,314 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.dynamic-form",
4
+ "name": "DynamicForm",
5
+ "displayName": "Dynamic Form",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "forms",
9
+ "bidi": true,
10
+ "description": "Config-driven form organism. Renders form fields from a FormConfig object, auto-wiring each field to a shared signal-based FormState. Supports grid layout with column spans, conditional visibility via dependsOn, validation with error display, and async submit.",
11
+ "props": [
12
+ {
13
+ "name": "fields",
14
+ "label": "Fields",
15
+ "type": "FieldConfig[]",
16
+ "placeholder": [
17
+ { "name": "name", "type": "text", "label": "Name", "required": true },
18
+ {
19
+ "name": "email",
20
+ "type": "email",
21
+ "label": "Email",
22
+ "required": true
23
+ },
24
+ {
25
+ "name": "message",
26
+ "type": "textarea",
27
+ "label": "Message",
28
+ "required": true,
29
+ "span": "full"
30
+ }
31
+ ],
32
+ "description": "Editable field list. Each field maps to an input in the rendered form. Takes precedence over `config.fields`.",
33
+ "itemSchema": {
34
+ "name": {
35
+ "type": "string",
36
+ "required": true,
37
+ "description": "Unique field name — key in the submitted values."
38
+ },
39
+ "type": {
40
+ "type": "enum",
41
+ "enum": [
42
+ "text",
43
+ "email",
44
+ "password",
45
+ "number",
46
+ "tel",
47
+ "url",
48
+ "textarea",
49
+ "checkbox",
50
+ "toggle"
51
+ ],
52
+ "required": true,
53
+ "description": "Input type."
54
+ },
55
+ "label": {
56
+ "type": "string",
57
+ "required": true,
58
+ "description": "Visible label shown above the input."
59
+ },
60
+ "required": {
61
+ "type": "boolean",
62
+ "description": "Mark the field as required — auto-adds a 'X is required' validation rule."
63
+ },
64
+ "placeholder": {
65
+ "type": "string",
66
+ "description": "Ghost text shown inside the empty input."
67
+ }
68
+ }
69
+ },
70
+ {
71
+ "name": "submitLabel",
72
+ "label": "Submit label",
73
+ "type": "string",
74
+ "placeholder": "Send",
75
+ "description": "Submit button text. Overrides config.submitLabel; defaults to 'Send'."
76
+ },
77
+ {
78
+ "name": "submissionsEmail",
79
+ "label": "Submissions email",
80
+ "type": "string",
81
+ "description": "Where submissions are delivered. When set (and no Action URL), the form auto-wires a zero-backend destination via https://formsubmit.co/<email> — no server needed. The Action URL, if set, overrides this. Example: you@example.com"
82
+ },
83
+ {
84
+ "name": "config",
85
+ "label": "Config",
86
+ "editor": { "tab": "settings" },
87
+ "type": "FormConfig | (() => FormConfig)",
88
+ "required": false,
89
+ "description": "Advanced: full form configuration object for code users. In the studio, use Fields / Submit label / Submissions email instead. `fields` overrides `config.fields` when both are set."
90
+ },
91
+ {
92
+ "name": "onSubmit",
93
+ "type": "(values: Record<string, unknown>) => void | Promise<void>",
94
+ "required": false,
95
+ "description": "Called with current values when form is submitted and valid. Optional when `action` is set — native browser submission handles delivery."
96
+ },
97
+ {
98
+ "name": "columns",
99
+ "label": "Columns",
100
+ "editor": { "tab": "layout" },
101
+ "type": "number",
102
+ "default": 1,
103
+ "description": "Number of grid columns for field layout."
104
+ },
105
+ {
106
+ "name": "validateOn",
107
+ "label": "Validate on",
108
+ "editor": { "tab": "settings" },
109
+ "type": "enum",
110
+ "enum": ["change", "blur", "submit"],
111
+ "default": "submit",
112
+ "description": "When to run validation."
113
+ },
114
+ {
115
+ "name": "action",
116
+ "label": "Action URL",
117
+ "editor": { "tab": "settings" },
118
+ "type": "string",
119
+ "description": "Advanced override: raw native form action URL. Takes precedence over Submissions email. When set, the form posts via the browser after passing client-side validation (e.g. for a custom Formsubmit.co endpoint)."
120
+ },
121
+ {
122
+ "name": "method",
123
+ "label": "Method",
124
+ "editor": { "tab": "settings" },
125
+ "type": "enum",
126
+ "enum": ["GET", "POST"],
127
+ "default": "POST",
128
+ "description": "Native form method when `action` is set."
129
+ },
130
+ {
131
+ "name": "ariaLabel",
132
+ "label": "ARIA label",
133
+ "editor": { "tab": "settings" },
134
+ "type": "string",
135
+ "description": "Accessible label for the form (sets aria-label). Use when no visible heading labels the form."
136
+ },
137
+ {
138
+ "name": "ariaLabelledBy",
139
+ "label": "ARIA labelled by",
140
+ "editor": { "tab": "settings" },
141
+ "type": "string",
142
+ "description": "ID of an element that labels this form (sets aria-labelledby)."
143
+ }
144
+ ],
145
+ "events": ["submit"],
146
+ "slots": [],
147
+ "itemSchema": {
148
+ "FormConfig": {
149
+ "fields": {
150
+ "type": "FieldConfig[]",
151
+ "required": true,
152
+ "description": "Field definitions."
153
+ },
154
+ "submitLabel": {
155
+ "type": "string",
156
+ "default": "Send",
157
+ "description": "Submit button label."
158
+ },
159
+ "hiddenFields": {
160
+ "type": "Record<string, string>",
161
+ "description": "Static hidden inputs (e.g. Formsubmit.co _subject, _next, _captcha)."
162
+ },
163
+ "honeypot": {
164
+ "type": "boolean | string",
165
+ "description": "Add a honeypot spam-protection field. true → '_honey', or string for custom name. Filled value silently rejects submission."
166
+ }
167
+ },
168
+ "FieldConfig": {
169
+ "name": {
170
+ "type": "string",
171
+ "required": true,
172
+ "description": "Unique field name — key in form values."
173
+ },
174
+ "type": {
175
+ "type": "enum",
176
+ "enum": [
177
+ "text",
178
+ "email",
179
+ "password",
180
+ "number",
181
+ "tel",
182
+ "url",
183
+ "textarea",
184
+ "select",
185
+ "checkbox",
186
+ "radio",
187
+ "toggle"
188
+ ],
189
+ "required": true
190
+ },
191
+ "label": { "type": "string", "required": true },
192
+ "placeholder": { "type": "string" },
193
+ "autocomplete": {
194
+ "type": "string",
195
+ "description": "Autocomplete hint (e.g. 'name', 'email') — maps to the native HTML autocomplete attribute on <input>."
196
+ },
197
+ "required": { "type": "boolean" },
198
+ "disabled": { "type": "boolean" },
199
+ "options": {
200
+ "type": "array",
201
+ "items": { "label": "string", "value": "string" },
202
+ "description": "Options for select and radio fields."
203
+ },
204
+ "validation": { "type": "ValidationRule[]" },
205
+ "dependsOn": {
206
+ "type": "{ field: string; value: unknown }",
207
+ "description": "Show field only when the referenced field matches the given value."
208
+ },
209
+ "span": { "type": "number | 'full'", "description": "Grid column span." },
210
+ "helperText": { "type": "string" }
211
+ }
212
+ },
213
+ "tokens": {
214
+ "grid.gap": "--space-md",
215
+ "actions.gap": "--space-sm",
216
+ "actions.padding-top": "--space-sm"
217
+ },
218
+ "spacing": {
219
+ "internal": {
220
+ "field-to-field": { "relationship": "friends", "token": "md" },
221
+ "actions-padding-top": { "relationship": "close-friends", "token": "sm" }
222
+ }
223
+ },
224
+ "constraints": [
225
+ "All field atoms are imported and rendered imperatively — no template compilation",
226
+ "Each field is wrapped in FormField for label association (except radio/checkbox/toggle, which are self-labelling)",
227
+ "Conditional fields hidden via dependsOn are excluded from validateAll()",
228
+ "Grid columns set via data-columns='2|3|4' attribute; SCSS uses N-capped intrinsic-fluid formula (repeat(auto-fit, minmax(max($min-column, calc((100% - (N-1)*gap)/N)), 1fr))) so the grid renders exactly N tracks at wide container widths and collapses to fewer tracks (down to 1) as the container narrows — no viewport breakpoints, responds to container inline-size",
229
+ "Field spans via data-span='2|3|4' attribute are advisory: span N applies only when the auto-fit grid has ≥N tracks; at narrower widths the span silently clamps as the grid collapses",
230
+ "Actions row spans full grid width via grid-column: 1 / -1 and right-aligns the submit button via justify-content: flex-end",
231
+ "Required fields get auto-generated 'X is required' rule plus any custom validation",
232
+ "All CSS uses design system tokens",
233
+ "When `action` is set, the form posts natively via the browser; the JS submit handler always runs first to perform honeypot check, syncDomToState, and validateAll, but only calls preventDefault on validation failure, honeypot rejection, or in-flight submission",
234
+ "When `action` is set without `onSubmit`, the validation gate still runs — the browser only proceeds with the native POST after client-side checks pass",
235
+ "honeypot field is wrapped in a display:none container so browser autofill (Chrome, Safari, password managers) skips it; bots scraping the DOM still find and fill it",
236
+ "honeypot input has data-lpignore and data-form-type='other' as additional anti-autofill hints",
237
+ "honeypot rejection is silent — the form submission is blocked with no UI feedback (anti-bot)",
238
+ "syncDomToState() runs at submit before validateAll — captures all visible input/textarea/select DOM values into state, catching browser-autofilled values that bypassed input/change events"
239
+ ],
240
+ "a11y": [
241
+ "Native <form novalidate> element",
242
+ "aria-busy='true' when submitting",
243
+ "aria-live='polite' status region for error count announcement",
244
+ "Conditional fields hidden via dependsOn get aria-hidden='true' AND inert (real focus removal). This refers to dependsOn-hidden fields, NOT the hiddenFields metadata config (which renders <input type=\"hidden\"> elements that are inherently inaccessible).",
245
+ "All fields have associated labels via FormField or RadioGroup",
246
+ "Validation errors linked via aria-describedby on each atom",
247
+ "Submit button shows loading state during async submit",
248
+ "Supports ariaLabel and ariaLabelledBy props for accessible form identification"
249
+ ],
250
+ "ai": {
251
+ "synonyms": [
252
+ "dynamic form",
253
+ "config form",
254
+ "json form",
255
+ "form builder",
256
+ "auto form",
257
+ "form generator"
258
+ ],
259
+ "avoid": [
260
+ "Building forms manually when a config object is available — use DynamicForm",
261
+ "Using DynamicForm for highly custom layouts — use Form with manual wiring",
262
+ "Putting validation logic in onSubmit — use validation rules in FieldConfig"
263
+ ],
264
+ "patterns": [
265
+ "Contact form: fields with text/email/textarea + required validation",
266
+ "Settings form: toggles + select + dependsOn for conditional fields",
267
+ "Multi-column: columns=2, span='full' for wide fields like textarea",
268
+ "Conditional: dependsOn: { field: 'type', value: 'business' } shows company field",
269
+ "Formsubmit.co export: action='https://formsubmit.co/your@email', hiddenFields={ _subject, _next, _captcha }, honeypot=true"
270
+ ]
271
+ },
272
+ "examples": [
273
+ {
274
+ "id": "contact",
275
+ "title": "Contact form",
276
+ "code": "DynamicForm({ config: { fields: [{ name: 'name', type: 'text', label: 'Name', required: true }, { name: 'email', type: 'email', label: 'Email', required: true, validation: [{ type: 'email', message: 'Invalid email' }] }, { name: 'message', type: 'textarea', label: 'Message', span: 'full' }], submitLabel: 'Send' }, columns: 2, onSubmit: console.log })",
277
+ "assertions": [
278
+ "renders 3 fields",
279
+ "email field validates on submit",
280
+ "message spans full width",
281
+ "submit button labeled 'Send'"
282
+ ]
283
+ },
284
+ {
285
+ "id": "conditional",
286
+ "title": "Conditional field",
287
+ "code": "DynamicForm({ config: { fields: [{ name: 'type', type: 'select', label: 'Type', options: [{ label: 'Personal', value: 'personal' }, { label: 'Business', value: 'business' }] }, { name: 'company', type: 'text', label: 'Company', dependsOn: { field: 'type', value: 'business' } }] }, onSubmit: console.log })",
288
+ "assertions": [
289
+ "company field hidden initially",
290
+ "company appears when type=business",
291
+ "hidden field excluded from validation"
292
+ ]
293
+ },
294
+ {
295
+ "id": "formsubmit",
296
+ "title": "Formsubmit.co contact form (zero backend)",
297
+ "code": "DynamicForm({ config: { fields: [{ name: 'name', type: 'text', label: 'Name', required: true }, { name: 'email', type: 'email', label: 'Email', required: true }, { name: 'message', type: 'textarea', label: 'Message', required: true, span: 'full' }], submitLabel: 'Send', hiddenFields: { _subject: 'New contact', _next: 'https://example.com/thanks', _captcha: 'false' }, honeypot: true }, columns: 2, action: 'https://formsubmit.co/owner@example.com' })",
298
+ "assertions": [
299
+ "form has action=formsubmit.co",
300
+ "hidden inputs rendered for _subject/_next/_captcha",
301
+ "honeypot _honey field wrapped in display:none container",
302
+ "validation runs before native submission",
303
+ "filled honeypot silently rejects"
304
+ ]
305
+ }
306
+ ],
307
+ "designGuidance": {
308
+ "surface": "No surface styling on the form itself — child atoms provide all visual feedback.",
309
+ "depth": "None — transparent container.",
310
+ "borders": "None.",
311
+ "interactivity": "Submit button uses Button solid/primary variant with loading state during async submit. data-submitting on form reduces opacity and blocks pointer events.",
312
+ "bestPractices": "Use DynamicForm for AI-generated or config-driven forms. Use columns for side-by-side layout. Use span='full' for fields that need full width (textarea, long text). Use dependsOn for conditional visibility. Always set required on mandatory fields — auto-generates the 'X is required' validation rule."
313
+ }
314
+ }
@@ -0,0 +1,135 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.form",
4
+ "name": "Form",
5
+ "displayName": "Form",
6
+ "version": "0.1.0",
7
+ "level": "organism",
8
+ "domain": "forms",
9
+ "studioReady": false,
10
+ "bidi": true,
11
+ "description": "Semantic <form> organism with signal-based state engine. Creates a <form novalidate> element, manages submit lifecycle (validate, submit, complete), and exposes state on the return object for manual wiring to child form atoms. No magic context injection — developers wire fields explicitly via state.fieldValue(), state.fieldHasError(), etc.",
12
+ "props": [
13
+ {
14
+ "name": "onSubmit",
15
+ "type": "(values: Record<string, unknown>) => void | Promise<void>",
16
+ "required": true,
17
+ "description": "Called with current values when form is submitted and valid."
18
+ },
19
+ {
20
+ "name": "initialValues",
21
+ "label": "Initial values",
22
+ "type": "Record<string, unknown>",
23
+ "default": {},
24
+ "description": "Initial field values."
25
+ },
26
+ {
27
+ "name": "validateOn",
28
+ "label": "Validate on",
29
+ "editor": { "tab": "settings" },
30
+ "type": "enum",
31
+ "enum": ["change", "blur", "submit"],
32
+ "default": "submit",
33
+ "description": "When to run validation — on every value change, on field blur, or only on submit."
34
+ },
35
+ {
36
+ "name": "validationRules",
37
+ "label": "Validation rules",
38
+ "editor": { "tab": "settings" },
39
+ "type": "Record<string, ValidationRule[]>",
40
+ "default": {},
41
+ "description": "Validation rules keyed by field name."
42
+ },
43
+ {
44
+ "name": "ariaLabel",
45
+ "label": "ARIA label",
46
+ "editor": { "tab": "settings" },
47
+ "type": "string",
48
+ "description": "Accessible label for the form (sets aria-label). Use when no visible heading labels the form."
49
+ },
50
+ {
51
+ "name": "ariaLabelledBy",
52
+ "label": "ARIA labelled by",
53
+ "editor": { "tab": "settings" },
54
+ "type": "string",
55
+ "description": "ID of an element that labels this form (sets aria-labelledby)."
56
+ }
57
+ ],
58
+ "events": ["submit"],
59
+ "slots": [
60
+ {
61
+ "name": "default",
62
+ "displayName": "Form Fields",
63
+ "description": "Form contents — typically Input/Select/Textarea/Checkbox/Radio/Toggle atoms wired to the exposed FormState via fieldValue/fieldHasError/setValue/setTouched. Children are projected via renderChildren() and appended inside the <form> element."
64
+ }
65
+ ],
66
+ "tokens": {
67
+ "submitting.opacity": "--state-disabled-opacity",
68
+ "submitting.transition": "--motion-duration-normal + --motion-easing-standard"
69
+ },
70
+ "spacing": {
71
+ "internal": {},
72
+ "external": {
73
+ "note": "Form is a container — spacing between child fields is the caller's responsibility (use Stack gap or grid layout)."
74
+ }
75
+ },
76
+ "constraints": [
77
+ "Always renders <form novalidate> — custom validation replaces native",
78
+ "No context injection — state exposed via return object",
79
+ "Children appended by caller — Form does not render fields",
80
+ "Double-submit prevention via isSubmitting guard",
81
+ "Async onSubmit is awaited before clearing isSubmitting",
82
+ "validateAll() must pass before onSubmit is called",
83
+ "FormState.fieldValue/fieldHasError/fieldErrorMessage return signal accessors compatible with atom props",
84
+ "All CSS uses design system tokens"
85
+ ],
86
+ "a11y": [
87
+ "Native <form> element with novalidate (custom validation)",
88
+ "aria-busy='true' when submitting",
89
+ "aria-live='polite' status region announces error count on failed submit",
90
+ "Status region uses u-sr-only (screen-reader only)",
91
+ "Error count announced: 'Form has N error(s). Please correct and try again.'",
92
+ "Supports ariaLabel and ariaLabelledBy props for accessible form identification"
93
+ ],
94
+ "ai": {
95
+ "synonyms": [
96
+ "form",
97
+ "form wrapper",
98
+ "form container",
99
+ "form state",
100
+ "form engine"
101
+ ],
102
+ "avoid": [
103
+ "Using Form without wiring state to child atoms — fields won't be reactive",
104
+ "Putting validation logic directly in onSubmit — use validationRules",
105
+ "Relying on native browser validation — Form sets novalidate",
106
+ "Using Form for config-driven forms — use DynamicForm instead"
107
+ ],
108
+ "patterns": [
109
+ "Manual wiring: form.state.fieldValue('email') as Input value prop",
110
+ "Validation: validationRules: { email: [{ type: 'required', message: 'Required' }] }",
111
+ "Submit: onSubmit: async (values) => await api.save(values)",
112
+ "Reset: form.state.reset()"
113
+ ]
114
+ },
115
+ "examples": [
116
+ {
117
+ "id": "basic",
118
+ "title": "Basic form with manual wiring",
119
+ "code": "const form = Form({ onSubmit: console.log, initialValues: { email: '' }, validationRules: { email: [{ type: 'required', message: 'Required' }] } }); const input = Input({ label: 'Email', value: form.state.fieldValue('email'), error: form.state.fieldHasError('email'), errorMessage: form.state.fieldErrorMessage('email'), onInput: (v) => form.state.setValue('email', v), onBlur: () => form.state.setTouched('email') }); form.el.appendChild(input.el);",
120
+ "assertions": [
121
+ "renders <form novalidate>",
122
+ "exposes state object",
123
+ "submit calls onSubmit with values",
124
+ "no axe violations"
125
+ ]
126
+ }
127
+ ],
128
+ "designGuidance": {
129
+ "surface": "Form has no visual styling — it is a semantic container. Child atoms provide all visual feedback.",
130
+ "depth": "None — Form is transparent.",
131
+ "borders": "None.",
132
+ "interactivity": "data-submitting attribute reduces opacity and disables pointer events during async submit. Transition respects prefers-reduced-motion.",
133
+ "bestPractices": "Use Form for developer-composed forms where you need full control over field layout and wiring. Use DynamicForm for config-driven forms. Always provide validationRules rather than validating inside onSubmit."
134
+ }
135
+ }