schema-components 0.0.0 → 1.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.
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/dist/core/adapter.d.mts +19 -0
- package/dist/core/adapter.mjs +140 -0
- package/dist/core/errors.d.mts +2 -0
- package/dist/core/errors.mjs +74 -0
- package/dist/core/guards.d.mts +44 -0
- package/dist/core/guards.mjs +58 -0
- package/dist/core/renderer.d.mts +2 -0
- package/dist/core/renderer.mjs +71 -0
- package/dist/core/types.d.mts +3 -0
- package/dist/core/types.mjs +40 -0
- package/dist/core/walker.d.mts +14 -0
- package/dist/core/walker.mjs +366 -0
- package/dist/errors-DIKI2C78.d.mts +57 -0
- package/dist/html/a11y.d.mts +47 -0
- package/dist/html/a11y.mjs +81 -0
- package/dist/html/html.d.mts +135 -0
- package/dist/html/html.mjs +168 -0
- package/dist/html/renderToHtml.d.mts +32 -0
- package/dist/html/renderToHtml.mjs +352 -0
- package/dist/html/renderToHtmlStream.d.mts +58 -0
- package/dist/html/renderToHtmlStream.mjs +285 -0
- package/dist/html/styles.css +151 -0
- package/dist/openapi/components.d.mts +76 -0
- package/dist/openapi/components.mjs +223 -0
- package/dist/openapi/parser.d.mts +45 -0
- package/dist/openapi/parser.mjs +159 -0
- package/dist/react/SchemaComponent.d.mts +96 -0
- package/dist/react/SchemaComponent.mjs +283 -0
- package/dist/react/SchemaErrorBoundary.d.mts +26 -0
- package/dist/react/SchemaErrorBoundary.mjs +47 -0
- package/dist/react/headless.d.mts +13 -0
- package/dist/react/headless.mjs +163 -0
- package/dist/themes/shadcn.d.mts +6 -0
- package/dist/themes/shadcn.mjs +166 -0
- package/dist/types-BU0ETFHk.d.mts +326 -0
- package/package.json +113 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
## [1.1.0](https://github.com/Mearman/schema-components/compare/v1.0.0...v1.1.0) (2026-05-14)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add validation, recursive, and OpenAPI operation stories ([d1b742e](https://github.com/Mearman/schema-components/commit/d1b742e58e57e7c11ce36a7a868e1efc2574a04b))
|
|
6
|
+
* expand Storybook coverage with JSON Schema, streaming, and error stories ([4378701](https://github.com/Mearman/schema-components/commit/4378701b29bcd1ebec5b8caf46bdd452d4b0f766))
|
|
7
|
+
|
|
8
|
+
## 1.0.0 (2026-05-14)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add accessibility attributes to HTML renderers ([ea49b72](https://github.com/Mearman/schema-components/commit/ea49b7264b3dd496afba851d6bcee6a103e688fb))
|
|
13
|
+
* add default stylesheet for sc- prefixed HTML classes ([44d1a91](https://github.com/Mearman/schema-components/commit/44d1a91ce9f986567552070f6d698d3f3d78f7b1))
|
|
14
|
+
* add html renderer — render schemas to raw html strings ([a803cfb](https://github.com/Mearman/schema-components/commit/a803cfb4fb8a881115dc224abe15cb4325844fbb))
|
|
15
|
+
* add renderChild to RenderProps for theme adapter recursion ([24305a2](https://github.com/Mearman/schema-components/commit/24305a2793d80d7a29384ba5fda2c46861479952))
|
|
16
|
+
* add shadcn adapter, schema caching, and update readme/package ([fff4082](https://github.com/Mearman/schema-components/commit/fff40825bdb6e3820010dd7faa48a307549af7c9))
|
|
17
|
+
* add Storybook with GitHub Pages deployment ([eae9817](https://github.com/Mearman/schema-components/commit/eae98178e565fa10ba1c2f258887fafb08cafd37))
|
|
18
|
+
* add streaming HTML renderer with three output formats ([5d3fa4a](https://github.com/Mearman/schema-components/commit/5d3fa4a20b3c65af83059ab2b111fda9392085c1))
|
|
19
|
+
* add type-safe openapi components with generic inference ([683e950](https://github.com/Mearman/schema-components/commit/683e950cf08a52538bbfa873400386616ec98756))
|
|
20
|
+
* add typed errors, onError callback, and React error boundary ([893f9a1](https://github.com/Mearman/schema-components/commit/893f9a1fc251159c80a73b45fbc1d6f63f99a857))
|
|
21
|
+
* add unit tests and fix schema passthrough bug ([23092cc](https://github.com/Mearman/schema-components/commit/23092ccdac3cd37068b0eae08753ba654c50359a))
|
|
22
|
+
* flatten nested fields prop for walker field overrides ([a72ffbe](https://github.com/Mearman/schema-components/commit/a72ffbe776feb830830bf7bf68dd6fc9b0deb8d0))
|
|
23
|
+
* implement core library — walker, adapter, renderer, React components ([2c95a2e](https://github.com/Mearman/schema-components/commit/2c95a2e448b2646c4d6c4565ceb30e99500ce732))
|
|
24
|
+
* replace string templates with typed h() builder ([83525b1](https://github.com/Mearman/schema-components/commit/83525b115f77b39c949e566d4838332dae5f92ae))
|
|
25
|
+
* type-safe fields prop with generic SchemaComponent<T, Ref> ([3ea1495](https://github.com/Mearman/schema-components/commit/3ea14950e3d20eb80d8b16186803f3ab0ed28f84))
|
|
26
|
+
* type-safe path prop on generic SchemaField ([8d5fef7](https://github.com/Mearman/schema-components/commit/8d5fef7405d6c4b2f914e0481e501eaf633ddc5b))
|
|
27
|
+
* wire adapter, resolver, and SchemaField into SchemaComponent ([cbc2ce1](https://github.com/Mearman/schema-components/commit/cbc2ce158c080a1a84a940781b81d9d065e055b1))
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* propagate field key as path in HTML renderChild ([e3f471c](https://github.com/Mearman/schema-components/commit/e3f471c2ee308edf1e6406c1fc6f70bfac8c6b37))
|
|
32
|
+
* readOnly/writeOnly overrides propagate correctly to nested fields ([a809b62](https://github.com/Mearman/schema-components/commit/a809b62bbe08ae44d3fdaa9b277a3ded07c6009d))
|
|
33
|
+
|
|
34
|
+
### Refactoring
|
|
35
|
+
|
|
36
|
+
* centralise type guards, resolver lookup, and resolver merge ([be354ac](https://github.com/Mearman/schema-components/commit/be354ac35b6d73ec0bbc746fae58c35758991ce0))
|
|
37
|
+
* remove duplicate ComponentResolver types from types.ts ([39702c5](https://github.com/Mearman/schema-components/commit/39702c5ab622be825ad7f1ad222c07dca85c57a4))
|
|
38
|
+
* unify RenderProps and HtmlRenderProps via BaseFieldProps ([a408da9](https://github.com/Mearman/schema-components/commit/a408da9a2f4b4bacf81f0a85f6aad117fa69bb8a))
|
|
39
|
+
* use json schema as authoritative internal representation ([6a996c5](https://github.com/Mearman/schema-components/commit/6a996c53517d18f39cb341f10ed3fafdd7777547))
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
* add README for schema-components design document ([c1251e4](https://github.com/Mearman/schema-components/commit/c1251e4eea39e0154c3e8cc91e5482271bb4ce95))
|
|
44
|
+
* rewrite README to match actual API ([a8fc57f](https://github.com/Mearman/schema-components/commit/a8fc57fe5e650c9f4f886f0525c8c58e309a70aa))
|
|
45
|
+
* update readme for json schema walker architecture ([9649430](https://github.com/Mearman/schema-components/commit/9649430f76f2fbef6d0ea7a8d9fd81ff478f644f))
|
|
46
|
+
* update README with h() builder, streaming, accessibility, errors ([01d2a9f](https://github.com/Mearman/schema-components/commit/01d2a9f32dcdf3827ce117901cd1b6ff9c3e3d26))
|
|
47
|
+
|
|
48
|
+
### Tests
|
|
49
|
+
|
|
50
|
+
* add parser unit tests and integration tests ([338dba3](https://github.com/Mearman/schema-components/commit/338dba368007dd2d34172a8dacfdb76606a588c6))
|
|
51
|
+
|
|
52
|
+
### Chores
|
|
53
|
+
|
|
54
|
+
* add mit license and contributing guide ([7d51c29](https://github.com/Mearman/schema-components/commit/7d51c2957ab06fdd6c8bb4a384b3906e453ae786))
|
|
55
|
+
* **build:** remove barrel files, use direct module exports ([08d4f21](https://github.com/Mearman/schema-components/commit/08d4f2148e3e06c95fd1db3a9c5a733a419f3fdc))
|
|
56
|
+
* **build:** replace tsc with tsdown for library bundling ([38951b2](https://github.com/Mearman/schema-components/commit/38951b2ef0b770ec883f18fe9f3d4f4bf7181a5a))
|
|
57
|
+
* pin all GitHub Actions to latest versions, fix CI types ([29c5b6e](https://github.com/Mearman/schema-components/commit/29c5b6e156c9a3b59e656a26c4bf147a1e5b5321))
|
|
58
|
+
* rename to schema-components, adopt OIDC trusted publishing ([61e5c75](https://github.com/Mearman/schema-components/commit/61e5c753d5d103538213157f6fd4810572480d72))
|
|
59
|
+
* set up repo devops ([1042810](https://github.com/Mearman/schema-components/commit/1042810992bf4cb45e77efd680008c184f307210))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joseph Mearman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# schema-components
|
|
2
|
+
|
|
3
|
+
React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents.
|
|
4
|
+
|
|
5
|
+
Define your data model once. Get presentational views, input fields, and editable forms — no manual wiring.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install schema-components
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies: `zod@^4.0.0`, `react@^18.0.0 || ^19.0.0`.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { SchemaComponent } from "schema-components/react/SchemaComponent";
|
|
20
|
+
|
|
21
|
+
const userSchema = z.object({
|
|
22
|
+
name: z.string().min(1).meta({ description: "Full name" }),
|
|
23
|
+
email: z.email().meta({ description: "Email address" }),
|
|
24
|
+
role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
|
|
25
|
+
active: z.boolean().meta({ description: "Active" }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function UserCard() {
|
|
29
|
+
const [user, setUser] = useState({
|
|
30
|
+
name: "Ada Lovelace",
|
|
31
|
+
email: "ada@example.com",
|
|
32
|
+
role: "admin",
|
|
33
|
+
active: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<SchemaComponent
|
|
38
|
+
schema={userSchema}
|
|
39
|
+
value={user}
|
|
40
|
+
onChange={setUser}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Renders every field as an editable input. Add `readOnly` to the component for a read-only view:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<SchemaComponent schema={userSchema} value={user} readOnly />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Zod schema ─── z.toJSONSchema() ──→ JSON Schema ──────────┐
|
|
56
|
+
▼
|
|
57
|
+
JSON Schema ─────────────────────────────────────────► JSON Schema ──► walker ──► React
|
|
58
|
+
▲
|
|
59
|
+
OpenAPI doc ── extract schemas ───────────────────────────┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
One walker, one input format. The walker reads standard JSON Schema keywords (Draft 2020-12) — decoupled from Zod's internal API. `z.toJSONSchema()` is lossless: it preserves `readOnly`, `writeOnly`, custom `.meta()` properties, constraints, formats, and defaults.
|
|
63
|
+
|
|
64
|
+
`z.fromJSONSchema()` is used **only for validation** — converting JSON Schema / OpenAPI inputs back to Zod when `validate` is true and the original wasn't a Zod schema.
|
|
65
|
+
|
|
66
|
+
## Component editability
|
|
67
|
+
|
|
68
|
+
Fields render in one of three states, controlled by `readOnly` and `writeOnly` from three sources:
|
|
69
|
+
|
|
70
|
+
| State | Rendering |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **Presentation** | Read-only display. Formatted text, links, badges. No inputs. |
|
|
73
|
+
| **Input** | Empty field. Blank inputs, "Select…" dropdowns, unchecked toggles. |
|
|
74
|
+
| **Editable** | Pre-populated input the user can change. |
|
|
75
|
+
|
|
76
|
+
### Three sources, priority order
|
|
77
|
+
|
|
78
|
+
1. **Schema property** (`.meta({ readOnly: true })`) — always wins
|
|
79
|
+
2. **Component props** (`readOnly` / `writeOnly` on `<SchemaComponent>`) — rendering context
|
|
80
|
+
3. **Schema root** (`.meta({ readOnly: true })` on root schema) — fallback default
|
|
81
|
+
4. Neither → Editable
|
|
82
|
+
|
|
83
|
+
### Overriding with `readOnly: false`
|
|
84
|
+
|
|
85
|
+
A field override can explicitly opt out of a higher-level `readOnly`:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<SchemaComponent
|
|
89
|
+
schema={userSchema}
|
|
90
|
+
value={user}
|
|
91
|
+
readOnly // everything presentation
|
|
92
|
+
fields={{
|
|
93
|
+
address: {
|
|
94
|
+
readOnly: false, // address subtree: editable
|
|
95
|
+
city: { readOnly: true }, // city: still presentation
|
|
96
|
+
},
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Type-safe field overrides
|
|
102
|
+
|
|
103
|
+
The `fields` prop type is inferred from the schema:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// Zod — full autocomplete
|
|
107
|
+
<SchemaComponent
|
|
108
|
+
schema={userSchema}
|
|
109
|
+
fields={{
|
|
110
|
+
name: { readOnly: true }, // ✓ type-safe
|
|
111
|
+
address: {
|
|
112
|
+
city: { description: "City" }, // ✓ nested, type-safe
|
|
113
|
+
},
|
|
114
|
+
// nme: { readOnly: true }, // ✗ TypeScript error: unknown key
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
// JSON Schema as const — full autocomplete
|
|
119
|
+
const jsonSchema = {
|
|
120
|
+
type: "object" as const,
|
|
121
|
+
properties: {
|
|
122
|
+
name: { type: "string" as const },
|
|
123
|
+
email: { type: "string" as const, format: "email" },
|
|
124
|
+
},
|
|
125
|
+
required: ["name"],
|
|
126
|
+
} as const;
|
|
127
|
+
|
|
128
|
+
<SchemaComponent
|
|
129
|
+
schema={jsonSchema}
|
|
130
|
+
fields={{
|
|
131
|
+
name: { readOnly: true }, // ✓ inferred from as const
|
|
132
|
+
// nme: { readOnly: true }, // ✗ TypeScript error
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
// OpenAPI as const + ref — full autocomplete
|
|
137
|
+
const spec = {
|
|
138
|
+
openapi: "3.1.0",
|
|
139
|
+
components: {
|
|
140
|
+
schemas: {
|
|
141
|
+
User: {
|
|
142
|
+
type: "object" as const,
|
|
143
|
+
properties: {
|
|
144
|
+
id: { type: "string" as const },
|
|
145
|
+
name: { type: "string" as const },
|
|
146
|
+
},
|
|
147
|
+
required: ["id", "name"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
} as const;
|
|
152
|
+
|
|
153
|
+
<SchemaComponent
|
|
154
|
+
schema={spec}
|
|
155
|
+
ref="#/components/schemas/User"
|
|
156
|
+
fields={{
|
|
157
|
+
id: { readOnly: true }, // ✓ inferred through ref
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Individual fields
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { SchemaField } from "schema-components/react/SchemaComponent";
|
|
166
|
+
|
|
167
|
+
// Type-safe path — only valid dot-paths accepted
|
|
168
|
+
<SchemaField
|
|
169
|
+
schema={userSchema}
|
|
170
|
+
path="address.city" // ✓ type-safe
|
|
171
|
+
// path="address.cty" // ✗ TypeScript error
|
|
172
|
+
value={user}
|
|
173
|
+
onChange={setUser}
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When the schema is a Zod schema or typed `as const`, only valid dot-paths like `"address.city"` are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.
|
|
178
|
+
|
|
179
|
+
## All input formats
|
|
180
|
+
|
|
181
|
+
`<SchemaComponent>` auto-detects the input format:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Zod schema
|
|
185
|
+
<SchemaComponent schema={z.object({ name: z.string() })} value={data} />
|
|
186
|
+
|
|
187
|
+
// JSON Schema
|
|
188
|
+
<SchemaComponent
|
|
189
|
+
schema={{ type: "object", properties: { name: { type: "string" } } }}
|
|
190
|
+
value={data}
|
|
191
|
+
/>
|
|
192
|
+
|
|
193
|
+
// OpenAPI document + ref
|
|
194
|
+
<SchemaComponent
|
|
195
|
+
schema={openApiSpec}
|
|
196
|
+
ref="#/components/schemas/User"
|
|
197
|
+
value={data}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## OpenAPI components
|
|
202
|
+
|
|
203
|
+
Render API operations with type-safe field overrides:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { ApiOperation } from "schema-components/openapi/components";
|
|
207
|
+
import type { ApiRequestBodyProps } from "schema-components/openapi/components";
|
|
208
|
+
|
|
209
|
+
const petStore = {
|
|
210
|
+
openapi: "3.1.0",
|
|
211
|
+
paths: {
|
|
212
|
+
"/pets": {
|
|
213
|
+
post: {
|
|
214
|
+
requestBody: {
|
|
215
|
+
content: {
|
|
216
|
+
"application/json": {
|
|
217
|
+
schema: {
|
|
218
|
+
type: "object" as const,
|
|
219
|
+
properties: {
|
|
220
|
+
name: { type: "string" as const },
|
|
221
|
+
tag: { type: "string" as const },
|
|
222
|
+
},
|
|
223
|
+
required: ["name"],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
responses: { "201": { description: "Created" } },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
} as const;
|
|
233
|
+
|
|
234
|
+
// Full operation — parameters, request body, responses
|
|
235
|
+
<ApiOperation schema={petStore} path="/pets" method="post" />
|
|
236
|
+
|
|
237
|
+
// Just the request body with type-safe fields
|
|
238
|
+
<ApiRequestBody
|
|
239
|
+
schema={petStore}
|
|
240
|
+
path="/pets"
|
|
241
|
+
method="post"
|
|
242
|
+
fields={{
|
|
243
|
+
name: { description: "Pet name" }, // ✓ inferred from as const
|
|
244
|
+
// nme: { description: "X" }, // ✗ TypeScript error
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
// Just parameters with type-safe overrides
|
|
249
|
+
<ApiParameters
|
|
250
|
+
schema={petStore}
|
|
251
|
+
path="/pets"
|
|
252
|
+
method="get"
|
|
253
|
+
overrides={{
|
|
254
|
+
limit: { description: "Max results" }, // ✓ inferred parameter names
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
|
|
258
|
+
// Response schema
|
|
259
|
+
<ApiResponse schema={petStore} path="/pets" method="get" status="200" />
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Theme adapters
|
|
263
|
+
|
|
264
|
+
Headless by default (plain HTML). Wrap with a theme adapter for styled components:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { SchemaProvider } from "schema-components/react/SchemaComponent";
|
|
268
|
+
import { shadcnResolver } from "schema-components/themes/shadcn";
|
|
269
|
+
|
|
270
|
+
<SchemaProvider resolver={shadcnResolver}>
|
|
271
|
+
<SchemaComponent schema={userSchema} value={user} onChange={setUser} />
|
|
272
|
+
</SchemaProvider>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Write a custom adapter:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
import type { RenderProps, ComponentResolver } from "schema-components/core/renderer";
|
|
279
|
+
|
|
280
|
+
const myResolver: ComponentResolver = {
|
|
281
|
+
string: (props: RenderProps) => {
|
|
282
|
+
if (props.readOnly) return <span>{props.value}</span>;
|
|
283
|
+
return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
|
|
284
|
+
},
|
|
285
|
+
object: (props: RenderProps) => {
|
|
286
|
+
// props.renderChild recursively renders each field
|
|
287
|
+
return (
|
|
288
|
+
<div>
|
|
289
|
+
{props.fields && Object.entries(props.fields).map(([key, field]) => (
|
|
290
|
+
<div key={key}>
|
|
291
|
+
<label>{field.meta.description}</label>
|
|
292
|
+
{props.renderChild(field, (props.value as Record<string, unknown>)?.[key], (v) => {
|
|
293
|
+
props.onChange({ ...(props.value as object), [key]: v });
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Every render function receives `props.renderChild` for recursive rendering — no need to know about the resolver or rendering context.
|
|
304
|
+
|
|
305
|
+
## Raw HTML
|
|
306
|
+
|
|
307
|
+
Render schemas to HTML strings — no React needed. Useful for server-side rendering, email templates, static sites, and non-React environments.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import { renderToHtml } from "schema-components/html/renderToHtml";
|
|
311
|
+
|
|
312
|
+
const userSchema = z.object({
|
|
313
|
+
name: z.string().meta({ description: "Name" }),
|
|
314
|
+
email: z.email().meta({ description: "Email" }),
|
|
315
|
+
role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Read-only display
|
|
319
|
+
const html = renderToHtml(userSchema, {
|
|
320
|
+
value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
|
|
321
|
+
readOnly: true,
|
|
322
|
+
});
|
|
323
|
+
// → <dl class="sc-object">
|
|
324
|
+
// <dt class="sc-label">Name</dt><dd class="sc-value"><span class="sc-value">Ada Lovelace</span></dd>
|
|
325
|
+
// <dt class="sc-label">Email</dt><dd class="sc-value"><a class="sc-value" href="mailto:ada@example.com">ada@example.com</a></dd>
|
|
326
|
+
// <dt class="sc-label">Role</dt><dd class="sc-value"><span class="sc-value">admin</span></dd>
|
|
327
|
+
// </dl>
|
|
328
|
+
|
|
329
|
+
// Editable form
|
|
330
|
+
const formHtml = renderToHtml(userSchema, {
|
|
331
|
+
value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
|
|
332
|
+
});
|
|
333
|
+
// → <fieldset class="sc-object">
|
|
334
|
+
// <div class="sc-field">
|
|
335
|
+
// <label class="sc-label" for="sc-name">Name</label>
|
|
336
|
+
// <input class="sc-input" type="text" name="" value="Ada Lovelace">
|
|
337
|
+
// </div>
|
|
338
|
+
// ...
|
|
339
|
+
// </fieldset>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
All HTML output uses `sc-` prefixed classes for styling hooks. HTML is properly escaped by the serialiser — no manual escaping needed.
|
|
343
|
+
|
|
344
|
+
A default stylesheet is included:
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<link rel="stylesheet" href="node_modules/schema-components/dist/html/styles.css">
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Or import in JS:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import "schema-components/styles.css";
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Structured HTML construction
|
|
357
|
+
|
|
358
|
+
The HTML renderer uses a typed `h()` builder instead of string templates. This gives compile-time safety and automatic escaping:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { h, serialize, raw } from "schema-components/html/html";
|
|
362
|
+
|
|
363
|
+
// Build elements — attrs are type-checked, values auto-escaped
|
|
364
|
+
const input = h("input", { type: "text", id: "name", value: userValue });
|
|
365
|
+
serialize(input); // → <input type="text" id="name" value="Ada">
|
|
366
|
+
|
|
367
|
+
// Embed pre-serialised HTML (from child renderers)
|
|
368
|
+
const div = h("div", { class: "field" }, raw(childHtml));
|
|
369
|
+
serialize(div);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The builder handles void elements (`<input>`, `<br>`, etc.), boolean attributes (`checked`, `disabled`), fragments, and nested children.
|
|
373
|
+
|
|
374
|
+
### Streaming HTML
|
|
375
|
+
|
|
376
|
+
Three output formats for incremental rendering:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { renderToHtmlChunks } from "schema-components/html/renderToHtmlStream";
|
|
380
|
+
import { renderToHtmlStream } from "schema-components/html/renderToHtmlStream";
|
|
381
|
+
import { renderToHtmlReadable } from "schema-components/html/renderToHtmlStream";
|
|
382
|
+
|
|
383
|
+
// Sync iterable — chunks yielded at field/item/entry boundaries
|
|
384
|
+
const chunks: string[] = [...renderToHtmlChunks(schema, { value })];
|
|
385
|
+
|
|
386
|
+
// Async iterable — yields control to event loop between chunks
|
|
387
|
+
for await (const chunk of renderToHtmlStream(schema, { value })) {
|
|
388
|
+
res.write(chunk);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Web ReadableStream — for Response, TransformStream, etc.
|
|
392
|
+
return new Response(renderToHtmlReadable(schema, { value }), {
|
|
393
|
+
headers: { "Content-Type": "text/html" },
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Concatenating all chunks produces identical output to `renderToHtml`.
|
|
398
|
+
|
|
399
|
+
### Accessibility
|
|
400
|
+
|
|
401
|
+
The HTML renderer produces WAI-ARIA-compliant markup:
|
|
402
|
+
|
|
403
|
+
| Attribute | When |
|
|
404
|
+
|---|---|
|
|
405
|
+
| `id="<key>"` | All editable inputs |
|
|
406
|
+
| `aria-required="true"` | Required fields (`isOptional === false`) |
|
|
407
|
+
| `aria-describedby="<id>-hint"` | Fields with constraints (min/max/length/pattern) |
|
|
408
|
+
| `aria-readonly="true"` | Read-only presentation spans |
|
|
409
|
+
| `aria-label="<description>"` | Checkboxes (no visible text node) |
|
|
410
|
+
| `role="group"` | Record containers |
|
|
411
|
+
| `aria-label` on `<fieldset>` | Object with description |
|
|
412
|
+
| `<small class="sc-hint">` | Constraint hint text |
|
|
413
|
+
| `<span class="sc-required" aria-hidden="true">*` | Required field indicator |
|
|
414
|
+
|
|
415
|
+
### Custom HTML resolver
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { renderToHtml } from "schema-components/html/renderToHtml";
|
|
419
|
+
import type { HtmlResolver, HtmlRenderProps } from "schema-components/html/renderToHtml";
|
|
420
|
+
|
|
421
|
+
const tailwindResolver: HtmlResolver = {
|
|
422
|
+
string: (props: HtmlRenderProps) => {
|
|
423
|
+
if (props.readOnly) {
|
|
424
|
+
return `<span class="text-sm text-gray-700">${typeof props.value === "string" ? props.value : ""}</span>`;
|
|
425
|
+
}
|
|
426
|
+
return `<input class="border rounded px-2 py-1" type="text" value="${typeof props.value === "string" ? props.value : "">">`;
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const html = renderToHtml(schema, { value, readOnly: true, resolver: tailwindResolver });
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Custom resolvers fall back to the default for any type you don't override.
|
|
434
|
+
|
|
435
|
+
## Custom widgets
|
|
436
|
+
|
|
437
|
+
Register widgets by `.meta({ component })` hint:
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import { registerWidget } from "schema-components/react/SchemaComponent";
|
|
441
|
+
|
|
442
|
+
registerWidget("richtext", ({ value, onChange }) => (
|
|
443
|
+
<RichTextEditor value={value} onChange={onChange} />
|
|
444
|
+
));
|
|
445
|
+
|
|
446
|
+
// In schema
|
|
447
|
+
const schema = z.object({
|
|
448
|
+
bio: z.string().meta({ component: "richtext" }),
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Resolution order: `.meta({ component })` → registered widget → theme adapter → headless default.
|
|
453
|
+
|
|
454
|
+
## Validation
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
<SchemaComponent
|
|
458
|
+
schema={userSchema}
|
|
459
|
+
value={user}
|
|
460
|
+
onChange={setUser}
|
|
461
|
+
validate
|
|
462
|
+
onValidationError={(error) => console.error(error)}
|
|
463
|
+
/>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
|
|
467
|
+
|
|
468
|
+
## Error handling
|
|
469
|
+
|
|
470
|
+
Typed errors with `onError` callback for graceful degradation:
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";
|
|
474
|
+
import { SchemaComponent } from "schema-components/react/SchemaComponent";
|
|
475
|
+
|
|
476
|
+
// Error boundary catches render errors from theme adapters
|
|
477
|
+
<SchemaErrorBoundary fallback={(error, reset) => <p>Error: {error.message}</p>}>
|
|
478
|
+
<SchemaComponent schema={schema} value={data} />
|
|
479
|
+
</SchemaErrorBoundary>
|
|
480
|
+
|
|
481
|
+
// Per-component error callback
|
|
482
|
+
<SchemaComponent
|
|
483
|
+
schema={schema}
|
|
484
|
+
value={data}
|
|
485
|
+
onError={(error) => {
|
|
486
|
+
console.error(error);
|
|
487
|
+
return null; // graceful degradation
|
|
488
|
+
}}
|
|
489
|
+
/>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Without `onError`, errors re-throw. Error hierarchy: `SchemaError` → `SchemaNormalisationError` | `SchemaRenderError` | `SchemaFieldError`.
|
|
493
|
+
|
|
494
|
+
## Architecture
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
schema-components
|
|
498
|
+
├── core # JSON Schema walker, ComponentResolver, RenderProps, typed errors, type guards
|
|
499
|
+
├── react # SchemaComponent, SchemaProvider, SchemaField, headless renderer, error boundary
|
|
500
|
+
├── openapi # Document parser, ApiOperation, ApiParameters, ApiRequestBody, ApiResponse
|
|
501
|
+
├── html # h() builder, renderToHtml, streaming renderers, ARIA helpers
|
|
502
|
+
└── themes # shadcn, MUI, custom adapters (separate packages)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Source files
|
|
506
|
+
|
|
507
|
+
Every module is imported directly — no barrel files.
|
|
508
|
+
|
|
509
|
+
| File | Role |
|
|
510
|
+
|---|---|
|
|
511
|
+
| `core/types.ts` | SchemaMeta, Editability, WalkedField, FieldConstraints, FieldOverrides, FromJSONSchema, PathOfType |
|
|
512
|
+
| `core/walker.ts` | JSON Schema walker (Draft 2020-12), `$ref` resolution, `allOf` merging, nullable/discriminated union detection |
|
|
513
|
+
| `core/adapter.ts` | Normalises all inputs to JSON Schema. WeakMap schema cache. |
|
|
514
|
+
| `core/renderer.ts` | `BaseFieldProps`, `RenderProps`, `HtmlRenderProps`, `ComponentResolver`, `HtmlResolver`, `mergeResolvers` |
|
|
515
|
+
| `core/guards.ts` | Shared type guards: `isObject`, `getProperty`, `hasProperty`, `toRecord` |
|
|
516
|
+
| `core/errors.ts` | `SchemaError`, `SchemaNormalisationError`, `SchemaRenderError`, `SchemaFieldError` |
|
|
517
|
+
| `react/SchemaComponent.tsx` | Generic `<SchemaComponent<T, Ref>>`, `SchemaProvider`, `registerWidget`, `SchemaField<P>` |
|
|
518
|
+
| `react/headless.tsx` | Headless default resolver, `createHeadlessResolver(renderChild)` factory |
|
|
519
|
+
| `react/SchemaErrorBoundary.tsx` | React error boundary catching render errors |
|
|
520
|
+
| `html/html.ts` | Typed `h()` builder — `serialize()`, `serializeChunks()`, `raw()`, `text()`, `fragment()` |
|
|
521
|
+
| `html/a11y.ts` | ARIA helpers — `ariaRequiredAttrs()`, `ariaDescribedByAttrs()`, `buildHintElement()`, `requiredIndicator()` |
|
|
522
|
+
| `html/renderToHtml.ts` | `renderToHtml()`, `defaultHtmlResolver` — schema → HTML string via `h()` builder |
|
|
523
|
+
| `html/renderToHtmlStream.ts` | `renderToHtmlChunks()` (sync), `renderToHtmlStream()` (async), `renderToHtmlReadable()` (web ReadableStream) |
|
|
524
|
+
| `openapi/parser.ts` | OpenAPI document parsing, operation extraction, `$ref` resolution |
|
|
525
|
+
| `openapi/components.tsx` | `ApiOperation`, `ApiParameters`, `ApiRequestBody`, `ApiResponse` with generic type inference |
|
|
526
|
+
| `themes/shadcn.tsx` | shadcn/ui theme adapter |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { l as JsonObject, m as SchemaMeta } from "../types-BU0ETFHk.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/adapter.d.ts
|
|
4
|
+
type SchemaInput = Record<string, unknown>;
|
|
5
|
+
type SchemaKind = "zod4" | "zod3" | "jsonSchema" | "openapi";
|
|
6
|
+
declare function detectSchemaKind(input: unknown): SchemaKind;
|
|
7
|
+
interface NormalisedSchema {
|
|
8
|
+
/** JSON Schema object — the authoritative schema for rendering. */
|
|
9
|
+
jsonSchema: JsonObject;
|
|
10
|
+
/** Original Zod schema, if input was Zod. Used for validation. */
|
|
11
|
+
zodSchema?: unknown;
|
|
12
|
+
/** Root-level metadata. */
|
|
13
|
+
rootMeta: SchemaMeta | undefined;
|
|
14
|
+
/** The root document for $ref resolution. */
|
|
15
|
+
rootDocument: JsonObject;
|
|
16
|
+
}
|
|
17
|
+
declare function normaliseSchema(input: unknown, ref?: string): NormalisedSchema;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { type JsonObject, NormalisedSchema, SchemaInput, SchemaKind, type SchemaMeta, detectSchemaKind, normaliseSchema };
|