@elabs-ai/components-ui 4.0.0 → 4.1.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 (223) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-JKPPTL63.js +48 -0
  3. package/dist/chunk-JKPPTL63.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2485 -167
  8. package/dist/index.js +10716 -2706
  9. package/dist/index.js.map +1 -1
  10. package/package.json +21 -7
  11. package/src/_peer-deps.test.ts +59 -0
  12. package/src/blocks-comparison-table.stories.tsx +2 -2
  13. package/src/components/accordion/accordion.tsx +1 -1
  14. package/src/components/advanced-group/advanced-group.tsx +1 -1
  15. package/src/components/alert/alert.tsx +4 -1
  16. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  17. package/src/components/app-shell/app-shell.tsx +35 -2
  18. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  19. package/src/components/attribution-panel/attribution-panel.tsx +1 -1
  20. package/src/components/attribution-panel/attributions.generated.ts +81 -77
  21. package/src/components/badge/badge.stories.tsx +8 -0
  22. package/src/components/badge/badge.tsx +1 -1
  23. package/src/components/bento-grid/bento-grid.test.tsx +1 -1
  24. package/src/components/bento-grid/bento-grid.tsx +1 -1
  25. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  26. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  27. package/src/components/breadcrumb/breadcrumb.tsx +7 -2
  28. package/src/components/button/button.stories.tsx +116 -8
  29. package/src/components/button/button.tsx +1 -1
  30. package/src/components/card/card.stories.tsx +11 -6
  31. package/src/components/card/card.tsx +1 -1
  32. package/src/components/change-review/change-review.stories.tsx +145 -1
  33. package/src/components/change-review/change-review.test.tsx +143 -0
  34. package/src/components/change-review/change-review.tsx +185 -4
  35. package/src/components/checkbox/checkbox.tsx +1 -1
  36. package/src/components/color-picker/color-picker.tsx +6 -5
  37. package/src/components/command/command.stories.tsx +16 -6
  38. package/src/components/command/command.test.tsx +73 -0
  39. package/src/components/command/command.tsx +30 -7
  40. package/src/components/command/index.ts +1 -0
  41. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  42. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  43. package/src/components/command-trigger/command-trigger.tsx +70 -0
  44. package/src/components/command-trigger/index.ts +1 -0
  45. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  46. package/src/components/context-rail/context-rail.test.tsx +263 -0
  47. package/src/components/context-rail/context-rail.tsx +678 -0
  48. package/src/components/context-rail/index.ts +1 -0
  49. package/src/components/copyable-value/copyable-value.tsx +1 -1
  50. package/src/components/dialog/dialog.tsx +2 -2
  51. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  52. package/src/components/empty-state/empty-state.test.tsx +26 -0
  53. package/src/components/empty-state/empty-state.tsx +6 -1
  54. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  55. package/src/components/field/field-context.ts +59 -0
  56. package/src/components/field/field.stories.tsx +142 -0
  57. package/src/components/field/field.test.tsx +415 -0
  58. package/src/components/field/field.tsx +340 -0
  59. package/src/components/field/index.ts +9 -0
  60. package/src/components/field-row/field-row.stories.tsx +85 -1
  61. package/src/components/field-row/field-row.test.tsx +145 -0
  62. package/src/components/field-row/field-row.tsx +48 -6
  63. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  64. package/src/components/file-upload/file-upload.test.tsx +7 -2
  65. package/src/components/file-upload/file-upload.tsx +15 -4
  66. package/src/components/form/form.tsx +5 -2
  67. package/src/components/icon-button/icon-button.test.tsx +1 -1
  68. package/src/components/icon-button/icon-button.tsx +2 -2
  69. package/src/components/input/input.stories.tsx +121 -0
  70. package/src/components/input/input.test.tsx +13 -0
  71. package/src/components/input/input.tsx +2 -2
  72. package/src/components/input-group/input-group.stories.tsx +74 -0
  73. package/src/components/input-group/input-group.tsx +13 -4
  74. package/src/components/input-otp/input-otp.tsx +5 -1
  75. package/src/components/keyboard-shortcuts/index.ts +6 -0
  76. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  77. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  78. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  79. package/src/components/link-preview/link-preview.tsx +1 -1
  80. package/src/components/locale-provider/index.ts +6 -1
  81. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  82. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  83. package/src/components/locale-provider/locale-provider.tsx +63 -8
  84. package/src/components/locale-provider/messages.ts +501 -2
  85. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  86. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  87. package/src/components/mention-input/mention-input.tsx +8 -38
  88. package/src/components/mention-input/mention-value.ts +8 -2
  89. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  90. package/src/components/metric-card/metric-card.tsx +21 -2
  91. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  92. package/src/components/model-picker/model-picker.test.tsx +63 -0
  93. package/src/components/model-picker/model-picker.tsx +62 -36
  94. package/src/components/nav-main/nav-main.tsx +1 -1
  95. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  96. package/src/components/nav-notifications/nav-notifications.tsx +57 -5
  97. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  98. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  99. package/src/components/navigation-menu/navigation-menu.tsx +189 -12
  100. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  101. package/src/components/page-shell/page-shell.test.tsx +229 -0
  102. package/src/components/page-shell/page-shell.tsx +108 -25
  103. package/src/components/pagination/pagination.test.tsx +90 -0
  104. package/src/components/pagination/pagination.tsx +9 -5
  105. package/src/components/radio-group/radio-group.tsx +1 -1
  106. package/src/components/rating/rating.tsx +1 -1
  107. package/src/components/resizable/resizable.stories.tsx +114 -2
  108. package/src/components/resizable/resizable.tsx +1 -1
  109. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  110. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  111. package/src/components/revision-timeline/revision-timeline.tsx +46 -3
  112. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  113. package/src/components/schema-form/from-json-schema.ts +375 -0
  114. package/src/components/schema-form/index.ts +81 -0
  115. package/src/components/schema-form/schema-form-spec.ts +698 -0
  116. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  117. package/src/components/schema-form/schema-form.test.tsx +900 -0
  118. package/src/components/schema-form/schema-form.tsx +1545 -0
  119. package/src/components/section-header/section-header.stories.tsx +37 -0
  120. package/src/components/section-header/section-header.tsx +12 -1
  121. package/src/components/select/select.tsx +6 -2
  122. package/src/components/sheet/sheet.tsx +1 -1
  123. package/src/components/side-dock/index.ts +1 -0
  124. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  125. package/src/components/side-dock/side-dock.test.tsx +273 -0
  126. package/src/components/side-dock/side-dock.tsx +502 -0
  127. package/src/components/sidebar/index.ts +1 -0
  128. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  129. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  130. package/src/components/sidebar/sidebar.test.tsx +36 -1
  131. package/src/components/sidebar/sidebar.tsx +245 -33
  132. package/src/components/skip-link/index.ts +1 -0
  133. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  134. package/src/components/skip-link/skip-link.test.tsx +20 -0
  135. package/src/components/skip-link/skip-link.tsx +34 -0
  136. package/src/components/slider/slider.tsx +1 -1
  137. package/src/components/split-panel/index.ts +6 -1
  138. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  139. package/src/components/split-panel/split-panel.test.tsx +20 -0
  140. package/src/components/split-panel/split-panel.tsx +28 -8
  141. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  142. package/src/components/state-panel/state-panel.test.tsx +125 -0
  143. package/src/components/state-panel/state-panel.tsx +96 -17
  144. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  145. package/src/components/switch/switch.tsx +1 -1
  146. package/src/components/table/table.stories.tsx +62 -0
  147. package/src/components/table/table.test.tsx +142 -1
  148. package/src/components/table/table.tsx +60 -1
  149. package/src/components/tabs/tabs.tsx +8 -2
  150. package/src/components/tag-input/tag-input.tsx +3 -2
  151. package/src/components/team-switcher/team-switcher.tsx +34 -5
  152. package/src/components/textarea/textarea.tsx +1 -1
  153. package/src/components/theme-switcher/theme-switcher.tsx +6 -2
  154. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  155. package/src/components/timeline/timeline.stories.tsx +27 -0
  156. package/src/components/timeline/timeline.tsx +11 -1
  157. package/src/components/toggle/toggle.tsx +1 -1
  158. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  159. package/src/components/tree/tree.stories.tsx +2 -2
  160. package/src/components/tree/tree.tsx +14 -9
  161. package/src/components/typography/prose.tsx +1 -1
  162. package/src/components/typography/typography.stories.tsx +228 -0
  163. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  164. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  165. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  166. package/src/components/virtual-select/virtual-select.test.tsx +1 -1
  167. package/src/components/wizard/wizard.tsx +1 -1
  168. package/src/components/workspace-picker/index.ts +3 -0
  169. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  170. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  171. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  172. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  173. package/src/illustrations/empty-list-illustration.tsx +26 -0
  174. package/src/illustrations/error-illustration.tsx +60 -0
  175. package/src/illustrations/first-run-illustration.tsx +27 -0
  176. package/src/illustrations/illustration-base.tsx +108 -0
  177. package/src/illustrations/illustrations.stories.tsx +94 -0
  178. package/src/illustrations/illustrations.test.tsx +98 -0
  179. package/src/illustrations/index.ts +16 -0
  180. package/src/illustrations/no-access-illustration.tsx +45 -0
  181. package/src/illustrations/no-results-illustration.tsx +46 -0
  182. package/src/illustrations/offline-illustration.tsx +37 -0
  183. package/src/illustrations/success-illustration.tsx +40 -0
  184. package/src/index.ts +75 -2
  185. package/src/lib/agent-event-model.test.ts +12 -0
  186. package/src/lib/agent-event-model.ts +42 -0
  187. package/src/lib/approval-option.test.ts +17 -0
  188. package/src/lib/approval-option.ts +53 -0
  189. package/src/lib/check-result.ts +33 -0
  190. package/src/lib/diff-line.test.ts +36 -0
  191. package/src/lib/diff-line.ts +63 -0
  192. package/src/lib/diff-rows.test.ts +76 -0
  193. package/src/lib/diff-rows.ts +92 -0
  194. package/src/lib/format-duration.test.ts +22 -0
  195. package/src/lib/format-duration.ts +30 -0
  196. package/src/lib/has-renderable-content.test.ts +49 -0
  197. package/src/lib/has-renderable-content.ts +32 -0
  198. package/src/lib/operating-mode.test.ts +36 -0
  199. package/src/lib/operating-mode.ts +81 -0
  200. package/src/lib/optional-peer.ts +59 -0
  201. package/src/lib/session-launch.ts +37 -0
  202. package/src/lib/slash-command.test.ts +44 -0
  203. package/src/lib/slash-command.ts +45 -0
  204. package/src/lib/trigger-query.test.ts +94 -0
  205. package/src/lib/trigger-query.ts +88 -0
  206. package/src/lib/use-mobile.ts +12 -6
  207. package/src/motion.stories.tsx +6 -0
  208. package/src/templates-object-detail-hub.stories.tsx +2 -2
  209. package/src/templates-screen-states.stories.tsx +5 -5
  210. package/src/templates-settings.stories.tsx +1 -1
  211. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  212. package/src/blocks/sidebar-02/logo.tsx +0 -18
  213. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  214. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  215. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  216. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  217. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  218. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  219. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  220. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  221. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  222. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  223. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
@@ -0,0 +1,375 @@
1
+ /**
2
+ * from-json-schema.ts — `fromJsonSchema()`: the narrow, documented JSON
3
+ * Schema → `FormSpec` adapter (issue #22, maintainer ruling 2026-09-01).
4
+ *
5
+ * Many consumers already have their config described as JSON Schema (an
6
+ * OpenAPI request body, a connector manifest). This maps ONLY the common
7
+ * subset such schemas actually use onto `SchemaForm`'s `FieldSpec`
8
+ * vocabulary:
9
+ *
10
+ * { type: "string" } → StringFieldSpec
11
+ * { type: "string", enum: [...] } → EnumFieldSpec
12
+ * { type: "number" | "integer" } → NumberFieldSpec / IntegerFieldSpec
13
+ * { type: "boolean" } → BooleanFieldSpec
14
+ * { type: "array", items: { type: "string" } } → ListFieldSpec (free text)
15
+ * { type: "array",
16
+ * items: { type: "string", enum: [...] } } → MultiEnumFieldSpec
17
+ * (a CONSTRAINED array
18
+ * item schema maps onto
19
+ * the field type that
20
+ * actually enforces the
21
+ * constraint — see
22
+ * "Constraint keywords"
23
+ * below for why this
24
+ * one keyword IS read)
25
+ * { type: "object", properties: {...} } → GroupFieldSpec
26
+ * (variant: "advanced",
27
+ * one always-open branch)
28
+ *
29
+ * `title`/`description` map to `label`/`description`; `required` (the
30
+ * enclosing object schema's array of property names) maps to `required:
31
+ * true`; `default` maps when it is type-compatible; `minLength`/`maxLength`/
32
+ * `pattern`/`minimum`/`maximum`/`minItems`/`maxItems` map to their FieldSpec
33
+ * equivalents. `format` maps only the four values `StringFieldSpec` itself
34
+ * understands (`email`/`uri`/`date`/`date-time`) — any other `format` value
35
+ * (`"binary"`, `"password"`, a custom one) is ignored predictably, the same
36
+ * way an unknown JSON Schema `format` is defined to be advisory, not
37
+ * validating.
38
+ *
39
+ * Deliberately NOT a general draft-07/2020-12 implementation (the maintainer
40
+ * ruling's explicit narrow option) — this module does not resolve `$ref` and
41
+ * does not implement the `allOf`/`oneOf`/`anyOf`/`not` combinators. Those
42
+ * keywords change what a schema actually MEANS, and approximating them would
43
+ * risk producing a field that looks plausible but is wrong (e.g. rendering a
44
+ * `$ref`'d object as an empty string field) — silently wrong is exactly what
45
+ * this adapter must never be. So `fromJsonSchema()` REFUSES: any of those
46
+ * five keywords, found anywhere in the input (top-level or nested inside a
47
+ * property/items schema), throws `UnsupportedJsonSchemaError` naming the
48
+ * keyword and its JSON-Pointer-style path. Pre-resolve/inline that part of
49
+ * the schema before calling `fromJsonSchema()`, or hand-write the affected
50
+ * `FieldSpec`.
51
+ *
52
+ * A property whose `type` this subset does not cover (e.g. `"null"`, a
53
+ * JSON-Schema-2020-12 array-of-types, an array of non-string items, an
54
+ * object schema with no `properties`) is DROPPED from the result —
55
+ * predictable and non-fatal, the same lenient-parse philosophy
56
+ * `normalizeFormSpec` already uses for the render path, because omitting one
57
+ * field is recoverable in a way a mis-mapped `$ref`/combinator is not.
58
+ *
59
+ * CONSTRAINT KEYWORDS this subset does not read: `const`, `if`/`then`/`else`,
60
+ * `dependentSchemas`, `dependentRequired`, `additionalProperties`,
61
+ * `patternProperties`, `propertyNames`, `contains`, `multipleOf`,
62
+ * `exclusiveMinimum`/`exclusiveMaximum`, `uniqueItems`. These are
63
+ * READ-IGNORED, not refused — unlike `$ref`/`allOf`/`oneOf`/`anyOf`/`not`,
64
+ * none of them changes what TYPE a property maps to, so ignoring one never
65
+ * produces a wrongly-shaped field. But ignoring one DOES widen what the
66
+ * rendered form accepts relative to the source schema (a `const`-pinned
67
+ * property renders as an ordinary editable field, not a fixed one; a
68
+ * `multipleOf`-constrained number accepts any value in range). This is the
69
+ * one place `fromJsonSchema()` is honestly silently-permissive rather than
70
+ * silently-wrong or refusing — call it out in review when a schema leans on
71
+ * one of these keywords for validation the mapped form will not enforce.
72
+ * `items.enum` is the one exception among these: a constrained array-item
73
+ * schema is common enough (and cheap enough to map faithfully) that it reads
74
+ * `items.enum` and maps onto `MultiEnumFieldSpec` — see the table above —
75
+ * rather than joining this ignore list and widening into a free-text
76
+ * `ListFieldSpec`.
77
+ *
78
+ * Widening this subset later (more `format`s, `$ref` resolution) is an
79
+ * additive, non-breaking change to this one function — see issue #22's
80
+ * "worth having alongside" list.
81
+ */
82
+
83
+ import type { FieldSpec, FormSpec, FormValues } from "./schema-form-spec";
84
+
85
+ /**
86
+ * The JSON Schema keywords this adapter refuses to approximate. Each changes
87
+ * a schema's actual shape in a way a narrow field-by-field mapping cannot
88
+ * safely ignore.
89
+ */
90
+ const UNSUPPORTED_COMBINATORS = ["$ref", "allOf", "oneOf", "anyOf", "not"] as const;
91
+
92
+ /**
93
+ * Thrown by `fromJsonSchema()` when the input uses a JSON Schema keyword
94
+ * outside its documented subset (see this module's doc comment) — never
95
+ * silently mismapped.
96
+ */
97
+ export class UnsupportedJsonSchemaError extends Error {
98
+ constructor(keyword: string, path: string) {
99
+ super(
100
+ `fromJsonSchema: unsupported JSON Schema keyword "${keyword}" at "${path}". ` +
101
+ "fromJsonSchema() maps only a narrow, documented subset (string/number/" +
102
+ "integer/boolean/enum/array-of-string/object) and does not resolve " +
103
+ "$ref or implement allOf/oneOf/anyOf/not. Pre-resolve or inline this " +
104
+ "part of the schema before calling fromJsonSchema(), or build the " +
105
+ "affected FieldSpec by hand.",
106
+ );
107
+ this.name = "UnsupportedJsonSchemaError";
108
+ }
109
+ }
110
+
111
+ type JsonSchemaObject = Record<string, unknown>;
112
+
113
+ function isPlainObject(value: unknown): value is JsonSchemaObject {
114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
115
+ }
116
+
117
+ /** Recursively asserts none of `UNSUPPORTED_COMBINATORS` appears anywhere under `schema`. */
118
+ function assertSupported(schema: unknown, path: string): void {
119
+ if (!isPlainObject(schema)) return;
120
+ for (const keyword of UNSUPPORTED_COMBINATORS) {
121
+ if (keyword in schema) throw new UnsupportedJsonSchemaError(keyword, path);
122
+ }
123
+ if (isPlainObject(schema.properties)) {
124
+ for (const [key, sub] of Object.entries(schema.properties)) {
125
+ assertSupported(sub, `${path}/properties/${key}`);
126
+ }
127
+ }
128
+ if ("items" in schema) {
129
+ assertSupported(schema.items, `${path}/items`);
130
+ }
131
+ }
132
+
133
+ /** `values` narrowed to `string[]`, or `null` if any entry isn't a string. */
134
+ function stringArray(values: unknown): string[] | null {
135
+ if (!Array.isArray(values) || !values.every((v) => typeof v === "string")) return null;
136
+ return values as string[];
137
+ }
138
+
139
+ const SUPPORTED_STRING_FORMATS = new Set(["email", "uri", "date", "date-time"]);
140
+
141
+ function mapProperty(name: string, schema: unknown, required: boolean): FieldSpec | null {
142
+ if (!isPlainObject(schema)) return null;
143
+
144
+ const title = typeof schema.title === "string" ? schema.title : undefined;
145
+ const description = typeof schema.description === "string" ? schema.description : undefined;
146
+ const base = {
147
+ name,
148
+ ...(title !== undefined ? { label: title } : {}),
149
+ ...(description !== undefined ? { description } : {}),
150
+ ...(required ? { required: true as const } : {}),
151
+ };
152
+
153
+ switch (schema.type) {
154
+ case "string": {
155
+ const enumValues = stringArray(schema.enum);
156
+ if (enumValues) {
157
+ return {
158
+ type: "enum",
159
+ ...base,
160
+ options: enumValues,
161
+ ...(typeof schema.default === "string" ? { default: schema.default } : {}),
162
+ };
163
+ }
164
+ const format =
165
+ typeof schema.format === "string" && SUPPORTED_STRING_FORMATS.has(schema.format)
166
+ ? (schema.format as "email" | "uri" | "date" | "date-time")
167
+ : undefined;
168
+ return {
169
+ type: "string",
170
+ ...base,
171
+ ...(format !== undefined ? { format } : {}),
172
+ ...(Number.isInteger(schema.minLength) && (schema.minLength as number) >= 0
173
+ ? { minLength: schema.minLength as number }
174
+ : {}),
175
+ ...(Number.isInteger(schema.maxLength) && (schema.maxLength as number) >= 0
176
+ ? { maxLength: schema.maxLength as number }
177
+ : {}),
178
+ ...(typeof schema.pattern === "string" ? { pattern: schema.pattern } : {}),
179
+ ...(typeof schema.default === "string" ? { default: schema.default } : {}),
180
+ };
181
+ }
182
+
183
+ case "number": {
184
+ const validDefault = typeof schema.default === "number";
185
+ return {
186
+ type: "number",
187
+ ...base,
188
+ ...(typeof schema.minimum === "number" ? { min: schema.minimum } : {}),
189
+ ...(typeof schema.maximum === "number" ? { max: schema.maximum } : {}),
190
+ ...(validDefault ? { default: schema.default as number } : {}),
191
+ };
192
+ }
193
+
194
+ case "integer": {
195
+ const validDefault = typeof schema.default === "number" && Number.isInteger(schema.default);
196
+ return {
197
+ type: "integer",
198
+ ...base,
199
+ ...(typeof schema.minimum === "number" ? { min: schema.minimum } : {}),
200
+ ...(typeof schema.maximum === "number" ? { max: schema.maximum } : {}),
201
+ ...(validDefault ? { default: schema.default as number } : {}),
202
+ };
203
+ }
204
+
205
+ case "boolean":
206
+ return {
207
+ type: "boolean",
208
+ ...base,
209
+ ...(typeof schema.default === "boolean" ? { default: schema.default } : {}),
210
+ };
211
+
212
+ case "array": {
213
+ // Only array-of-string is in the documented subset (issue #22's
214
+ // "array-of-string" entry) — an array of numbers/objects/booleans, or
215
+ // one with no `items` schema at all, is dropped predictably rather than
216
+ // guessed at.
217
+ if (!isPlainObject(schema.items) || schema.items.type !== "string") return null;
218
+
219
+ // A CONSTRAINED item schema (`items.enum`) is a multi-select from a
220
+ // fixed option list, not free text — map it onto the FieldSpec that
221
+ // actually enforces that constraint (`multi-enum`) rather than
222
+ // relaxing it into an unconstrained `list` (fix-round-1, issue #22: a
223
+ // validator probe found `{type:'array', items:{type:'string',
224
+ // enum:[...]}}` was silently widening to free text). `minItems`/
225
+ // `maxItems` have no `multi-enum` equivalent and are dropped here —
226
+ // documented in this module's doc comment alongside the other
227
+ // constraint keywords this subset does not enforce.
228
+ const itemEnum = stringArray(schema.items.enum);
229
+ if (itemEnum) {
230
+ const enumDefault = stringArray(schema.default);
231
+ return {
232
+ type: "multi-enum",
233
+ ...base,
234
+ options: itemEnum,
235
+ ...(enumDefault && enumDefault.every((v) => itemEnum.includes(v))
236
+ ? { default: enumDefault }
237
+ : {}),
238
+ };
239
+ }
240
+
241
+ const listDefault = stringArray(schema.default);
242
+ return {
243
+ type: "list",
244
+ ...base,
245
+ ...(Number.isInteger(schema.minItems) && (schema.minItems as number) >= 0
246
+ ? { minItems: schema.minItems as number }
247
+ : {}),
248
+ ...(Number.isInteger(schema.maxItems) && (schema.maxItems as number) >= 0
249
+ ? { maxItems: schema.maxItems as number }
250
+ : {}),
251
+ ...(listDefault ? { default: listDefault } : {}),
252
+ };
253
+ }
254
+
255
+ case "object": {
256
+ const nested = mapProperties(schema);
257
+ if (nested.length === 0) return null; // an empty/property-less object has nothing to render
258
+ return {
259
+ type: "group",
260
+ name,
261
+ ...(title !== undefined ? { label: title } : {}),
262
+ ...(description !== undefined ? { description } : {}),
263
+ ...(required ? { required: true } : {}),
264
+ variant: "advanced",
265
+ groups: [{ key: name, label: title ?? name, fields: nested }],
266
+ };
267
+ }
268
+
269
+ default:
270
+ // An unrecognized/absent `type` (JSON-Schema-2020-12 type arrays,
271
+ // `"null"`, a schema with no `type` at all) — outside the documented
272
+ // subset, dropped predictably rather than mismapped.
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /** Maps every property of an OBJECT schema onto `FieldSpec[]`, dropping unsupported ones. */
278
+ function mapProperties(objectSchema: JsonSchemaObject): FieldSpec[] {
279
+ const properties = isPlainObject(objectSchema.properties) ? objectSchema.properties : {};
280
+ const required = stringArray(objectSchema.required) ?? [];
281
+ const fields: FieldSpec[] = [];
282
+ for (const [name, propertySchema] of Object.entries(properties)) {
283
+ const field = mapProperty(name, propertySchema, required.includes(name));
284
+ if (field) fields.push(field);
285
+ }
286
+ return fields;
287
+ }
288
+
289
+ export interface FromJsonSchemaOptions {
290
+ /** `FormSpec.formName` — required; JSON Schema has no equivalent stable identifier. */
291
+ formName: string;
292
+ /** Overrides the mapped `schema.title`. */
293
+ title?: string;
294
+ /** Overrides the mapped `schema.description`. */
295
+ description?: string;
296
+ /** `FormSpec.submitLabel` — JSON Schema has no equivalent. */
297
+ submitLabel?: string;
298
+ }
299
+
300
+ /**
301
+ * Maps a JSON Schema OBJECT schema (an OpenAPI request body, a connector
302
+ * manifest) onto a `FormSpec`, using only the documented subset described in
303
+ * this module's doc comment. Throws `UnsupportedJsonSchemaError` if `schema`
304
+ * (at any depth) uses `$ref`/`allOf`/`oneOf`/`anyOf`/`not`, or if the
305
+ * top-level schema is not an object schema — never silently mismapped. A
306
+ * property whose shape falls outside the subset is dropped, not thrown on.
307
+ */
308
+ export function fromJsonSchema(schema: unknown, options: FromJsonSchemaOptions): FormSpec {
309
+ assertSupported(schema, "#");
310
+ if (!isPlainObject(schema) || schema.type !== "object") {
311
+ throw new UnsupportedJsonSchemaError('type (expected "object" at the top level)', "#");
312
+ }
313
+ return {
314
+ formName: options.formName,
315
+ title: options.title ?? (typeof schema.title === "string" ? schema.title : undefined),
316
+ description:
317
+ options.description ??
318
+ (typeof schema.description === "string" ? schema.description : undefined),
319
+ submitLabel: options.submitLabel,
320
+ fields: mapProperties(schema),
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Reconstructs the NESTED JSON object shape that `fromJsonSchema()`'s
326
+ * `{ type: "object", properties: {...} }` mapping flattens away (PR #119
327
+ * review thread 3, P1). `SchemaForm` stores every field's value in one flat
328
+ * `FormValues` map keyed by BARE property name (`GroupFieldSpec` — see the
329
+ * "no notion of an active branch" comment on `schema-form-spec.ts`'s
330
+ * `GroupFieldSpec` — has no value slot of its own), so a form built from a
331
+ * nested schema like `{ address: { properties: { city, zip } } }` submits
332
+ * `{ city, zip }` instead of the advertised `{ address: { city, zip } }`.
333
+ *
334
+ * Call this (instead of reading `effectiveValues`/`onSubmit`'s payload
335
+ * directly) when the schema passed to `fromJsonSchema()` had nested
336
+ * `"object"` properties and the caller needs the request body back in its
337
+ * original shape — e.g. `SchemaFormTestAction`'s `onTest` or a submit
338
+ * handler that forwards the payload to the API the schema describes.
339
+ *
340
+ * Walks `spec.fields` and, for each `group` field this adapter produced
341
+ * (always `variant: "advanced"`, always exactly one branch — see
342
+ * `mapProperty`'s `"object"` case), recurses into `groups[0].fields` and
343
+ * nests the result under that group's own `name` instead of splicing its
344
+ * children into the top level.
345
+ *
346
+ * Two known, out-of-scope limits (see the reply on review thread 3):
347
+ * 1. Two properties at DIFFERENT nesting depths that happen to share a bare
348
+ * name (`{ name }` at the top level and `{ address: { name } }`) collide
349
+ * in the flat `FormValues` map itself, upstream of this function — this
350
+ * function cannot recover data `SchemaForm` never kept separate.
351
+ * 2. A nested object's own `required` flag (`GroupFieldSpec.required`) is
352
+ * mapped but never validated anywhere in `schema-form-spec.ts`'s
353
+ * `collectValidatableFields` — fixing that touches a file outside this
354
+ * unit's scope fence and is called out, unfixed, in the PR review reply.
355
+ */
356
+ export function jsonSchemaRequestBody(spec: FormSpec, values: FormValues): Record<string, unknown> {
357
+ return collectRequestBody(spec.fields, values);
358
+ }
359
+
360
+ function collectRequestBody(fields: FieldSpec[], values: FormValues): Record<string, unknown> {
361
+ const body: Record<string, unknown> = {};
362
+ for (const field of fields) {
363
+ if (field.type === "group") {
364
+ // `fromJsonSchema()` only ever emits a single-branch `"advanced"`
365
+ // group per nested object (`mapProperty`'s `"object"` case) — nest
366
+ // the branch's reconstructed values under the group's own name.
367
+ const branch = field.groups[0];
368
+ if (branch) body[field.name] = collectRequestBody(branch.fields, values);
369
+ continue;
370
+ }
371
+ const value = values[field.name];
372
+ if (value !== undefined) body[field.name] = value;
373
+ }
374
+ return body;
375
+ }
@@ -0,0 +1,81 @@
1
+ export {
2
+ SchemaForm,
3
+ SchemaFormProvider,
4
+ SchemaFormRoot,
5
+ SchemaFormFields,
6
+ SchemaFormField,
7
+ SchemaFormSubmit,
8
+ SchemaFormTestAction,
9
+ SchemaFormError,
10
+ SchemaFormTitle,
11
+ SchemaFormDescription,
12
+ SchemaFormFallback,
13
+ type SchemaFormProps,
14
+ type SchemaFormProviderProps,
15
+ type SchemaFormRootProps,
16
+ type SchemaFormFieldsProps,
17
+ type SchemaFormFieldProps,
18
+ type SchemaFormSubmitProps,
19
+ type SchemaFormTestActionProps,
20
+ type SchemaFormTestActionStatus,
21
+ type SchemaFormErrorProps,
22
+ type SchemaFormTitleProps,
23
+ type SchemaFormDescriptionProps,
24
+ type SchemaFormFallbackProps,
25
+ } from "./schema-form";
26
+
27
+ export {
28
+ fromJsonSchema,
29
+ UnsupportedJsonSchemaError,
30
+ type FromJsonSchemaOptions,
31
+ } from "./from-json-schema";
32
+
33
+ export {
34
+ fieldSpecSchema,
35
+ formSpecSchema,
36
+ enumOptionSchema,
37
+ keyValueRowSchema,
38
+ stringFieldSchema,
39
+ numberFieldSchema,
40
+ integerFieldSchema,
41
+ booleanFieldSchema,
42
+ enumFieldSchema,
43
+ multiEnumFieldSchema,
44
+ listFieldSchema,
45
+ keyValueFieldSchema,
46
+ fileFieldSchema,
47
+ groupItemSchema,
48
+ groupFieldSchema,
49
+ optionValue,
50
+ optionLabel,
51
+ fieldLabel,
52
+ initialFormValues,
53
+ normalizeFormSpec,
54
+ validateField,
55
+ validateForm,
56
+ findFieldByName,
57
+ collectValidatableFields,
58
+ isFieldVisible,
59
+ fileMatchesAccept,
60
+ checkFileIssue,
61
+ type EnumOption,
62
+ type FieldKeyValueRow,
63
+ type StringFieldSpec,
64
+ type NumberFieldSpec,
65
+ type IntegerFieldSpec,
66
+ type BooleanFieldSpec,
67
+ type EnumFieldSpec,
68
+ type MultiEnumFieldSpec,
69
+ type ListFieldSpec,
70
+ type KeyValueFieldSpec,
71
+ type FileFieldSpec,
72
+ type GroupItemSpec,
73
+ type GroupFieldSpec,
74
+ type FieldSpec,
75
+ type FormSpec,
76
+ type FormValue,
77
+ type FormValues,
78
+ type FormSubmitState,
79
+ type NormalizedFormSpec,
80
+ type FileIssue,
81
+ } from "./schema-form-spec";