schema-components 1.16.1 → 1.16.3

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.
@@ -1,8 +1,8 @@
1
1
  import { isObject } from "./guards.mjs";
2
2
  import { appendPointer, emitDiagnostic } from "./diagnostics.mjs";
3
3
  import { countDistinctRefs, resolveRef } from "./ref.mjs";
4
- import { ANNOTATION_SIBLINGS, detectDiscriminated, mergeAllOf, mergeRefSiblings, normaliseAnyOf } from "./merge.mjs";
5
4
  import { extractArrayConstraints, extractObjectConstraints, stripInapplicableConstraints } from "./constraints.mjs";
5
+ import { ANNOTATION_SIBLINGS, detectDiscriminated, mergeAllOf, mergeRefSiblings, normaliseAnyOf } from "./merge.mjs";
6
6
  import { buildBase, buildBooleanField, buildFileField, buildNullField, buildNumberField, buildStringField, buildUnknownField, extractChildOverride, extractSchemaMetaFields, getArray, getObject, getString, isPrimitive, walkDependentRequiredMap, walkSubSchemaMap, withoutKeys } from "./walkBuilders.mjs";
7
7
  //#region src/core/walker.ts
8
8
  /**
@@ -1,7 +1,7 @@
1
- import { h, serialize } from "./html.mjs";
2
1
  import { normaliseSchema } from "../core/adapter.mjs";
3
- import { walk } from "../core/walker.mjs";
4
2
  import { getHtmlRenderFn, mergeHtmlResolvers } from "../core/renderer.mjs";
3
+ import { walk } from "../core/walker.mjs";
4
+ import { h, serialize } from "./html.mjs";
5
5
  import { defaultHtmlResolver } from "./renderers.mjs";
6
6
  //#region src/html/renderToHtml.ts
7
7
  /**
@@ -1,6 +1,6 @@
1
1
  import { normaliseSchema } from "../core/adapter.mjs";
2
- import { walk } from "../core/walker.mjs";
3
2
  import { mergeHtmlResolvers } from "../core/renderer.mjs";
3
+ import { walk } from "../core/walker.mjs";
4
4
  import { defaultHtmlResolver } from "./renderers.mjs";
5
5
  import { streamField } from "./streamRenderers.mjs";
6
6
  //#region src/html/renderToHtmlStream.ts
@@ -1,7 +1,7 @@
1
- import { VOID_ELEMENTS, h, raw, serialize, serializeAttributes } from "./html.mjs";
2
- import { ariaLabelAttrs, buildHintElement, buildInputId, requiredIndicator } from "./a11y.mjs";
3
1
  import { isObject } from "../core/guards.mjs";
4
2
  import { getHtmlRenderFn } from "../core/renderer.mjs";
3
+ import { VOID_ELEMENTS, h, raw, serialize, serializeAttributes } from "./html.mjs";
4
+ import { ariaLabelAttrs, buildHintElement, buildInputId, requiredIndicator } from "./a11y.mjs";
5
5
  //#region src/html/streamRenderers.ts
6
6
  function yieldOpen(el) {
7
7
  const attrStr = serializeAttributes(el.attributes);
@@ -2,12 +2,12 @@ import { toRecordOrUndefined } from "../core/guards.mjs";
2
2
  import { SchemaNormalisationError } from "../core/errors.mjs";
3
3
  import { normaliseSchema } from "../core/adapter.mjs";
4
4
  import { walk } from "../core/walker.mjs";
5
- import { renderField } from "../react/SchemaComponent.mjs";
6
5
  import { ApiCallbacks } from "./ApiCallbacks.mjs";
7
6
  import { ApiLinks } from "./ApiLinks.mjs";
8
7
  import { ApiResponseHeaders } from "./ApiResponseHeaders.mjs";
9
8
  import { ApiSecurity } from "./ApiSecurity.mjs";
10
9
  import { getLinks, getSecurityRequirements, getSecuritySchemes, listCallbacks } from "./parser.mjs";
10
+ import { renderField } from "../react/SchemaComponent.mjs";
11
11
  import { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
13
  //#region src/openapi/components.tsx
@@ -4,8 +4,8 @@ import { t as SchemaError } from "../errors-C5zRC2PU.mjs";
4
4
  import { l as RenderProps, r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
5
5
  import { c as ResolveOpenAPIRef, s as PathOfType, t as FromJSONSchema } from "../typeInference-k7FXfTVO.mjs";
6
6
  import { z } from "zod";
7
- import { ReactNode } from "react";
8
7
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
8
+ import { ReactNode } from "react";
9
9
 
10
10
  //#region src/react/SchemaComponent.d.ts
11
11
  /**
@@ -2,13 +2,13 @@
2
2
  import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
4
4
  import { normaliseSchema } from "../core/adapter.mjs";
5
- import { walk } from "../core/walker.mjs";
6
5
  import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
6
+ import { walk } from "../core/walker.mjs";
7
7
  import { headlessResolver } from "./headless.mjs";
8
8
  import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
9
9
  import { z } from "zod";
10
- import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
11
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
12
12
  //#region src/react/SchemaComponent.tsx
13
13
  /**
14
14
  * <SchemaComponent> — renders UI from Zod, JSON Schema, or OpenAPI schemas.
@@ -1,10 +1,10 @@
1
1
  import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
2
2
  import { normaliseSchema } from "../core/adapter.mjs";
3
- import { walk } from "../core/walker.mjs";
4
3
  import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
4
+ import { walk } from "../core/walker.mjs";
5
5
  import { headlessResolver } from "./headless.mjs";
6
- import { createElement, isValidElement } from "react";
7
6
  import { jsx } from "react/jsx-runtime";
7
+ import { createElement, isValidElement } from "react";
8
8
  //#region src/react/SchemaView.tsx
9
9
  /**
10
10
  * React Server Component for read-only schema rendering.
@@ -1,3 +1,4 @@
1
+ import { M as WalkedField } from "../types-D_5ST7SS.mjs";
1
2
  import { l as RenderProps } from "../renderer-BdSqllx5.mjs";
2
3
  import { ReactNode } from "react";
3
4
 
@@ -12,12 +13,37 @@ declare function renderNumber(props: RenderProps): ReactNode;
12
13
  declare function renderBoolean(props: RenderProps): ReactNode;
13
14
  declare function renderEnum(props: RenderProps): ReactNode;
14
15
  declare function renderObject(props: RenderProps): ReactNode;
16
+ /**
17
+ * Compute the default value for a freshly added record entry based on the
18
+ * record's value-type schema. Mirrors the read of `defaultValue` used
19
+ * elsewhere in the renderer, falling back to a type-appropriate empty value.
20
+ */
21
+ declare function defaultRecordValue(valueType: WalkedField): unknown;
22
+ /**
23
+ * Generate a unique, currently-unused key for a new record entry.
24
+ * Picks the first of `key`, `key-1`, `key-2`, … that is not in `existing`.
25
+ */
26
+ declare function nextRecordKey(existing: readonly string[], base?: string): string;
27
+ /**
28
+ * Rename a key in an object while preserving insertion order. Returns the
29
+ * original object reference when the rename is a no-op (oldKey === newKey)
30
+ * or when newKey collides with an existing key.
31
+ */
32
+ declare function renameRecordKey(obj: Record<string, unknown>, oldKey: string, newKey: string): Record<string, unknown>;
15
33
  declare function renderRecord(props: RenderProps): ReactNode;
16
34
  declare function renderArray(props: RenderProps): ReactNode;
17
35
  declare function renderUnion(props: RenderProps): ReactNode;
18
36
  declare function renderDiscriminatedUnion(props: RenderProps): ReactNode;
37
+ /**
38
+ * Pure helper: convert a tab index into the new value the discriminated
39
+ * union should emit. Returns `undefined` when the index is out of bounds.
40
+ *
41
+ * Extracted from `DiscriminatedUnionTabs` so the contract is unit-testable
42
+ * without rendering the tabs component (which relies on React hooks).
43
+ */
44
+ declare function discriminatedUnionValueForTab(optionLabels: readonly string[], discKey: string, newIndex: number): Record<string, string> | undefined;
19
45
  declare function renderFile(props: RenderProps): ReactNode;
20
46
  declare function renderRecursive(props: RenderProps): ReactNode;
21
47
  declare function renderUnknown(props: RenderProps): ReactNode;
22
48
  //#endregion
23
- export { renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
49
+ export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
@@ -1,6 +1,6 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
- import { isValidElement, useCallback, useRef } from "react";
3
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { isValidElement, useCallback, useRef } from "react";
4
4
  //#region src/react/headlessRenderers.tsx
5
5
  /**
6
6
  * Headless renderer functions — one per schema type.
@@ -264,31 +264,123 @@ function renderObject(props) {
264
264
  }), child] }, key);
265
265
  })] });
266
266
  }
267
+ /**
268
+ * Compute the default value for a freshly added record entry based on the
269
+ * record's value-type schema. Mirrors the read of `defaultValue` used
270
+ * elsewhere in the renderer, falling back to a type-appropriate empty value.
271
+ */
272
+ function defaultRecordValue(valueType) {
273
+ if (valueType.defaultValue !== void 0) return valueType.defaultValue;
274
+ switch (valueType.type) {
275
+ case "string": return "";
276
+ case "number": return 0;
277
+ case "boolean": return false;
278
+ case "array": return [];
279
+ case "object":
280
+ case "record": return {};
281
+ default: return;
282
+ }
283
+ }
284
+ /**
285
+ * Generate a unique, currently-unused key for a new record entry.
286
+ * Picks the first of `key`, `key-1`, `key-2`, … that is not in `existing`.
287
+ */
288
+ function nextRecordKey(existing, base = "key") {
289
+ if (!existing.includes(base)) return base;
290
+ let i = 1;
291
+ while (existing.includes(`${base}-${String(i)}`)) i += 1;
292
+ return `${base}-${String(i)}`;
293
+ }
294
+ /**
295
+ * Rename a key in an object while preserving insertion order. Returns the
296
+ * original object reference when the rename is a no-op (oldKey === newKey)
297
+ * or when newKey collides with an existing key.
298
+ */
299
+ function renameRecordKey(obj, oldKey, newKey) {
300
+ if (oldKey === newKey) return obj;
301
+ if (newKey in obj && newKey !== oldKey) return obj;
302
+ const renamed = {};
303
+ for (const [k, v] of Object.entries(obj)) renamed[k === oldKey ? newKey : k] = v;
304
+ return renamed;
305
+ }
267
306
  function renderRecord(props) {
268
307
  const obj = isObject(props.value) ? props.value : {};
269
308
  const valueType = props.valueType;
270
309
  if (valueType === void 0) return null;
271
310
  const entries = Object.entries(obj);
272
- if (entries.length === 0) return /* @__PURE__ */ jsx("span", {
273
- "aria-readonly": props.readOnly ? "true" : void 0,
274
- children: ""
275
- });
276
- return /* @__PURE__ */ jsx("div", {
311
+ if (props.readOnly) {
312
+ if (entries.length === 0) return /* @__PURE__ */ jsx("span", {
313
+ "aria-readonly": "true",
314
+ children: "—"
315
+ });
316
+ return /* @__PURE__ */ jsx("div", {
317
+ role: "group",
318
+ "aria-label": props.meta.description ?? "Record",
319
+ children: entries.map(([key, value]) => {
320
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
321
+ htmlFor: inputId(props.path ? `${props.path}.${key}` : key),
322
+ children: key
323
+ }), toReactNode(props.renderChild(valueType, value, () => {}))] }, key);
324
+ })
325
+ });
326
+ }
327
+ const handleRename = (oldKey, newKey) => {
328
+ const renamed = renameRecordKey(obj, oldKey, newKey);
329
+ if (renamed === obj) return;
330
+ props.onChange(renamed);
331
+ };
332
+ const handleValueChange = (key, nextValue) => {
333
+ const updated = {};
334
+ for (const [k, val] of Object.entries(obj)) updated[k] = val;
335
+ updated[key] = nextValue;
336
+ props.onChange(updated);
337
+ };
338
+ const handleRemove = (key) => {
339
+ const next = {};
340
+ for (const [k, v] of Object.entries(obj)) {
341
+ if (k === key) continue;
342
+ next[k] = v;
343
+ }
344
+ props.onChange(next);
345
+ };
346
+ const handleAdd = () => {
347
+ const newKey = nextRecordKey(Object.keys(obj));
348
+ const next = { ...obj };
349
+ next[newKey] = defaultRecordValue(valueType);
350
+ props.onChange(next);
351
+ };
352
+ return /* @__PURE__ */ jsxs("div", {
277
353
  role: "group",
278
354
  "aria-label": props.meta.description ?? "Record",
279
- children: entries.map(([key, value]) => {
280
- const childId = inputId(props.path ? `${props.path}.${key}` : key);
281
- const childOnChange = (nextValue) => {
282
- const updated = {};
283
- for (const [k, val] of Object.entries(obj)) updated[k] = val;
284
- updated[key] = nextValue;
285
- props.onChange(updated);
286
- };
287
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
288
- htmlFor: childId,
289
- children: key
290
- }), toReactNode(props.renderChild(valueType, value, childOnChange))] }, key);
291
- })
355
+ children: [entries.map(([key, value]) => {
356
+ return /* @__PURE__ */ jsxs("div", { children: [
357
+ /* @__PURE__ */ jsx("input", {
358
+ id: `${inputId(props.path ? `${props.path}.${key}` : key)}-key`,
359
+ type: "text",
360
+ "aria-label": "Entry key",
361
+ defaultValue: key,
362
+ onBlur: (e) => {
363
+ handleRename(key, e.target.value);
364
+ }
365
+ }),
366
+ toReactNode(props.renderChild(valueType, value, (nextValue) => {
367
+ handleValueChange(key, nextValue);
368
+ })),
369
+ /* @__PURE__ */ jsx("button", {
370
+ type: "button",
371
+ "aria-label": `Remove entry ${key}`,
372
+ onClick: () => {
373
+ handleRemove(key);
374
+ },
375
+ children: "Remove"
376
+ })
377
+ ] }, key);
378
+ }), /* @__PURE__ */ jsx("button", {
379
+ type: "button",
380
+ "aria-label": "Add entry",
381
+ onClick: handleAdd,
382
+ children: "Add"
383
+ })]
292
384
  });
293
385
  }
294
386
  function renderArray(props) {
@@ -362,6 +454,18 @@ function renderDiscriminatedUnion(props) {
362
454
  });
363
455
  }
364
456
  /**
457
+ * Pure helper: convert a tab index into the new value the discriminated
458
+ * union should emit. Returns `undefined` when the index is out of bounds.
459
+ *
460
+ * Extracted from `DiscriminatedUnionTabs` so the contract is unit-testable
461
+ * without rendering the tabs component (which relies on React hooks).
462
+ */
463
+ function discriminatedUnionValueForTab(optionLabels, discKey, newIndex) {
464
+ const label = optionLabels[newIndex];
465
+ if (label === void 0) return void 0;
466
+ return { [discKey]: label };
467
+ }
468
+ /**
365
469
  * WAI-ARIA tabs component for discriminated unions.
366
470
  * Implements the full tabs keyboard pattern:
367
471
  * - Left/Right arrow keys move between tabs
@@ -372,9 +476,9 @@ function renderDiscriminatedUnion(props) {
372
476
  function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
373
477
  const tabRefs = useRef([]);
374
478
  const handleTabChange = useCallback((newIndex) => {
375
- const label = optionLabels[newIndex];
376
- if (label === void 0) return;
377
- props.onChange({ [discKey]: label });
479
+ const next = discriminatedUnionValueForTab(optionLabels, discKey, newIndex);
480
+ if (next === void 0) return;
481
+ props.onChange(next);
378
482
  }, [
379
483
  optionLabels,
380
484
  discKey,
@@ -504,4 +608,4 @@ function matchUnionOption(options, value) {
504
608
  if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
505
609
  }
506
610
  //#endregion
507
- export { renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
611
+ export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
@@ -1,8 +1,8 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
2
  import { toReactNode } from "../react/headlessRenderers.mjs";
3
3
  import { headlessResolver } from "../react/headless.mjs";
4
- import { isValidElement } from "react";
5
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { isValidElement } from "react";
6
6
  //#region src/themes/mui.tsx
7
7
  function ariaRequired(tree) {
8
8
  return { required: tree.isOptional === false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.16.1",
3
+ "version": "1.16.3",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",