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.
Files changed (39) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +526 -0
  4. package/dist/core/adapter.d.mts +19 -0
  5. package/dist/core/adapter.mjs +140 -0
  6. package/dist/core/errors.d.mts +2 -0
  7. package/dist/core/errors.mjs +74 -0
  8. package/dist/core/guards.d.mts +44 -0
  9. package/dist/core/guards.mjs +58 -0
  10. package/dist/core/renderer.d.mts +2 -0
  11. package/dist/core/renderer.mjs +71 -0
  12. package/dist/core/types.d.mts +3 -0
  13. package/dist/core/types.mjs +40 -0
  14. package/dist/core/walker.d.mts +14 -0
  15. package/dist/core/walker.mjs +366 -0
  16. package/dist/errors-DIKI2C78.d.mts +57 -0
  17. package/dist/html/a11y.d.mts +47 -0
  18. package/dist/html/a11y.mjs +81 -0
  19. package/dist/html/html.d.mts +135 -0
  20. package/dist/html/html.mjs +168 -0
  21. package/dist/html/renderToHtml.d.mts +32 -0
  22. package/dist/html/renderToHtml.mjs +352 -0
  23. package/dist/html/renderToHtmlStream.d.mts +58 -0
  24. package/dist/html/renderToHtmlStream.mjs +285 -0
  25. package/dist/html/styles.css +151 -0
  26. package/dist/openapi/components.d.mts +76 -0
  27. package/dist/openapi/components.mjs +223 -0
  28. package/dist/openapi/parser.d.mts +45 -0
  29. package/dist/openapi/parser.mjs +159 -0
  30. package/dist/react/SchemaComponent.d.mts +96 -0
  31. package/dist/react/SchemaComponent.mjs +283 -0
  32. package/dist/react/SchemaErrorBoundary.d.mts +26 -0
  33. package/dist/react/SchemaErrorBoundary.mjs +47 -0
  34. package/dist/react/headless.d.mts +13 -0
  35. package/dist/react/headless.mjs +163 -0
  36. package/dist/themes/shadcn.d.mts +6 -0
  37. package/dist/themes/shadcn.mjs +166 -0
  38. package/dist/types-BU0ETFHk.d.mts +326 -0
  39. package/package.json +113 -3
@@ -0,0 +1,366 @@
1
+ import { isObject } from "./guards.mjs";
2
+ import { resolveEditability } from "./types.mjs";
3
+ //#region src/core/walker.ts
4
+ function getString(obj, key) {
5
+ const value = obj[key];
6
+ return typeof value === "string" ? value : void 0;
7
+ }
8
+ function getNumber(obj, key) {
9
+ const value = obj[key];
10
+ return typeof value === "number" ? value : void 0;
11
+ }
12
+ function getArray(obj, key) {
13
+ const value = obj[key];
14
+ return Array.isArray(value) ? value : void 0;
15
+ }
16
+ function getObject(obj, key) {
17
+ const value = obj[key];
18
+ return isObject(value) ? value : void 0;
19
+ }
20
+ const MAX_REF_DEPTH = 10;
21
+ function resolveRef(schema, rootDocument, visited) {
22
+ const ref = getString(schema, "$ref");
23
+ if (ref === void 0) return schema;
24
+ if (visited.has(ref)) return { type: "unknown" };
25
+ if (visited.size >= MAX_REF_DEPTH) return { type: "unknown" };
26
+ const resolved = dereference(ref, rootDocument);
27
+ if (resolved === void 0) return { type: "unknown" };
28
+ const nextVisited = new Set(visited);
29
+ nextVisited.add(ref);
30
+ return resolveRef(resolved, rootDocument, nextVisited);
31
+ }
32
+ function dereference(ref, root) {
33
+ if (!ref.startsWith("#/")) return void 0;
34
+ const parts = ref.slice(2).split("/");
35
+ let current = root;
36
+ for (const part of parts) {
37
+ if (!isObject(current)) return void 0;
38
+ const decoded = part.replace(/~1/g, "/").replace(/~0/g, "~");
39
+ current = current[decoded];
40
+ }
41
+ return isObject(current) ? current : void 0;
42
+ }
43
+ /**
44
+ * Merge multiple JSON Schema objects from allOf into one.
45
+ * Merges: properties, required, meta fields, and constraints.
46
+ */
47
+ function mergeAllOf(schemas) {
48
+ const merged = {};
49
+ const properties = {};
50
+ const required = [];
51
+ for (const entry of schemas) {
52
+ if (!isObject(entry)) continue;
53
+ const props = getObject(entry, "properties");
54
+ if (props !== void 0) for (const [key, value] of Object.entries(props)) properties[key] = value;
55
+ const req = getArray(entry, "required");
56
+ if (req !== void 0) {
57
+ for (const r of req) if (typeof r === "string" && !required.includes(r)) required.push(r);
58
+ }
59
+ for (const [key, value] of Object.entries(entry)) {
60
+ if (key === "properties" || key === "required" || key === "allOf" || key === "type") continue;
61
+ if (!(key in merged)) merged[key] = value;
62
+ }
63
+ if (!("type" in merged)) {
64
+ const type = getString(entry, "type");
65
+ if (type !== void 0) merged.type = type;
66
+ }
67
+ }
68
+ if (Object.keys(properties).length > 0) merged.properties = properties;
69
+ if (required.length > 0) merged.required = required;
70
+ return merged;
71
+ }
72
+ /**
73
+ * Detect `anyOf: [T, { type: "null" }]` → nullable T.
74
+ * Returns the non-null schema and a nullable flag.
75
+ */
76
+ function normaliseAnyOf(options) {
77
+ if (options.length !== 2) return void 0;
78
+ let inner;
79
+ let hasNull = false;
80
+ for (const opt of options) {
81
+ if (!isObject(opt)) return void 0;
82
+ if (opt.type === "null") hasNull = true;
83
+ else inner = opt;
84
+ }
85
+ if (!hasNull || inner === void 0) return void 0;
86
+ return {
87
+ inner,
88
+ isNullable: true
89
+ };
90
+ }
91
+ /**
92
+ * Detect oneOf where every option is an object with a property
93
+ * that has a `const` value → discriminated union.
94
+ */
95
+ function detectDiscriminated(options) {
96
+ if (options.length === 0) return void 0;
97
+ let discriminator;
98
+ for (const opt of options) {
99
+ if (!isObject(opt)) return void 0;
100
+ const props = getObject(opt, "properties");
101
+ if (props === void 0) return void 0;
102
+ let foundKey;
103
+ for (const [key, value] of Object.entries(props)) if (isObject(value) && "const" in value) {
104
+ foundKey = key;
105
+ break;
106
+ }
107
+ if (foundKey === void 0) return void 0;
108
+ if (discriminator === void 0) discriminator = foundKey;
109
+ else if (discriminator !== foundKey) return;
110
+ }
111
+ if (discriminator === void 0) return void 0;
112
+ return {
113
+ options: options.filter(isObject),
114
+ discriminator
115
+ };
116
+ }
117
+ const META_KEYWORDS = new Set([
118
+ "readOnly",
119
+ "writeOnly",
120
+ "description",
121
+ "title",
122
+ "deprecated",
123
+ "default",
124
+ "component",
125
+ "example",
126
+ "examples"
127
+ ]);
128
+ function extractMetaFromJson(schema) {
129
+ const meta = {};
130
+ for (const [key, value] of Object.entries(schema)) if (META_KEYWORDS.has(key)) meta[key] = value;
131
+ return meta;
132
+ }
133
+ function extractConstraintsFromJson(schema) {
134
+ const constraints = {};
135
+ const minLength = getNumber(schema, "minLength");
136
+ if (minLength !== void 0) constraints.minLength = minLength;
137
+ const maxLength = getNumber(schema, "maxLength");
138
+ if (maxLength !== void 0) constraints.maxLength = maxLength;
139
+ const minimum = getNumber(schema, "minimum");
140
+ if (minimum !== void 0) constraints.minimum = minimum;
141
+ const maximum = getNumber(schema, "maximum");
142
+ if (maximum !== void 0) constraints.maximum = maximum;
143
+ const pattern = getString(schema, "pattern");
144
+ if (pattern !== void 0) constraints.pattern = pattern;
145
+ const format = getString(schema, "format");
146
+ if (format !== void 0) constraints.format = format;
147
+ const minItems = getNumber(schema, "minItems");
148
+ if (minItems !== void 0) constraints.minItems = minItems;
149
+ const maxItems = getNumber(schema, "maxItems");
150
+ if (maxItems !== void 0) constraints.maxItems = maxItems;
151
+ if (format === "binary") {
152
+ const contentMediaType = getString(schema, "contentMediaType");
153
+ if (contentMediaType !== void 0) constraints.mimeTypes = [contentMediaType];
154
+ }
155
+ return constraints;
156
+ }
157
+ const OVERRIDE_META_KEYS = new Set([
158
+ "readOnly",
159
+ "writeOnly",
160
+ "description",
161
+ "title",
162
+ "deprecated",
163
+ "component"
164
+ ]);
165
+ function extractSchemaMetaFields(overrides) {
166
+ if (overrides === void 0) return void 0;
167
+ const meta = {};
168
+ for (const key of Object.keys(overrides)) if (OVERRIDE_META_KEYS.has(key)) meta[key] = overrides[key];
169
+ return Object.keys(meta).length > 0 ? meta : void 0;
170
+ }
171
+ function extractChildOverride(overrides, key) {
172
+ if (overrides === void 0) return void 0;
173
+ const child = overrides[key];
174
+ if (child === void 0 || child === null) return void 0;
175
+ if (typeof child !== "object" || Array.isArray(child)) return void 0;
176
+ const result = {};
177
+ for (const [k, v] of Object.entries(child)) result[k] = v;
178
+ return Object.keys(result).length > 0 ? result : void 0;
179
+ }
180
+ function walk(schema, options = {}) {
181
+ const { componentMeta, rootMeta, fieldOverrides, rootDocument } = options;
182
+ if (!isObject(schema)) return {
183
+ type: "unknown",
184
+ editability: "editable",
185
+ meta: {},
186
+ constraints: {}
187
+ };
188
+ const doc = rootDocument ?? schema;
189
+ return walkNode(resolveRef(schema, doc, /* @__PURE__ */ new Set()), {
190
+ componentMeta,
191
+ rootMeta,
192
+ fieldOverrides,
193
+ rootDocument: doc,
194
+ isNullable: false,
195
+ isOptional: false,
196
+ defaultValue: void 0
197
+ });
198
+ }
199
+ function walkNode(schema, ctx) {
200
+ const allOf = getArray(schema, "allOf");
201
+ if (allOf !== void 0 && allOf.length > 0) return walkNode(mergeAllOf(allOf), ctx);
202
+ const anyOf = getArray(schema, "anyOf");
203
+ if (anyOf !== void 0) {
204
+ const nullable = normaliseAnyOf(anyOf);
205
+ if (nullable !== void 0) return walkNode(nullable.inner, {
206
+ ...ctx,
207
+ isNullable: true
208
+ });
209
+ return walkUnion(anyOf, ctx);
210
+ }
211
+ const oneOf = getArray(schema, "oneOf");
212
+ if (oneOf !== void 0) {
213
+ const discriminated = detectDiscriminated(oneOf);
214
+ if (discriminated !== void 0) return walkDiscriminatedUnion(discriminated, ctx);
215
+ return walkUnion(oneOf, ctx);
216
+ }
217
+ const resolved = resolveRef(schema, ctx.rootDocument, /* @__PURE__ */ new Set());
218
+ const enumValues = getArray(resolved, "enum");
219
+ if (enumValues !== void 0) return walkEnum(resolved, enumValues, ctx);
220
+ if ("const" in resolved) return walkLiteral(resolved, ctx);
221
+ const type = getString(resolved, "type");
222
+ if (type === void 0) return buildField(resolved, "unknown", ctx);
223
+ if (type === "string") return walkString(resolved, ctx);
224
+ if (type === "number" || type === "integer") return walkNumber(resolved, ctx);
225
+ if (type === "boolean") return walkBoolean(resolved, ctx);
226
+ if (type === "null") return buildField(resolved, "null", ctx);
227
+ if (type === "object") {
228
+ const properties = getObject(resolved, "properties");
229
+ if (properties !== void 0) return walkObject(resolved, properties, ctx);
230
+ const additionalProps = getObject(resolved, "additionalProperties");
231
+ if (additionalProps !== void 0) return walkRecord(resolved, additionalProps, ctx);
232
+ return buildField(resolved, "object", ctx);
233
+ }
234
+ if (type === "array") return walkArray(resolved, ctx);
235
+ return buildField(resolved, "unknown", ctx);
236
+ }
237
+ function walkString(schema, ctx) {
238
+ if (getString(schema, "format") === "binary") return buildField(schema, "file", ctx);
239
+ return buildField(schema, "string", ctx);
240
+ }
241
+ function walkNumber(schema, ctx) {
242
+ return buildField(schema, "number", ctx);
243
+ }
244
+ function walkBoolean(schema, ctx) {
245
+ return buildField(schema, "boolean", ctx);
246
+ }
247
+ function walkEnum(schema, enumValues, ctx) {
248
+ return {
249
+ ...buildField(schema, "enum", ctx),
250
+ enumValues: enumValues.filter((v) => typeof v === "string")
251
+ };
252
+ }
253
+ function walkLiteral(schema, ctx) {
254
+ const constValue = schema.const;
255
+ const values = isPrimitive(constValue) ? [constValue] : [];
256
+ return {
257
+ ...buildField(schema, "literal", ctx),
258
+ literalValues: values
259
+ };
260
+ }
261
+ function walkObject(schema, properties, ctx) {
262
+ const base = buildField(schema, "object", ctx);
263
+ const required = getArray(schema, "required");
264
+ const fields = {};
265
+ for (const [key, propSchema] of Object.entries(properties)) {
266
+ const childOverride = extractChildOverride(ctx.fieldOverrides, key);
267
+ const isRequired = required?.includes(key) === true;
268
+ const childCtx = {
269
+ ...ctx,
270
+ fieldOverrides: childOverride,
271
+ isOptional: !isRequired
272
+ };
273
+ const overrideMeta = extractSchemaMetaFields(childOverride);
274
+ if (overrideMeta !== void 0 && ("readOnly" in overrideMeta || "writeOnly" in overrideMeta)) childCtx.componentMeta = void 0;
275
+ if (isObject(propSchema)) fields[key] = walkNode(propSchema, childCtx);
276
+ else fields[key] = {
277
+ type: "unknown",
278
+ editability: "editable",
279
+ meta: {},
280
+ constraints: {}
281
+ };
282
+ }
283
+ return {
284
+ ...base,
285
+ fields
286
+ };
287
+ }
288
+ function walkRecord(schema, valueSchema, ctx) {
289
+ const base = buildField(schema, "record", ctx);
290
+ const propertyNames = getObject(schema, "propertyNames");
291
+ const keyType = propertyNames !== void 0 ? walkNode(propertyNames, ctx) : {
292
+ type: "string",
293
+ editability: "editable",
294
+ meta: {},
295
+ constraints: {}
296
+ };
297
+ const valueType = walkNode(valueSchema, ctx);
298
+ return {
299
+ ...base,
300
+ keyType,
301
+ valueType
302
+ };
303
+ }
304
+ function walkArray(schema, ctx) {
305
+ const base = buildField(schema, "array", ctx);
306
+ const items = getObject(schema, "items");
307
+ if (items !== void 0) {
308
+ const elementOverride = extractChildOverride(ctx.fieldOverrides, "[]");
309
+ return {
310
+ ...base,
311
+ element: walkNode(items, {
312
+ ...ctx,
313
+ fieldOverrides: elementOverride
314
+ })
315
+ };
316
+ }
317
+ return base;
318
+ }
319
+ function walkUnion(options, ctx) {
320
+ const optionsArray = options.filter(isObject);
321
+ return {
322
+ ...buildField({}, "union", ctx),
323
+ options: optionsArray.map((opt) => walkNode(opt, {
324
+ ...ctx,
325
+ fieldOverrides: void 0
326
+ }))
327
+ };
328
+ }
329
+ function walkDiscriminatedUnion(discriminated, ctx) {
330
+ return {
331
+ ...buildField({}, "discriminatedUnion", ctx),
332
+ options: discriminated.options.map((opt) => walkNode(opt, {
333
+ ...ctx,
334
+ fieldOverrides: void 0
335
+ })),
336
+ discriminator: discriminated.discriminator
337
+ };
338
+ }
339
+ function buildField(schema, type, ctx) {
340
+ const propertyMeta = extractMetaFromJson(schema);
341
+ const overrideMeta = extractSchemaMetaFields(ctx.fieldOverrides);
342
+ const mergedMeta = {
343
+ ...propertyMeta,
344
+ ...overrideMeta
345
+ };
346
+ const defaultValue = "default" in schema ? schema.default : void 0;
347
+ const editability = resolveEditability(mergedMeta, ctx.componentMeta, ctx.rootMeta);
348
+ if ((overrideMeta !== void 0 && ("readOnly" in overrideMeta || "writeOnly" in overrideMeta) || Boolean(propertyMeta.readOnly) || Boolean(propertyMeta.writeOnly)) && ctx.componentMeta !== void 0) ctx = {
349
+ ...ctx,
350
+ componentMeta: void 0
351
+ };
352
+ return {
353
+ type,
354
+ editability,
355
+ meta: mergedMeta,
356
+ isOptional: ctx.isOptional,
357
+ isNullable: ctx.isNullable,
358
+ defaultValue: defaultValue ?? ctx.defaultValue,
359
+ constraints: extractConstraintsFromJson(schema)
360
+ };
361
+ }
362
+ function isPrimitive(value) {
363
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
364
+ }
365
+ //#endregion
366
+ export { walk };
@@ -0,0 +1,57 @@
1
+ //#region src/core/errors.d.ts
2
+ /**
3
+ * Structured error types for schema-components.
4
+ *
5
+ * Every error produced by the library is one of these three types:
6
+ *
7
+ * - SchemaNormalisationError — the adapter failed to convert the input
8
+ * to JSON Schema (invalid Zod, bad OpenAPI ref, malformed schema)
9
+ * - SchemaRenderError — a theme adapter's render function threw
10
+ * - SchemaFieldError — a field path couldn't be resolved
11
+ *
12
+ * All extend `SchemaError` so consumers can catch the base class.
13
+ */
14
+ /**
15
+ * Base class for all schema-components errors.
16
+ * Catch this to handle any library error uniformly.
17
+ */
18
+ declare class SchemaError extends Error {
19
+ /** The schema input that caused the error. */
20
+ readonly schema: unknown;
21
+ constructor(message: string, schema: unknown);
22
+ }
23
+ /**
24
+ * The adapter failed to convert the input schema to JSON Schema.
25
+ *
26
+ * Causes: invalid Zod schema, Zod 3 schema (unsupported), malformed
27
+ * JSON Schema, missing OpenAPI ref, unsupported ref format.
28
+ */
29
+ declare class SchemaNormalisationError extends SchemaError {
30
+ readonly kind: "invalid-zod" | "zod3-unsupported" | "invalid-json-schema" | "openapi-missing-ref" | "openapi-invalid" | "unknown";
31
+ constructor(message: string, schema: unknown, kind: SchemaNormalisationError["kind"]);
32
+ }
33
+ /**
34
+ * A theme adapter's render function threw during rendering.
35
+ *
36
+ * The `cause` is the original error from the render function.
37
+ */
38
+ declare class SchemaRenderError extends SchemaError {
39
+ /** The schema type being rendered when the error occurred. */
40
+ readonly schemaType: string;
41
+ /** The original error from the render function. */
42
+ readonly cause: unknown;
43
+ constructor(message: string, schema: unknown, schemaType: string, cause: unknown);
44
+ }
45
+ /**
46
+ * A field path couldn't be resolved against the walked schema tree.
47
+ *
48
+ * This is produced by `<SchemaField>` when the `path` prop doesn't
49
+ * match any field in the schema.
50
+ */
51
+ declare class SchemaFieldError extends SchemaError {
52
+ /** The unresolvable dot-separated path. */
53
+ readonly path: string;
54
+ constructor(message: string, schema: unknown, path: string);
55
+ }
56
+ //#endregion
57
+ export { SchemaRenderError as i, SchemaFieldError as n, SchemaNormalisationError as r, SchemaError as t };
@@ -0,0 +1,47 @@
1
+ import { _ as WalkedField, n as FieldConstraints } from "../types-BU0ETFHk.mjs";
2
+ import { HtmlAttributes, HtmlNode } from "./html.mjs";
3
+
4
+ //#region src/html/a11y.d.ts
5
+ /**
6
+ * Build the input ID for a field at a given path.
7
+ */
8
+ declare function buildInputId(path: string, key: string): string;
9
+ /**
10
+ * Derive the hint element ID from the input ID.
11
+ */
12
+ declare function buildHintId(inputId: string): string;
13
+ /**
14
+ * Build a human-readable constraint description string.
15
+ * Returns undefined if no constraints are present.
16
+ */
17
+ declare function constraintHint(c: FieldConstraints): string | undefined;
18
+ /**
19
+ * Build `aria-required` attribute for required fields.
20
+ * Returns an object to spread into `h()` attributes, or empty object.
21
+ */
22
+ declare function ariaRequiredAttrs(tree: WalkedField): Pick<HtmlAttributes, "aria-required"> | undefined;
23
+ /**
24
+ * Build `aria-describedby` attribute pointing to the constraint hint element.
25
+ * Only present when constraints exist.
26
+ */
27
+ declare function ariaDescribedByAttrs(inputId: string, constraints: FieldConstraints): Pick<HtmlAttributes, "aria-describedby"> | undefined;
28
+ /**
29
+ * Build `aria-readonly` attribute for read-only presentation.
30
+ */
31
+ declare function ariaReadonlyAttrs(): Pick<HtmlAttributes, "aria-readonly">;
32
+ /**
33
+ * Build `aria-label` attribute from description, if present.
34
+ */
35
+ declare function ariaLabelAttrs(description: unknown): Pick<HtmlAttributes, "aria-label"> | undefined;
36
+ /**
37
+ * Build a `<small class="sc-hint">` element for constraint hints.
38
+ * Returns undefined if no constraints are present.
39
+ */
40
+ declare function buildHintElement(inputId: string, constraints: FieldConstraints): HtmlNode;
41
+ /**
42
+ * Build the required-field asterisk indicator for labels.
43
+ * Returns undefined if the field is optional.
44
+ */
45
+ declare function requiredIndicator(field: WalkedField): HtmlNode;
46
+ //#endregion
47
+ export { ariaDescribedByAttrs, ariaLabelAttrs, ariaReadonlyAttrs, ariaRequiredAttrs, buildHintElement, buildHintId, buildInputId, constraintHint, requiredIndicator };
@@ -0,0 +1,81 @@
1
+ import { h } from "./html.mjs";
2
+ //#region src/html/a11y.ts
3
+ /**
4
+ * Build the input ID for a field at a given path.
5
+ */
6
+ function buildInputId(path, key) {
7
+ return `sc-${path ? `${path}-${key}` : key}`;
8
+ }
9
+ /**
10
+ * Derive the hint element ID from the input ID.
11
+ */
12
+ function buildHintId(inputId) {
13
+ return `${inputId}-hint`;
14
+ }
15
+ /**
16
+ * Build a human-readable constraint description string.
17
+ * Returns undefined if no constraints are present.
18
+ */
19
+ function constraintHint(c) {
20
+ const parts = [];
21
+ if (c.minLength !== void 0) parts.push(`Minimum ${String(c.minLength)} characters`);
22
+ if (c.maxLength !== void 0) parts.push(`Maximum ${String(c.maxLength)} characters`);
23
+ if (c.minimum !== void 0) parts.push(`Minimum ${String(c.minimum)}`);
24
+ if (c.maximum !== void 0) parts.push(`Maximum ${String(c.maximum)}`);
25
+ if (c.pattern !== void 0 && c.format === void 0) parts.push("Must match pattern");
26
+ if (c.minItems !== void 0) parts.push(`Minimum ${String(c.minItems)} items`);
27
+ if (c.maxItems !== void 0) parts.push(`Maximum ${String(c.maxItems)} items`);
28
+ if (parts.length === 0) return void 0;
29
+ return parts.join(". ");
30
+ }
31
+ /**
32
+ * Build `aria-required` attribute for required fields.
33
+ * Returns an object to spread into `h()` attributes, or empty object.
34
+ */
35
+ function ariaRequiredAttrs(tree) {
36
+ if (tree.isOptional === false) return { "aria-required": "true" };
37
+ }
38
+ /**
39
+ * Build `aria-describedby` attribute pointing to the constraint hint element.
40
+ * Only present when constraints exist.
41
+ */
42
+ function ariaDescribedByAttrs(inputId, constraints) {
43
+ if (constraintHint(constraints) === void 0) return void 0;
44
+ return { "aria-describedby": buildHintId(inputId) };
45
+ }
46
+ /**
47
+ * Build `aria-readonly` attribute for read-only presentation.
48
+ */
49
+ function ariaReadonlyAttrs() {
50
+ return { "aria-readonly": "true" };
51
+ }
52
+ /**
53
+ * Build `aria-label` attribute from description, if present.
54
+ */
55
+ function ariaLabelAttrs(description) {
56
+ if (typeof description === "string" && description.length > 0) return { "aria-label": description };
57
+ }
58
+ /**
59
+ * Build a `<small class="sc-hint">` element for constraint hints.
60
+ * Returns undefined if no constraints are present.
61
+ */
62
+ function buildHintElement(inputId, constraints) {
63
+ const hint = constraintHint(constraints);
64
+ if (hint === void 0) return void 0;
65
+ return h("small", {
66
+ class: "sc-hint",
67
+ id: buildHintId(inputId)
68
+ }, hint);
69
+ }
70
+ /**
71
+ * Build the required-field asterisk indicator for labels.
72
+ * Returns undefined if the field is optional.
73
+ */
74
+ function requiredIndicator(field) {
75
+ if (field.isOptional === false) return h("span", {
76
+ class: "sc-required",
77
+ "aria-hidden": "true"
78
+ }, " *");
79
+ }
80
+ //#endregion
81
+ export { ariaDescribedByAttrs, ariaLabelAttrs, ariaReadonlyAttrs, ariaRequiredAttrs, buildHintElement, buildHintId, buildInputId, constraintHint, requiredIndicator };
@@ -0,0 +1,135 @@
1
+ //#region src/html/html.d.ts
2
+ /**
3
+ * Typed HTML builder — structured HTML construction with compile-time safety.
4
+ *
5
+ * Instead of string templates, renderers call `h(tag, attrs, ...children)` to
6
+ * build an AST, then `serialize()` converts it to an HTML string. This gives:
7
+ *
8
+ * - Compile-time checking of tag names and attribute keys
9
+ * - Automatic HTML escaping (serialiser handles it — callers never escape manually)
10
+ * - Streaming via `serializeChunks()` which yields at element boundaries
11
+ * - Zero dependencies
12
+ *
13
+ * Usage:
14
+ *
15
+ * import { h, serialize } from "./html.ts";
16
+ *
17
+ * const el = h("input", { type: "text", id: "name", "aria-required": true });
18
+ * serialize(el); // → '<input type="text" id="name" aria-required>'
19
+ *
20
+ * const form = h("form", {},
21
+ * h("label", { for: "name" }, "Name"),
22
+ * h("input", { type: "text", id: "name" }),
23
+ * );
24
+ * serialize(form); // → '<form><label for="name">Name</label><input type="text" id="name"></form>'
25
+ */
26
+ /**
27
+ * An HTML element node. Void elements (input, br, etc.) have no children
28
+ * in the serialiser regardless of what's passed.
29
+ */
30
+ interface HtmlElement {
31
+ readonly tag: string;
32
+ readonly attributes: Readonly<HtmlAttributes>;
33
+ readonly children: readonly (HtmlElement | HtmlText | HtmlRaw | string)[];
34
+ }
35
+ /**
36
+ * A text node. The `text` value is stored raw (unescaped) — the serialiser
37
+ * escapes it during output. Callers should NOT pre-escape.
38
+ */
39
+ interface HtmlText {
40
+ readonly text: string;
41
+ }
42
+ /**
43
+ * A raw HTML node. The `html` value is emitted verbatim — NOT escaped.
44
+ * Use for embedding already-serialised HTML from resolvers or external sources.
45
+ * Never use for user-supplied data.
46
+ */
47
+ interface HtmlRaw {
48
+ readonly html: string;
49
+ }
50
+ /**
51
+ * Any node that can appear in the HTML tree.
52
+ * - `string` is treated as a text node (will be escaped by the serialiser)
53
+ * - `HtmlElement` and `HtmlText` are structured nodes
54
+ * - `undefined` and `null` are silently dropped (useful for conditional children)
55
+ * - `false` is silently dropped (useful for `{condition && h(...)}`)
56
+ */
57
+ type HtmlNode = HtmlElement | HtmlText | HtmlRaw | string | undefined | null | false;
58
+ /**
59
+ * Attribute value types. `true` renders as a boolean attribute (`disabled`),
60
+ * `false` and `undefined` are omitted. Numbers are converted to strings.
61
+ */
62
+ type AttrValue = string | number | boolean | undefined;
63
+ /**
64
+ * HTML attributes. Standard attributes are typed per-element via overloads;
65
+ * arbitrary `data-*` and `aria-*` keys are allowed via index signature.
66
+ */
67
+ type HtmlAttributes = Record<string, AttrValue>;
68
+ declare const VOID_ELEMENTS: Set<string>;
69
+ /**
70
+ * Build an HTML element node.
71
+ *
72
+ * - Tag name is type-checked (must be a known HTML tag)
73
+ * - Attributes are collected as a record — callers get IntelliSense for
74
+ * common attributes but can also pass `aria-*`, `data-*` etc.
75
+ * - Children are flattened; `undefined`, `null`, and `false` are dropped.
76
+ * - For void elements (input, img, etc.), children are ignored.
77
+ *
78
+ * @param tag - HTML element tag name
79
+ * @param attrs - Optional attributes (class, id, aria-*, etc.)
80
+ * @param children - Child nodes (strings are escaped by the serialiser)
81
+ */
82
+ declare function h(tag: string, attrs?: HtmlAttributes, ...children: HtmlNode[]): HtmlElement;
83
+ /**
84
+ * Create a text node. The value is NOT escaped — the serialiser handles it.
85
+ * Use this for dynamic text that must appear in the output.
86
+ */
87
+ declare function text(value: string): HtmlText;
88
+ /**
89
+ * Create a raw HTML node. The value is emitted verbatim — NOT escaped.
90
+ * Use for embedding already-serialised HTML (e.g. from child renderers).
91
+ * Never use for user-supplied data.
92
+ */
93
+ declare function raw(html: string): HtmlRaw;
94
+ /**
95
+ * Serialise an HTML node to a string.
96
+ *
97
+ * - Text content is automatically escaped
98
+ * - Void elements are self-closing
99
+ * - Boolean attributes render as just the name (`disabled`, `checked`)
100
+ * - `false`/`undefined` attribute values are omitted
101
+ *
102
+ * @param node - An HtmlElement, HtmlText, or string to serialise
103
+ * @returns HTML string
104
+ */
105
+ declare function serialize(node: HtmlNode): string;
106
+ declare function serializeElement(el: HtmlElement): string;
107
+ declare function serializeAttributes(attrs: HtmlAttributes): string;
108
+ /**
109
+ * Serialise an HTML node to chunks, yielded at natural element boundaries.
110
+ *
111
+ * - Each top-level child element becomes its own chunk
112
+ * - Leaf text within an element stays with its parent
113
+ * - Void elements are single chunks
114
+ *
115
+ * This is used by the streaming renderer to produce incremental output.
116
+ *
117
+ * @param node - An HTML node to serialise
118
+ * @returns Iterable of HTML string chunks
119
+ */
120
+ declare function serializeChunks(node: HtmlNode): Iterable<string, void, undefined>;
121
+ /**
122
+ * Escape a string for safe inclusion in HTML text content or attribute values.
123
+ */
124
+ declare function escapeHtml(str: string): string;
125
+ /**
126
+ * Create a fragment: children rendered sequentially with no wrapping element.
127
+ * Useful when a renderer needs to return multiple top-level nodes.
128
+ */
129
+ declare function fragment(...children: HtmlNode[]): HtmlElement;
130
+ /**
131
+ * Serialise a node, treating fragments (empty tag) as just their children.
132
+ */
133
+ declare function serializeFragment(node: HtmlNode): string;
134
+ //#endregion
135
+ export { AttrValue, HtmlAttributes, HtmlElement, HtmlNode, HtmlRaw, HtmlText, VOID_ELEMENTS, escapeHtml, fragment, h, raw, serialize, serializeAttributes, serializeChunks, serializeElement, serializeFragment, text };