schema-components 1.28.2 → 2.0.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/README.md +38 -16
- package/dist/core/adapter.d.mts +213 -3
- package/dist/core/adapter.mjs +21 -2
- package/dist/core/constraintHint.d.mts +15 -0
- package/dist/core/constraintHint.mjs +24 -0
- package/dist/core/constraints.d.mts +34 -2
- package/dist/core/constraints.mjs +33 -1
- package/dist/core/cssClasses.d.mts +1 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +22 -12
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +7 -1
- package/dist/core/formats.mjs +6 -0
- package/dist/core/idPath.d.mts +35 -5
- package/dist/core/idPath.mjs +79 -7
- package/dist/core/inferValue.d.mts +2 -0
- package/dist/core/inferValue.mjs +1 -0
- package/dist/core/limits.d.mts +1 -1
- package/dist/core/limits.mjs +6 -0
- package/dist/core/merge.d.mts +22 -1
- package/dist/core/merge.mjs +66 -3
- package/dist/core/normalise.d.mts +17 -2
- package/dist/core/normalise.mjs +1 -1
- package/dist/core/openapi30.mjs +1 -1
- package/dist/core/openapiConstants.d.mts +1 -0
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/refChain.d.mts +3 -4
- package/dist/core/refChain.mjs +2 -3
- package/dist/core/renderer.d.mts +199 -2
- package/dist/core/renderer.mjs +5 -0
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +3 -3
- package/dist/core/types.d.mts +1 -1
- package/dist/core/types.mjs +17 -0
- package/dist/core/unionMatch.d.mts +1 -1
- package/dist/core/uri.d.mts +12 -4
- package/dist/core/uri.mjs +30 -4
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +63 -6
- package/dist/core/walkBuilders.mjs +33 -1
- package/dist/core/walker.d.mts +14 -1
- package/dist/core/walker.mjs +18 -0
- package/dist/{diagnostics-Cbwak-ZX.d.mts → diagnostics-BTrm3O6J.d.mts} +9 -1
- package/dist/{errors-DQSIK4n1.d.mts → errors-Dki7tji4.d.mts} +23 -13
- package/dist/html/a11y.d.mts +3 -7
- package/dist/html/a11y.mjs +1 -16
- package/dist/html/html.d.mts +11 -0
- package/dist/html/html.mjs +11 -0
- package/dist/html/renderToHtml.d.mts +45 -12
- package/dist/html/renderToHtml.mjs +20 -4
- package/dist/html/renderToHtmlStream.d.mts +63 -18
- package/dist/html/renderToHtmlStream.mjs +34 -8
- package/dist/html/renderers.d.mts +6 -31
- package/dist/html/renderers.mjs +45 -91
- package/dist/html/streamRenderers.d.mts +31 -3
- package/dist/html/streamRenderers.mjs +41 -8
- package/dist/inferValue-PPXWJpbN.d.mts +77 -0
- package/dist/{limits-DJhgx5Ay.d.mts → limits-x4OiyJxh.d.mts} +6 -0
- package/dist/{normalise-Db1xaxgx.mjs → normalise-DB-Xtjmn.mjs} +43 -2
- package/dist/openapi/ApiCallbacks.d.mts +13 -1
- package/dist/openapi/ApiCallbacks.mjs +7 -0
- package/dist/openapi/ApiLinks.d.mts +13 -1
- package/dist/openapi/ApiLinks.mjs +7 -0
- package/dist/openapi/ApiResponseHeaders.d.mts +13 -1
- package/dist/openapi/ApiResponseHeaders.mjs +7 -0
- package/dist/openapi/ApiSecurity.d.mts +14 -1
- package/dist/openapi/ApiSecurity.mjs +29 -8
- package/dist/openapi/bundle.d.mts +31 -0
- package/dist/openapi/components.d.mts +135 -20
- package/dist/openapi/components.mjs +90 -15
- package/dist/openapi/parser.d.mts +140 -13
- package/dist/openapi/parser.mjs +84 -12
- package/dist/openapi/resolve.d.mts +42 -47
- package/dist/openapi/resolve.mjs +62 -56
- package/dist/react/SchemaComponent.d.mts +90 -88
- package/dist/react/SchemaComponent.mjs +74 -2
- package/dist/react/SchemaErrorBoundary.d.mts +18 -1
- package/dist/react/SchemaErrorBoundary.mjs +13 -1
- package/dist/react/SchemaView.d.mts +39 -11
- package/dist/react/SchemaView.mjs +23 -6
- package/dist/react/a11y.d.mts +74 -7
- package/dist/react/a11y.mjs +67 -6
- package/dist/react/fieldPath.d.mts +16 -1
- package/dist/react/fieldPath.mjs +25 -1
- package/dist/react/fieldShell.d.mts +49 -0
- package/dist/react/fieldShell.mjs +37 -0
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +13 -2
- package/dist/react/headlessRenderers.mjs +134 -54
- package/dist/{ref-TdeMfaV_.d.mts → ref-DdsbekXX.d.mts} +33 -1
- package/dist/themes/mantine.d.mts +54 -12
- package/dist/themes/mantine.mjs +195 -140
- package/dist/themes/mui.d.mts +64 -11
- package/dist/themes/mui.mjs +277 -213
- package/dist/themes/radix.d.mts +67 -15
- package/dist/themes/radix.mjs +235 -170
- package/dist/themes/shadcn.d.mts +25 -1
- package/dist/themes/shadcn.mjs +112 -91
- package/dist/{types-BTB73MB8.d.mts → types-BrYbjC7_.d.mts} +30 -0
- package/dist/{version-ZzL5R6cS.d.mts → version-DL8U5RuA.d.mts} +6 -0
- package/package.json +8 -1
- package/dist/adapter-DqlAnZ_w.d.mts +0 -172
- package/dist/renderer-Ul9taFYp.d.mts +0 -169
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { w as SchemaMeta } from "../types-
|
|
2
|
-
import { t as Diagnostic } from "../diagnostics-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { w as SchemaMeta } from "../types-BrYbjC7_.mjs";
|
|
2
|
+
import { t as Diagnostic } from "../diagnostics-BTrm3O6J.mjs";
|
|
3
|
+
import { SchemaIoSide } from "../core/adapter.mjs";
|
|
4
|
+
import { ComponentResolver, WidgetMap } from "../core/renderer.mjs";
|
|
5
5
|
import { RejectUnrepresentableZod } from "../core/typeInference.mjs";
|
|
6
|
-
import { InferredValue,
|
|
6
|
+
import { a as InferredValue, t as InferFields } from "../inferValue-PPXWJpbN.mjs";
|
|
7
7
|
import { ReactNode } from "react";
|
|
8
8
|
|
|
9
9
|
//#region src/react/SchemaView.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* Props accepted by {@link SchemaView}.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors `SchemaComponentProps` for the read-only / RSC path — no
|
|
14
|
+
* `onChange`, no `validate`, and the theme is supplied via the
|
|
15
|
+
* `resolver` prop because Server Components cannot read React context.
|
|
16
|
+
*
|
|
17
|
+
* @group Components
|
|
18
|
+
*/
|
|
10
19
|
interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
|
|
11
20
|
/**
|
|
12
21
|
* Zod schema, JSON Schema object, or OpenAPI document.
|
|
@@ -35,8 +44,12 @@ interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefine
|
|
|
35
44
|
* schemas where the value type cannot be statically inferred.
|
|
36
45
|
*/
|
|
37
46
|
value?: InferredValue<T, Ref, undefined, Mode>;
|
|
38
|
-
/**
|
|
39
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Per-field meta overrides — nested object mirroring schema shape.
|
|
49
|
+
* Typed against {@link InferFields} so a typed `schema` prop drives
|
|
50
|
+
* autocomplete on the override map, matching `<SchemaComponent>`.
|
|
51
|
+
*/
|
|
52
|
+
fields?: InferFields<T, Ref>;
|
|
40
53
|
/** Meta overrides applied to the root schema. */
|
|
41
54
|
meta?: SchemaMeta;
|
|
42
55
|
/** Convenience: sets description on the root. */
|
|
@@ -61,11 +74,26 @@ interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefine
|
|
|
61
74
|
idPrefix?: string;
|
|
62
75
|
}
|
|
63
76
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
77
|
+
* Read-only schema renderer that is safe to use inside a React Server
|
|
78
|
+
* Component.
|
|
79
|
+
*
|
|
80
|
+
* Uses no context, state, or effects — the only hook called is
|
|
81
|
+
* `useId()`, which is RSC-safe. Always renders read-only; pair with
|
|
82
|
+
* `SchemaComponent` (which requires `"use client"`) when an editable
|
|
83
|
+
* form is required. Because Server Components cannot read React
|
|
84
|
+
* context, the theme adapter is passed via the `resolver` prop rather
|
|
85
|
+
* than `SchemaProvider`.
|
|
86
|
+
*
|
|
87
|
+
* @group Components
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* import { SchemaView } from "schema-components/react/SchemaView";
|
|
66
91
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
92
|
+
* export default async function Page() {
|
|
93
|
+
* const user = await getUser();
|
|
94
|
+
* return <SchemaView schema={userSchema} value={user} />;
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
69
97
|
*/
|
|
70
98
|
declare function SchemaView<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output">({
|
|
71
99
|
schema: schemaInput,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toRecordOrUndefined } from "../core/guards.mjs";
|
|
1
2
|
import "../core/limits.mjs";
|
|
2
3
|
import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
3
4
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
@@ -5,8 +6,8 @@ import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/ren
|
|
|
5
6
|
import { walk } from "../core/walker.mjs";
|
|
6
7
|
import { headlessResolver } from "./headless.mjs";
|
|
7
8
|
import { joinPath, sanitisePrefix } from "./SchemaComponent.mjs";
|
|
8
|
-
import { createElement, isValidElement, useId } from "react";
|
|
9
9
|
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
import { createElement, isValidElement, useId } from "react";
|
|
10
11
|
//#region src/react/SchemaView.tsx
|
|
11
12
|
/**
|
|
12
13
|
* React Server Component for read-only schema rendering.
|
|
@@ -36,11 +37,26 @@ import { jsx } from "react/jsx-runtime";
|
|
|
36
37
|
* is passed explicitly.
|
|
37
38
|
*/
|
|
38
39
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* Read-only schema renderer that is safe to use inside a React Server
|
|
41
|
+
* Component.
|
|
42
|
+
*
|
|
43
|
+
* Uses no context, state, or effects — the only hook called is
|
|
44
|
+
* `useId()`, which is RSC-safe. Always renders read-only; pair with
|
|
45
|
+
* `SchemaComponent` (which requires `"use client"`) when an editable
|
|
46
|
+
* form is required. Because Server Components cannot read React
|
|
47
|
+
* context, the theme adapter is passed via the `resolver` prop rather
|
|
48
|
+
* than `SchemaProvider`.
|
|
49
|
+
*
|
|
50
|
+
* @group Components
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* import { SchemaView } from "schema-components/react/SchemaView";
|
|
41
54
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
55
|
+
* export default async function Page() {
|
|
56
|
+
* const user = await getUser();
|
|
57
|
+
* return <SchemaView schema={userSchema} value={user} />;
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
44
60
|
*/
|
|
45
61
|
function SchemaView({ schema: schemaInput, ref: refInput, io, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict, idPrefix }) {
|
|
46
62
|
const generatedId = useId();
|
|
@@ -69,10 +85,11 @@ function SchemaView({ schema: schemaInput, ref: refInput, io, value, fields, met
|
|
|
69
85
|
if (err instanceof SchemaNormalisationError) throw err;
|
|
70
86
|
throw new SchemaNormalisationError(err instanceof Error ? err.message : "Failed to normalise schema", schemaInput, "unknown");
|
|
71
87
|
}
|
|
88
|
+
const fieldsRecord = toRecordOrUndefined(fields);
|
|
72
89
|
const walkOptions = {
|
|
73
90
|
componentMeta: mergedMeta,
|
|
74
91
|
rootMeta,
|
|
75
|
-
fieldOverrides:
|
|
92
|
+
fieldOverrides: fieldsRecord,
|
|
76
93
|
rootDocument,
|
|
77
94
|
...diagnostics !== void 0 ? { diagnostics } : {}
|
|
78
95
|
};
|
package/dist/react/a11y.d.mts
CHANGED
|
@@ -1,21 +1,88 @@
|
|
|
1
|
-
import { j as WalkedField } from "../types-
|
|
1
|
+
import { j as WalkedField } from "../types-BrYbjC7_.mjs";
|
|
2
|
+
import { AllConstraints } from "../core/renderer.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/react/a11y.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Build the ARIA attribute bundle for a renderer.
|
|
6
7
|
*
|
|
7
8
|
* - `aria-required="true"` whenever the field is non-optional.
|
|
9
|
+
* - `aria-describedby=<hint-id>` whenever a constraint hint applies.
|
|
8
10
|
* - `aria-label=<description>` when a non-empty description is supplied.
|
|
9
11
|
*
|
|
10
12
|
* Returns a plain `Record<string, string>` (rather than a typed
|
|
11
13
|
* attribute interface) so callers can spread the result into any JSX
|
|
12
14
|
* element type without per-element TypeScript widening.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* `inputId` and `constraints` together control whether
|
|
17
|
+
* `aria-describedby` is emitted. Pass `undefined` for `constraints`
|
|
18
|
+
* when the renderer never emits a constraint hint (e.g. boolean
|
|
19
|
+
* checkboxes); the helper then skips the attribute entirely. When the
|
|
20
|
+
* caller does emit a hint via `buildHint(...)`, the `aria-describedby`
|
|
21
|
+
* id matches `hintIdFor(inputId)` so the wire-up holds end-to-end.
|
|
18
22
|
*/
|
|
19
|
-
declare function buildAriaAttrs(tree: WalkedField, description?: unknown): Record<string, string>;
|
|
23
|
+
declare function buildAriaAttrs(tree: WalkedField, description?: unknown, inputId?: string, constraints?: AllConstraints): Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Description for a constraint hint emitted alongside an input.
|
|
26
|
+
*
|
|
27
|
+
* Returned by {@link constraintHint} when the field carries one or
|
|
28
|
+
* more constraint keywords the user should be told about (min/max,
|
|
29
|
+
* pattern, item count, …). Theme adapters render this as a `<small>`
|
|
30
|
+
* element wired to the input via `aria-describedby`.
|
|
31
|
+
*/
|
|
32
|
+
interface Hint {
|
|
33
|
+
/** DOM id matching {@link hintIdFor}(inputId) on the host input. */
|
|
34
|
+
readonly id: string;
|
|
35
|
+
/** Human-readable hint text. */
|
|
36
|
+
readonly text: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Derive the constraint-hint descriptor for a field at `inputId`. Returns
|
|
40
|
+
* `undefined` when the field has no constraint worth announcing — callers
|
|
41
|
+
* skip rendering the `<small>` element entirely rather than emitting an
|
|
42
|
+
* empty node.
|
|
43
|
+
*
|
|
44
|
+
* Shares its text builder with `html/a11y.ts` so the two renderers
|
|
45
|
+
* always announce the same constraint copy for the same schema field.
|
|
46
|
+
*/
|
|
47
|
+
declare function constraintHint(inputId: string, constraints: AllConstraints): Hint | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* True when the supplied field is non-optional and therefore deserves
|
|
50
|
+
* a visual required indicator alongside its label.
|
|
51
|
+
*
|
|
52
|
+
* Exposed as a predicate rather than a JSX element so theme adapters
|
|
53
|
+
* can render the indicator in whatever element type matches their
|
|
54
|
+
* design language (`<span>`, `<sup>`, an icon component, …).
|
|
55
|
+
*/
|
|
56
|
+
declare function isFieldRequired(tree: WalkedField): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Narrow `meta.description` (typed `unknown`) to a string value safe to
|
|
59
|
+
* pass into JSX `aria-label`. Returns `undefined` for non-string or
|
|
60
|
+
* empty-string descriptions so React drops the attribute rather than
|
|
61
|
+
* stringifying e.g. `{}` to `"[object Object]"`.
|
|
62
|
+
*/
|
|
63
|
+
declare function ariaLabel(description: unknown): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Structured constraint-hint data for the React renderers.
|
|
66
|
+
*
|
|
67
|
+
* The HTML pipeline emits an inline `<small class="sc-hint">` element
|
|
68
|
+
* next to each input and references it through `aria-describedby`. The
|
|
69
|
+
* React pipeline mirrors that contract: the input takes the
|
|
70
|
+
* `ariaDescribedBy` id, the renderer emits a sibling `<small id={...}>`
|
|
71
|
+
* whose text is `hint`. Returns `undefined` when the field has no
|
|
72
|
+
* advertise-able constraints (`constraintHint` returns `undefined`) so
|
|
73
|
+
* callers can skip both the attribute and the element cleanly.
|
|
74
|
+
*/
|
|
75
|
+
interface HintInfo {
|
|
76
|
+
readonly id: string;
|
|
77
|
+
readonly hint: string;
|
|
78
|
+
readonly ariaDescribedBy: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build {@link HintInfo} for a field at `inputId` given its declared
|
|
82
|
+
* constraints. Returns `undefined` when no constraint message would be
|
|
83
|
+
* produced — the React renderers then skip emitting the hint element
|
|
84
|
+
* entirely so consumers don't see an empty `<small>`.
|
|
85
|
+
*/
|
|
86
|
+
declare function buildHintInfo(inputId: string, constraints: AllConstraints): HintInfo | undefined;
|
|
20
87
|
//#endregion
|
|
21
|
-
export { buildAriaAttrs };
|
|
88
|
+
export { Hint, HintInfo, ariaLabel, buildAriaAttrs, buildHintInfo, constraintHint, isFieldRequired };
|
package/dist/react/a11y.mjs
CHANGED
|
@@ -1,24 +1,85 @@
|
|
|
1
|
+
import { constraintHint as constraintHint$1 } from "../core/constraintHint.mjs";
|
|
2
|
+
import { hintIdFor } from "../core/idPath.mjs";
|
|
1
3
|
//#region src/react/a11y.ts
|
|
2
4
|
/**
|
|
3
5
|
* Build the ARIA attribute bundle for a renderer.
|
|
4
6
|
*
|
|
5
7
|
* - `aria-required="true"` whenever the field is non-optional.
|
|
8
|
+
* - `aria-describedby=<hint-id>` whenever a constraint hint applies.
|
|
6
9
|
* - `aria-label=<description>` when a non-empty description is supplied.
|
|
7
10
|
*
|
|
8
11
|
* Returns a plain `Record<string, string>` (rather than a typed
|
|
9
12
|
* attribute interface) so callers can spread the result into any JSX
|
|
10
13
|
* element type without per-element TypeScript widening.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* `inputId` and `constraints` together control whether
|
|
16
|
+
* `aria-describedby` is emitted. Pass `undefined` for `constraints`
|
|
17
|
+
* when the renderer never emits a constraint hint (e.g. boolean
|
|
18
|
+
* checkboxes); the helper then skips the attribute entirely. When the
|
|
19
|
+
* caller does emit a hint via `buildHint(...)`, the `aria-describedby`
|
|
20
|
+
* id matches `hintIdFor(inputId)` so the wire-up holds end-to-end.
|
|
16
21
|
*/
|
|
17
|
-
function buildAriaAttrs(tree, description) {
|
|
22
|
+
function buildAriaAttrs(tree, description, inputId, constraints) {
|
|
18
23
|
const attrs = {};
|
|
19
24
|
if (tree.isOptional === false) attrs["aria-required"] = "true";
|
|
25
|
+
if (inputId !== void 0 && constraints !== void 0 && constraintHint$1(constraints) !== void 0) attrs["aria-describedby"] = hintIdFor(inputId);
|
|
20
26
|
if (typeof description === "string" && description.length > 0) attrs["aria-label"] = description;
|
|
21
27
|
return attrs;
|
|
22
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive the constraint-hint descriptor for a field at `inputId`. Returns
|
|
31
|
+
* `undefined` when the field has no constraint worth announcing — callers
|
|
32
|
+
* skip rendering the `<small>` element entirely rather than emitting an
|
|
33
|
+
* empty node.
|
|
34
|
+
*
|
|
35
|
+
* Shares its text builder with `html/a11y.ts` so the two renderers
|
|
36
|
+
* always announce the same constraint copy for the same schema field.
|
|
37
|
+
*/
|
|
38
|
+
function constraintHint(inputId, constraints) {
|
|
39
|
+
const text = constraintHint$1(constraints);
|
|
40
|
+
if (text === void 0) return void 0;
|
|
41
|
+
return {
|
|
42
|
+
id: hintIdFor(inputId),
|
|
43
|
+
text
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when the supplied field is non-optional and therefore deserves
|
|
48
|
+
* a visual required indicator alongside its label.
|
|
49
|
+
*
|
|
50
|
+
* Exposed as a predicate rather than a JSX element so theme adapters
|
|
51
|
+
* can render the indicator in whatever element type matches their
|
|
52
|
+
* design language (`<span>`, `<sup>`, an icon component, …).
|
|
53
|
+
*/
|
|
54
|
+
function isFieldRequired(tree) {
|
|
55
|
+
return tree.isOptional === false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Narrow `meta.description` (typed `unknown`) to a string value safe to
|
|
59
|
+
* pass into JSX `aria-label`. Returns `undefined` for non-string or
|
|
60
|
+
* empty-string descriptions so React drops the attribute rather than
|
|
61
|
+
* stringifying e.g. `{}` to `"[object Object]"`.
|
|
62
|
+
*/
|
|
63
|
+
function ariaLabel(description) {
|
|
64
|
+
if (typeof description !== "string") return void 0;
|
|
65
|
+
if (description.length === 0) return void 0;
|
|
66
|
+
return description;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build {@link HintInfo} for a field at `inputId` given its declared
|
|
70
|
+
* constraints. Returns `undefined` when no constraint message would be
|
|
71
|
+
* produced — the React renderers then skip emitting the hint element
|
|
72
|
+
* entirely so consumers don't see an empty `<small>`.
|
|
73
|
+
*/
|
|
74
|
+
function buildHintInfo(inputId, constraints) {
|
|
75
|
+
const hint = constraintHint$1(constraints);
|
|
76
|
+
if (hint === void 0) return void 0;
|
|
77
|
+
const id = hintIdFor(inputId);
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
hint,
|
|
81
|
+
ariaDescribedBy: id
|
|
82
|
+
};
|
|
83
|
+
}
|
|
23
84
|
//#endregion
|
|
24
|
-
export { buildAriaAttrs };
|
|
85
|
+
export { ariaLabel, buildAriaAttrs, buildHintInfo, constraintHint, isFieldRequired };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { j as WalkedField } from "../types-
|
|
1
|
+
import { j as WalkedField } from "../types-BrYbjC7_.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/react/fieldPath.d.ts
|
|
4
4
|
/**
|
|
@@ -9,11 +9,26 @@ declare function resolvePath(tree: WalkedField, path: string): WalkedField | und
|
|
|
9
9
|
/**
|
|
10
10
|
* Resolve a dot-separated path through a data value.
|
|
11
11
|
* Supports array index notation: `field[0]`.
|
|
12
|
+
*
|
|
13
|
+
* Path segments naming a prototype-polluting property (`__proto__`,
|
|
14
|
+
* `constructor`, `prototype`) refuse to resolve and return `undefined`.
|
|
15
|
+
* Without the refusal, an attacker-supplied path would read
|
|
16
|
+
* `Object.prototype` (or similar) and surface fields injected into the
|
|
17
|
+
* runtime prototype chain as if they belonged to the user's data.
|
|
12
18
|
*/
|
|
13
19
|
declare function resolveValue(root: unknown, path: string): unknown;
|
|
14
20
|
/**
|
|
15
21
|
* Set a value at a dot-separated path, producing a new root object.
|
|
16
22
|
* Does not mutate the input — returns a shallow-updated copy at each level.
|
|
23
|
+
*
|
|
24
|
+
* Refuses paths whose segments name a prototype-polluting property
|
|
25
|
+
* (`__proto__`, `constructor`, `prototype`). Such a path could otherwise
|
|
26
|
+
* mutate `Object.prototype` (or similar) through the assignment, planting
|
|
27
|
+
* fields visible to every plain object in the runtime. The input `root`
|
|
28
|
+
* is returned unchanged so the caller's onChange handler treats the
|
|
29
|
+
* write as a no-op rather than propagating a poisoned state. This
|
|
30
|
+
* matches the silent-refusal semantics of `dereference` in `core/ref.ts`
|
|
31
|
+
* when a JSON Pointer segment names a prototype-polluting key.
|
|
17
32
|
*/
|
|
18
33
|
declare function setNestedValue(root: unknown, path: string, leafValue: unknown): unknown;
|
|
19
34
|
//#endregion
|
package/dist/react/fieldPath.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { isPrototypePollutingKey } from "../core/uri.mjs";
|
|
2
3
|
//#region src/react/fieldPath.ts
|
|
3
4
|
/**
|
|
4
5
|
* Resolve a dot-separated path through a WalkedField tree.
|
|
@@ -26,6 +27,12 @@ function resolvePath(tree, path) {
|
|
|
26
27
|
/**
|
|
27
28
|
* Resolve a dot-separated path through a data value.
|
|
28
29
|
* Supports array index notation: `field[0]`.
|
|
30
|
+
*
|
|
31
|
+
* Path segments naming a prototype-polluting property (`__proto__`,
|
|
32
|
+
* `constructor`, `prototype`) refuse to resolve and return `undefined`.
|
|
33
|
+
* Without the refusal, an attacker-supplied path would read
|
|
34
|
+
* `Object.prototype` (or similar) and surface fields injected into the
|
|
35
|
+
* runtime prototype chain as if they belonged to the user's data.
|
|
29
36
|
*/
|
|
30
37
|
function resolveValue(root, path) {
|
|
31
38
|
if (path.length === 0) return root;
|
|
@@ -36,21 +43,38 @@ function resolveValue(root, path) {
|
|
|
36
43
|
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
37
44
|
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
38
45
|
const key = bracketMatch[1];
|
|
46
|
+
if (isPrototypePollutingKey(key)) return void 0;
|
|
39
47
|
const index = Number(bracketMatch[2]);
|
|
40
48
|
const arr = current[key];
|
|
41
49
|
if (Array.isArray(arr)) current = arr[index];
|
|
42
50
|
else return;
|
|
43
|
-
} else
|
|
51
|
+
} else {
|
|
52
|
+
if (isPrototypePollutingKey(part)) return void 0;
|
|
53
|
+
current = current[part];
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
return current;
|
|
46
57
|
}
|
|
47
58
|
/**
|
|
48
59
|
* Set a value at a dot-separated path, producing a new root object.
|
|
49
60
|
* Does not mutate the input — returns a shallow-updated copy at each level.
|
|
61
|
+
*
|
|
62
|
+
* Refuses paths whose segments name a prototype-polluting property
|
|
63
|
+
* (`__proto__`, `constructor`, `prototype`). Such a path could otherwise
|
|
64
|
+
* mutate `Object.prototype` (or similar) through the assignment, planting
|
|
65
|
+
* fields visible to every plain object in the runtime. The input `root`
|
|
66
|
+
* is returned unchanged so the caller's onChange handler treats the
|
|
67
|
+
* write as a no-op rather than propagating a poisoned state. This
|
|
68
|
+
* matches the silent-refusal semantics of `dereference` in `core/ref.ts`
|
|
69
|
+
* when a JSON Pointer segment names a prototype-polluting key.
|
|
50
70
|
*/
|
|
51
71
|
function setNestedValue(root, path, leafValue) {
|
|
52
72
|
if (path.length === 0) return leafValue;
|
|
53
73
|
const parts = path.split(".");
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
76
|
+
if (isPrototypePollutingKey(bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0 ? bracketMatch[1] : part)) return root;
|
|
77
|
+
}
|
|
54
78
|
const result = isObject(root) ? { ...root } : {};
|
|
55
79
|
let current = result;
|
|
56
80
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RenderProps } from "../core/renderer.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/fieldShell.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Render-time inputs to {@link FieldShell}. The shell does not depend
|
|
7
|
+
* on a theme; it consumes only the field metadata the walker has
|
|
8
|
+
* already produced.
|
|
9
|
+
*/
|
|
10
|
+
interface FieldShellProps {
|
|
11
|
+
/** The walked field props passed to the theme's render function. */
|
|
12
|
+
readonly props: RenderProps;
|
|
13
|
+
/** Stable DOM id for the host input. */
|
|
14
|
+
readonly inputId: string;
|
|
15
|
+
/**
|
|
16
|
+
* Children-as-function. Receives the ARIA attribute bundle so the
|
|
17
|
+
* caller can spread the attributes onto whatever element it
|
|
18
|
+
* produces; the shell does not inject them because the spread
|
|
19
|
+
* point varies between libraries (`inputProps`, top-level, slot
|
|
20
|
+
* props, …).
|
|
21
|
+
*/
|
|
22
|
+
readonly children: (ariaAttrs: Record<string, string>) => ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Optional override for the label text. Defaults to
|
|
25
|
+
* `props.meta.description` when undefined.
|
|
26
|
+
*/
|
|
27
|
+
readonly label?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When true, suppress the wrapping `<label>` element. Useful for
|
|
30
|
+
* adapters whose host primitive (e.g. MUI's `TextField`) already
|
|
31
|
+
* renders its own label.
|
|
32
|
+
*/
|
|
33
|
+
readonly hideLabel?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compose label, host primitive, and constraint hint around a render
|
|
37
|
+
* function supplied by the theme adapter. Returns plain JSX — no
|
|
38
|
+
* theme-specific element types — so the same shell works under
|
|
39
|
+
* shadcn, MUI, Mantine, Radix, or any custom theme.
|
|
40
|
+
*/
|
|
41
|
+
declare function FieldShell({
|
|
42
|
+
props,
|
|
43
|
+
inputId,
|
|
44
|
+
children,
|
|
45
|
+
label,
|
|
46
|
+
hideLabel
|
|
47
|
+
}: FieldShellProps): ReactNode;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { FieldShell, FieldShellProps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildAriaAttrs, constraintHint, isFieldRequired } from "./a11y.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react/fieldShell.tsx
|
|
4
|
+
/**
|
|
5
|
+
* Compose label, host primitive, and constraint hint around a render
|
|
6
|
+
* function supplied by the theme adapter. Returns plain JSX — no
|
|
7
|
+
* theme-specific element types — so the same shell works under
|
|
8
|
+
* shadcn, MUI, Mantine, Radix, or any custom theme.
|
|
9
|
+
*/
|
|
10
|
+
function FieldShell({ props, inputId, children, label, hideLabel }) {
|
|
11
|
+
const description = typeof label === "string" ? label : typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
12
|
+
const required = isFieldRequired(props.tree);
|
|
13
|
+
const hint = constraintHint(inputId, props.constraints);
|
|
14
|
+
const ariaAttrs = buildAriaAttrs(props.tree, description, inputId, props.constraints);
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "sc-field",
|
|
17
|
+
children: [
|
|
18
|
+
hideLabel !== true && description !== void 0 && /* @__PURE__ */ jsxs("label", {
|
|
19
|
+
htmlFor: inputId,
|
|
20
|
+
children: [description, required && /* @__PURE__ */ jsxs("span", {
|
|
21
|
+
"aria-hidden": "true",
|
|
22
|
+
className: "sc-required",
|
|
23
|
+
style: { color: "#dc2626" },
|
|
24
|
+
children: [" ", "*"]
|
|
25
|
+
})]
|
|
26
|
+
}),
|
|
27
|
+
children(ariaAttrs),
|
|
28
|
+
hint !== void 0 && /* @__PURE__ */ jsx("small", {
|
|
29
|
+
className: "sc-hint",
|
|
30
|
+
id: hint.id,
|
|
31
|
+
children: hint.text
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { FieldShell };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { j as WalkedField } from "../types-
|
|
2
|
-
import {
|
|
1
|
+
import { j as WalkedField } from "../types-BrYbjC7_.mjs";
|
|
2
|
+
import { RenderProps } from "../core/renderer.mjs";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react/headlessRenderers.d.ts
|
|
@@ -20,10 +20,15 @@ declare function toReactNode(value: unknown): ReactNode;
|
|
|
20
20
|
* Throws on an empty path; see `fieldDomId` for the rationale.
|
|
21
21
|
*/
|
|
22
22
|
declare function inputId(path: string): string;
|
|
23
|
+
/** Headless renderer for `StringField` — plain `<input>` / `<span>`. */
|
|
23
24
|
declare function renderString(props: RenderProps): ReactNode;
|
|
25
|
+
/** Headless renderer for `NumberField` — plain `<input type="number">`. */
|
|
24
26
|
declare function renderNumber(props: RenderProps): ReactNode;
|
|
27
|
+
/** Headless renderer for `BooleanField` — plain `<input type="checkbox">`. */
|
|
25
28
|
declare function renderBoolean(props: RenderProps): ReactNode;
|
|
29
|
+
/** Headless renderer for `EnumField` — plain `<select>` listing each option. */
|
|
26
30
|
declare function renderEnum(props: RenderProps): ReactNode;
|
|
31
|
+
/** Headless renderer for `ObjectField` — `<fieldset>` per object with one child per property. */
|
|
27
32
|
declare function renderObject(props: RenderProps): ReactNode;
|
|
28
33
|
/**
|
|
29
34
|
* Compute the default value for a freshly added record entry based on the
|
|
@@ -42,9 +47,13 @@ declare function nextRecordKey(existing: readonly string[], base?: string): stri
|
|
|
42
47
|
* or when newKey collides with an existing key.
|
|
43
48
|
*/
|
|
44
49
|
declare function renameRecordKey(obj: Record<string, unknown>, oldKey: string, newKey: string): Record<string, unknown>;
|
|
50
|
+
/** Headless renderer for `RecordField` — editable key/value rows with add/remove controls. */
|
|
45
51
|
declare function renderRecord(props: RenderProps): ReactNode;
|
|
52
|
+
/** Headless renderer for `ArrayField` — ordered list with add/remove controls. */
|
|
46
53
|
declare function renderArray(props: RenderProps): ReactNode;
|
|
54
|
+
/** Headless renderer for plain `UnionField` — picks the matching option and renders it. */
|
|
47
55
|
declare function renderUnion(props: RenderProps): ReactNode;
|
|
56
|
+
/** Headless renderer for `DiscriminatedUnionField` — tabbed UI driven by the discriminator. */
|
|
48
57
|
declare function renderDiscriminatedUnion(props: RenderProps): ReactNode;
|
|
49
58
|
/**
|
|
50
59
|
* Pure helper: convert a tab index into the new value the discriminated
|
|
@@ -54,6 +63,7 @@ declare function renderDiscriminatedUnion(props: RenderProps): ReactNode;
|
|
|
54
63
|
* without rendering the tabs component (which relies on React hooks).
|
|
55
64
|
*/
|
|
56
65
|
declare function discriminatedUnionValueForTab(optionLabels: readonly string[], discKey: string, newIndex: number): Record<string, string> | undefined;
|
|
66
|
+
/** Headless renderer for `FileField` — plain `<input type="file">`. */
|
|
57
67
|
declare function renderFile(props: RenderProps): ReactNode;
|
|
58
68
|
/**
|
|
59
69
|
* Render a literal field — `z.literal("a")` or `{ const: 5 }`.
|
|
@@ -106,6 +116,7 @@ declare function renderConditional(props: RenderProps): ReactNode;
|
|
|
106
116
|
* beneath an explanatory preamble.
|
|
107
117
|
*/
|
|
108
118
|
declare function renderNegation(props: RenderProps): ReactNode;
|
|
119
|
+
/** Headless renderer for `UnknownField` — JSON-encoded fallback for unconstrained values. */
|
|
109
120
|
declare function renderUnknown(props: RenderProps): ReactNode;
|
|
110
121
|
//#endregion
|
|
111
122
|
export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderString, renderTuple, renderUnion, renderUnknown, toReactNode };
|