@petrarca/sonnet-schema-designer 0.5.0 → 0.7.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 ADDED
@@ -0,0 +1,97 @@
1
+ # @petrarca/sonnet-schema-designer
2
+
3
+ A visual editor for JSON Schema, part of the Petrarca Sonnet component library.
4
+ One component, `SchemaDesigner`, edits a schema in place. Presentation hints
5
+ travel beside it in a UI Schema map, or embedded on the schema when you ask for
6
+ that shape.
7
+
8
+ ## What it provides
9
+
10
+ **Three panes.** A property tree on the left for structure -- select, add,
11
+ delete, reorder, rename, with a collapsible region for `$defs`. A preview in
12
+ the centre with tabs over the live form, the form layout designer, a table and
13
+ the JSON source. An inspector on the right with Data, Enum, UI, Rule and Table
14
+ editors for the selected node. Both side panes resize and collapse.
15
+
16
+ **Schema authoring.** Types and validation, nesting, `oneOf` variants,
17
+ conditionals, definitions and references. Local `#/$defs/...` and
18
+ `#/definitions/...` references resolve on their own; anything else goes through
19
+ a synchronous `refResolver` and expands read-only in the tree.
20
+
21
+ **Presentation authoring.** Widgets and their options, table hints, visibility
22
+ rules and form layout, in the same `x-ui-*` vocabulary
23
+ `JsonSchemaFormRenderer` reads. `embedUiSchema` merges the top-level form hints
24
+ onto the schema when a single self-contained artifact is wanted.
25
+
26
+ **Capability gating.** A profile restricts what a user may author -- a
27
+ data-only profile, a fixed schema with editable areas, or a locked view --
28
+ validated by `resolveCapabilities`, `validateCapabilities` and
29
+ `capabilitiesSchema`.
30
+
31
+ **The pieces behind the shell.** `useDesignerState` holds the whole model and
32
+ every mutation; `PropertyTree`, `Inspector`, `PreviewPane`,
33
+ `FormLayoutDesigner` and `FormSettingsSheet` each take that state. A real seam,
34
+ though the shell is the supported way to use them.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pnpm add @petrarca/sonnet-schema-designer
40
+ ```
41
+
42
+ `@petrarca/sonnet-core`, `@petrarca/sonnet-ui` and `@petrarca/sonnet-forms` are
43
+ regular dependencies and arrive with it, as do `lucide-react` and Zod.
44
+
45
+ | Peer | Range | Needed for |
46
+ |---|---|---|
47
+ | `react`, `react-dom` | `>=19` | Everything |
48
+ | `tailwindcss` | `>=3` | The styles the components emit |
49
+
50
+ It renders through `sonnet-ui`'s stylesheet and Tailwind preset, so set those
51
+ up first -- see
52
+ [sonnet-ui: getting started](../sonnet-ui/docs/usage/getting-started.md).
53
+
54
+ ## Usage
55
+
56
+ ```tsx
57
+ import { SchemaDesigner } from "@petrarca/sonnet-schema-designer";
58
+ import type { JsonSchema, UISchema } from "@petrarca/sonnet-core/schema";
59
+
60
+ const [schema, setSchema] = useState<JsonSchema>(initial);
61
+ const [uiSchema, setUiSchema] = useState<Record<string, UISchema>>({});
62
+
63
+ <SchemaDesigner
64
+ schema={schema}
65
+ uiSchema={uiSchema}
66
+ onChange={(nextSchema, nextUiSchema) => {
67
+ setSchema(nextSchema);
68
+ setUiSchema(nextUiSchema);
69
+ }}
70
+ />;
71
+ ```
72
+
73
+ All nine props are optional -- `schema`, `uiSchema`, `initialSchema`,
74
+ `initialUiSchema`, `onChange`, `readOnly`, `capabilities`, `widgetOptions` and
75
+ `refResolver`. The mode is decided by whether `schema` is `undefined`: omit it
76
+ and seed with `initialSchema` for uncontrolled use, where the designer keeps
77
+ the model internally and still reports every edit through `onChange`. With
78
+ neither, it starts from an empty object schema.
79
+
80
+ The root element is `w-full h-full`, so the parent must have a resolved height.
81
+
82
+ ## Documentation
83
+
84
+ - [Usage guides](docs/usage/index.md) -- getting started,
85
+ [authoring schemas](docs/usage/authoring-schemas.md),
86
+ [UI hints](docs/usage/ui-hints.md) and
87
+ [capabilities](docs/usage/capabilities.md).
88
+ - [Design docs](docs/design/index.md) -- the designer's structure, logic
89
+ authoring, and references and definitions.
90
+ - [The UISchema vocabulary](../sonnet-forms/docs/usage/ui-schema.md) -- what the
91
+ `x-ui-*` keys mean to the form renderer.
92
+ - [Workspace documentation](../../docs/README.md) -- how the packages layer and
93
+ the architecture decision records.
94
+
95
+ ## License
96
+
97
+ Apache 2.0. See [LICENSE.md](../../LICENSE.md).
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JsonSchemaProperty, JsonSchema, UISchema, UILayoutConfig, UISchemaOptions } from '@petrarca/sonnet-core/schema';
1
+ import { JsonSchemaProperty, JsonSchema, RefResolver, UISchema, UILayoutConfig, UISchemaOptions, UILayoutGroupConfig, UILayoutRowConfig } from '@petrarca/sonnet-core/schema';
2
2
  import { z } from 'zod';
3
3
  import React from 'react';
4
4
  import { WidgetOptionDef } from '@petrarca/sonnet-forms/widget-options';
@@ -7,26 +7,115 @@ import { TableUiSchema } from '@petrarca/sonnet-ui';
7
7
  /**
8
8
  * Path navigation over a JsonSchema tree.
9
9
  *
10
- * A property path is an array of property keys from the root down to a
11
- * property, e.g. ["address", "city"]. Object children nest through
12
- * `properties`; array children nest through `items.properties`. The path names
13
- * the property and the navigator descends through `items` transparently, so an
14
- * array-of-objects and its item object share one editing surface.
10
+ * A path is a list of typed segments from the root down to a property. Most
11
+ * segments are property keys (`{kind:"prop"}`) -- object children nest through
12
+ * `properties`, array-of-object children through `items.properties`, and the
13
+ * navigator descends through `items` transparently so an array and its item
14
+ * object share one editing surface.
15
+ *
16
+ * Logic constructs introduce non-property segments: `{kind:"branch"}` for a
17
+ * `oneOf`/`allOf` element, and `{kind:"slot"}` for a fixed schema slot
18
+ * (`then`/`else`/`items`). The discriminated union keeps the distinction
19
+ * explicit and type-checked; a single codec (pathKey/parsePathKey) owns all
20
+ * conversion to/from strings.
15
21
  */
16
22
 
17
- type PropertyPath = string[];
18
- /** Serialize a path for use as a stable React key or selection id. */
23
+ /** A fixed schema slot that holds a single sub-schema. */
24
+ type PathSlot = "then" | "else" | "items";
25
+ /**
26
+ * The array a conditional (if/then/else) member lives in. `allOf` = every
27
+ * condition must hold (AND); `anyOf` = any condition suffices (OR). Maps to
28
+ * FHIR `enableBehavior` "all" / "any".
29
+ */
30
+ type ConditionCombinator = "allOf" | "anyOf";
31
+ /** One step in a property path. */
32
+ type PathSegment = {
33
+ kind: "prop";
34
+ key: string;
35
+ } | {
36
+ kind: "branch";
37
+ index: number;
38
+ } | {
39
+ kind: "condition";
40
+ combinator: ConditionCombinator;
41
+ index: number;
42
+ } | {
43
+ kind: "slot";
44
+ slot: PathSlot;
45
+ } | {
46
+ kind: "def";
47
+ name: string;
48
+ };
49
+ type PropertyPath = PathSegment[];
50
+ /**
51
+ * Whether a property key is reserved by the path codec. Keys matching
52
+ * `/^#(branch|condition|def|slot):/` would corrupt the pathKey/parsePathKey
53
+ * round-trip. The designer guards against these at rename/add time.
54
+ */
55
+ declare function isReservedKey(key: string): boolean;
56
+ /** Construct a property-key segment. */
57
+ declare function prop(key: string): PathSegment;
58
+ /** Construct a branch (oneOf element) segment. */
59
+ declare function branch(index: number): PathSegment;
60
+ /** Construct a condition (if/then/else member) segment for a combinator array. */
61
+ declare function condition(index: number, combinator?: ConditionCombinator): PathSegment;
62
+ /** Construct a fixed-slot segment. */
63
+ declare function slot(name: PathSlot): PathSegment;
64
+ /** Construct a definition ($defs entry) segment. Root-only in practice. */
65
+ declare function def(name: string): PathSegment;
66
+ /** Serialize a path to a stable string (React key, selection id, scope value). */
19
67
  declare function pathKey(path: PropertyPath): string;
20
- /** Whether two paths refer to the same property. */
68
+ /** Parse a string produced by {@link pathKey} back into a path. */
69
+ declare function parsePathKey(key: string): PropertyPath;
70
+ /** Whether two path segments refer to the same position. */
71
+ declare function sameSegment(a: PathSegment, b: PathSegment): boolean;
72
+ /** Whether two paths refer to the same node (structural comparison). */
21
73
  declare function samePath(a: PropertyPath | null, b: PropertyPath | null): boolean;
22
- /** The container that holds a property's children, or undefined for scalars. */
74
+ /** How a child slot is presented and mutated. */
75
+ type SlotKind = "property" | "branch" | "conditional" | "definition";
76
+ /** A single editable child of a property (or the root). */
77
+ interface ChildSlot {
78
+ /** Segment appended to the parent path to reach this child. */
79
+ segment: PathSegment;
80
+ /** Display label for the tree row. */
81
+ label: string;
82
+ /** The child schema. */
83
+ property: JsonSchemaProperty;
84
+ kind: SlotKind;
85
+ removable: boolean;
86
+ reorderable: boolean;
87
+ }
88
+ /**
89
+ * The properties map that holds a property's named children, or undefined when
90
+ * it has none. Object children live in `properties`; array-of-object children
91
+ * live in `items.properties`. (Logic constructs use branch/slot children and
92
+ * are surfaced via {@link childSlots}, not here.)
93
+ */
23
94
  declare function childContainer(prop: JsonSchemaProperty): Record<string, JsonSchemaProperty> | undefined;
95
+ /**
96
+ * The reusable definitions map of a schema. Canonically 2020-12 `$defs`; also
97
+ * reads Draft-7 `definitions` so both dialects surface as editable nodes. New
98
+ * definitions are always authored into `$defs` (see mutate.addDefinition).
99
+ */
100
+ declare function definitionsOf(property: JsonSchemaProperty): Record<string, JsonSchemaProperty> | undefined;
101
+ /**
102
+ * The ordered child slots of a property (the single extension point for the
103
+ * tree/inspector/mutations). Surfaces object/array property children, oneOf
104
+ * branches, if/then/else conditionals (allOf/anyOf members), and $defs
105
+ * definitions (root-only in practice).
106
+ */
107
+ declare function childSlots(property: JsonSchemaProperty): ChildSlot[];
108
+ /**
109
+ * Wrap a JsonSchema root as an object-shaped JsonSchemaProperty so the path
110
+ * navigation machinery can traverse it with the same stepInto logic as any
111
+ * nested property. Pass the keys needed by the caller; unused fields default
112
+ * to undefined.
113
+ */
114
+ declare function schemaAsRootProp(schema: JsonSchema): JsonSchemaProperty;
24
115
  /** Resolve a path to its property definition, or undefined if not found. */
25
116
  declare function getProperty(schema: JsonSchema, path: PropertyPath): JsonSchemaProperty | undefined;
26
- /** Whether a property has (or can have) child properties. */
117
+ /** Whether a property has (or can have) child slots (properties, branches, conditionals). */
27
118
  declare function isContainer(prop: JsonSchemaProperty): boolean;
28
- /** The child properties of a property, or an empty map for scalars. */
29
- declare function childrenOf(prop: JsonSchemaProperty): Record<string, JsonSchemaProperty>;
30
119
 
31
120
  /**
32
121
  * Read-shaping: turn a JsonSchema into a property tree for the designer UI.
@@ -35,32 +124,51 @@ declare function childrenOf(prop: JsonSchemaProperty): Record<string, JsonSchema
35
124
  * first, then remaining keys in declaration order.
36
125
  */
37
126
 
38
- type PropertyKind = "scalar" | "enum" | "object" | "array";
127
+ type PropertyKind = "scalar" | "enum" | "object" | "array" | "oneOf" | "conditional" | "reference" | "definition";
39
128
  interface PropertyNode {
40
129
  path: PropertyPath;
41
130
  key: string;
42
131
  property: JsonSchemaProperty;
43
132
  kind: PropertyKind;
133
+ /** How this node sits in its parent: a named property or a oneOf branch. */
134
+ slotKind: SlotKind;
44
135
  required: boolean;
45
136
  /** Short type descriptor for the tree row, e.g. "string", "object[]". */
46
137
  typeLabel: string;
47
138
  /** Resolved widget override (x-ui-widget) if any. */
48
139
  widget?: string;
140
+ /**
141
+ * When this node is a `$ref`, the ref string it points at. Undefined for
142
+ * non-ref nodes.
143
+ */
144
+ reference?: string;
145
+ /**
146
+ * True for nodes that live inside a resolved `$ref` (the ref's descendants).
147
+ * These come from a shared definition and are not editable here -- the tree
148
+ * and inspector use this to show read-only affordances. Computed once at
149
+ * build time (no per-render schema walks). The ref node itself is NOT marked
150
+ * (the usage can be renamed/deleted).
151
+ */
152
+ readOnly?: boolean;
49
153
  children: PropertyNode[];
50
154
  }
51
155
  /** Classify a property into a tree kind. */
52
156
  declare function propertyKind(prop: JsonSchemaProperty): PropertyKind;
53
157
  /** Order keys by explicit x-ui-order, then remaining declaration order. */
54
158
  declare function orderedKeys(properties: Record<string, JsonSchemaProperty>, order: string[] | undefined): string[];
55
- /** Build the full property tree for a schema. */
56
- declare function buildPropertyTree(schema: JsonSchema): PropertyNode[];
159
+ /**
160
+ * Build the full property tree for a schema. Pass `resolve` to expand `$ref`
161
+ * nodes (read-only) so referenced structure is legible; without it, ref nodes
162
+ * show as leaves. `resolve` mirrors the renderer's RefResolver.
163
+ */
164
+ declare function buildPropertyTree(schema: JsonSchema, resolve?: RefResolver): PropertyNode[];
57
165
  /**
58
166
  * A layout scope: a container whose fields can be arranged into rows. Root is
59
167
  * the form itself (path []); nested scopes are object/array containers.
60
168
  */
61
169
  interface LayoutScope {
62
170
  /** Empty for root, otherwise the property path to the container. */
63
- path: string[];
171
+ path: PropertyPath;
64
172
  /** Human label, e.g. "Form (root)" or "contact" or "addresses (items)". */
65
173
  label: string;
66
174
  }
@@ -81,13 +189,56 @@ interface DesignerModel {
81
189
  schema: JsonSchema;
82
190
  uiSchema: UiSchemaMap;
83
191
  }
192
+ /** Kinds that can be created as a new property. */
193
+ type NewPropertyKind = "scalar" | "object" | "array" | "oneOf";
84
194
  /** A default property for a newly created property of a given kind. */
85
- declare function newProperty(kind: "scalar" | "object" | "array"): JsonSchemaProperty;
195
+ declare function newProperty(kind: NewPropertyKind): JsonSchemaProperty;
196
+ /** A default oneOf branch: an object with a const-valued `kind` discriminator. */
197
+ declare function newBranch(value: string): JsonSchemaProperty;
86
198
  /** Add a property under a parent path (root when empty). Returns the new model. */
87
- declare function addProperty(model: DesignerModel, parentPath: PropertyPath, kind: "scalar" | "object" | "array"): {
199
+ declare function addProperty(model: DesignerModel, parentPath: PropertyPath, kind: NewPropertyKind): {
200
+ model: DesignerModel;
201
+ path: PropertyPath;
202
+ };
203
+ /** Add a branch to the oneOf property at `path`. Returns the new model + branch path. */
204
+ declare function addBranch(model: DesignerModel, path: PropertyPath): {
205
+ model: DesignerModel;
206
+ path: PropertyPath;
207
+ };
208
+ /** Remove the branch at `path` (a branch segment). Returns the new model. */
209
+ declare function removeBranch(model: DesignerModel, path: PropertyPath): DesignerModel;
210
+ /** Move the branch at `path` up (-1) or down (+1) among its siblings. */
211
+ declare function moveBranch(model: DesignerModel, path: PropertyPath, direction: -1 | 1): DesignerModel;
212
+ /**
213
+ * A seed conditional member: an empty `if` (no field yet) plus an empty `then`.
214
+ * The condition is filled in by setCondition; `then` fields are added like any
215
+ * object's properties.
216
+ */
217
+ declare function newCondition(): JsonSchemaProperty;
218
+ /**
219
+ * Add a conditional (if/then/else) to the object at `objectPath` (root when
220
+ * empty), appended to the given combinator array (`allOf` = AND, default;
221
+ * `anyOf` = OR). Returns the new model + the condition path.
222
+ */
223
+ declare function addConditional(model: DesignerModel, objectPath: PropertyPath, combinator?: ConditionCombinator): {
88
224
  model: DesignerModel;
89
225
  path: PropertyPath;
90
226
  };
227
+ /** Remove the conditional at `path` (a condition segment). Returns the new model. */
228
+ declare function removeConditional(model: DesignerModel, path: PropertyPath): DesignerModel;
229
+ /** Write the `if` of the conditional at `path` from a condition triple. */
230
+ declare function setCondition(model: DesignerModel, path: PropertyPath, ifSchema: JsonSchemaProperty): DesignerModel;
231
+ /** Add an empty `else` branch to the conditional at `path`. */
232
+ declare function addElse(model: DesignerModel, path: PropertyPath): DesignerModel;
233
+ /** Remove the `else` branch from the conditional at `path`. */
234
+ declare function removeElse(model: DesignerModel, path: PropertyPath): DesignerModel;
235
+ /**
236
+ * Switch the combinator (AND/OR) for the conditionals of the object at
237
+ * `objectPath`. Migrates every conditional member from the current combinator
238
+ * array to `next`. Non-conditional members (structural allOf/anyOf entries)
239
+ * stay put. No-op when already using `next` or when there are no conditionals.
240
+ */
241
+ declare function setConditionalBehavior(model: DesignerModel, objectPath: PropertyPath, next: ConditionCombinator): DesignerModel;
91
242
  /** Delete the property at path. Returns the new model. */
92
243
  declare function deleteProperty(model: DesignerModel, path: PropertyPath): DesignerModel;
93
244
  /** Rename the property at path. Rewrites key, order, required, and uiSchema. */
@@ -101,12 +252,45 @@ declare function moveProperty(model: DesignerModel, path: PropertyPath, directio
101
252
  declare function setRequired(model: DesignerModel, path: PropertyPath, required: boolean): DesignerModel;
102
253
  /** Replace the property definition at path (merges the given partial). */
103
254
  declare function updateProperty(model: DesignerModel, path: PropertyPath, patch: Partial<JsonSchemaProperty>): DesignerModel;
255
+ /** The kind a property can be changed to: a JSON Schema type, or a "oneOf" union. */
256
+ type PropertyTypeChoice = NonNullable<JsonSchemaProperty["type"]> | "oneOf";
257
+ /**
258
+ * Change the kind of the property at path, reshaping structural keys: object
259
+ * adds an empty `properties`; array adds `items`; oneOf adds branches + a
260
+ * discriminator; leaving a kind strips its keys. Type-specific validation keys
261
+ * that no longer apply are dropped.
262
+ */
263
+ declare function changePropertyType(model: DesignerModel, path: PropertyPath, next: PropertyTypeChoice): DesignerModel;
264
+ /**
265
+ * Point the property at path to a definition by name, writing a local `$ref`
266
+ * (`#/$defs/<name>`) and stripping type-specific keys. Turns a plain property
267
+ * into a reference usage. No-op for the root or an unknown target.
268
+ */
269
+ declare function setReference(model: DesignerModel, path: PropertyPath, name: string): DesignerModel;
270
+ /**
271
+ * Add a new empty object definition under `$defs`. Returns the new model and the
272
+ * path to the created definition for selection.
273
+ */
274
+ declare function addDefinition(model: DesignerModel, name?: string): {
275
+ model: DesignerModel;
276
+ path: PropertyPath;
277
+ };
278
+ /**
279
+ * Rename a `$defs` entry, preserving order, and rewrite every local `$ref` that
280
+ * targets it (both `#/$defs/` and `#/definitions/` forms) to the new pointer.
281
+ * No-op on collision, reserved, or empty names.
282
+ */
283
+ declare function renameDefinition(model: DesignerModel, oldName: string, newName: string): {
284
+ model: DesignerModel;
285
+ path: PropertyPath;
286
+ };
104
287
  /**
105
- * Change the type of the property at path, reshaping structural keys: switching to
106
- * object adds an empty `properties`; to array adds `items`; leaving them strips
107
- * those keys. Type-specific validation keys that no longer apply are dropped.
288
+ * Remove a `$defs` entry. Blocked (no-op) while any local `$ref` still targets
289
+ * it; the UI surfaces the usage count. Drops the `$defs` map when it empties.
108
290
  */
109
- declare function changePropertyType(model: DesignerModel, path: PropertyPath, nextType: JsonSchemaProperty["type"]): DesignerModel;
291
+ declare function removeDefinition(model: DesignerModel, name: string): DesignerModel;
292
+ /** Whether a definition is referenced anywhere (for UI delete-guard messaging). */
293
+ declare function definitionUsageCount(schema: JsonSchema, name: string): number;
110
294
  /** One enum choice: a value and an optional display label. */
111
295
  interface EnumChoice {
112
296
  value: string;
@@ -160,6 +344,58 @@ declare function getScopedLayout(schema: JsonSchema, uiSchema: UiSchemaMap, scop
160
344
  * x-ui-layout, preserving rows and other keys. Undefined values clear the key.
161
345
  */
162
346
  declare function setScopedLayoutOptions(model: DesignerModel, scopePath: PropertyPath, options: Pick<UILayoutConfig, "direction" | "gap" | "labels" | "compact">): DesignerModel;
347
+ /**
348
+ * Set the group definitions for a scope, preserving other x-ui-layout keys.
349
+ * Passing an empty list (or undefined) clears groups, returning the scope to the
350
+ * flat row layout. Root groups live in uiSchema["x-ui-layout"]; a container's
351
+ * live on the property's x-ui-layout (array -> items).
352
+ *
353
+ * Groups and flat rows are mutually exclusive (the renderer gives groups
354
+ * precedence), so setting groups drops `rows` -- otherwise the orphaned list
355
+ * would be invisible in both the form and the designer.
356
+ */
357
+ declare function setScopedLayoutGroups(model: DesignerModel, scopePath: PropertyPath, groups: UILayoutConfig["groups"]): DesignerModel;
358
+ /** Append an empty group. The label defaults to the next ordinal ("Group 3"). */
359
+ declare function addLayoutGroup(model: DesignerModel, scopePath: PropertyPath, label?: string): DesignerModel;
360
+ /** Rename one group. No-op for an unknown index or a blank label. */
361
+ declare function renameLayoutGroup(model: DesignerModel, scopePath: PropertyPath, groupIndex: number, label: string): DesignerModel;
362
+ /**
363
+ * Place a property into one group's row, removing it from wherever it currently
364
+ * sits across *all* groups. Passing a null target returns it to the pool.
365
+ *
366
+ * Needed because each group owns its own rows: without a cross-group operation a
367
+ * field could only be moved between groups by visiting its old group first.
368
+ */
369
+ declare function placeFieldInGroup(model: DesignerModel, scopePath: PropertyPath, key: string, target: {
370
+ groupIndex: number;
371
+ rowIndex: number;
372
+ } | null): DesignerModel;
373
+ /** Move one group left (-1) or right (+1). No-op at the ends. */
374
+ declare function moveLayoutGroup(model: DesignerModel, scopePath: PropertyPath, groupIndex: number, direction: -1 | 1): DesignerModel;
375
+ /**
376
+ * Set (or clear) a group's show/hide rule. Passing undefined makes the group
377
+ * unconditional.
378
+ */
379
+ declare function setLayoutGroupRule(model: DesignerModel, scopePath: PropertyPath, groupIndex: number, rule: UILayoutGroupConfig["rule"]): DesignerModel;
380
+ /**
381
+ * Set the group presentation for a scope ("groups", "stepper", or a custom
382
+ * template name). Passing undefined restores the default group presentation.
383
+ */
384
+ declare function setScopedLayoutVariant(model: DesignerModel, scopePath: PropertyPath, variant: UILayoutConfig["variant"]): DesignerModel;
385
+ /**
386
+ * Remove one group. Its rows are not discarded: removing the last remaining group
387
+ * flattens them back into the scope's flat row layout, so a single click can
388
+ * never destroy every field placement.
389
+ */
390
+ declare function removeLayoutGroup(model: DesignerModel, scopePath: PropertyPath, groupIndex: number): DesignerModel;
391
+ /** Replace the rows of one group in a scope. No-op for an unknown group index. */
392
+ declare function setScopedGroupRows(model: DesignerModel, scopePath: PropertyPath, groupIndex: number, rows: UILayoutRowConfig[]): DesignerModel;
393
+ /**
394
+ * Switch a scope between flat rows and groups, carrying the existing layout over
395
+ * so no field placement is lost: rows become the first group's rows, and leaving
396
+ * group mode flattens every group's rows back into one list.
397
+ */
398
+ declare function setScopedLayoutMode(model: DesignerModel, scopePath: PropertyPath, mode: "rows" | "groups"): DesignerModel;
163
399
  /** Set (or clear) the root global options (uiSchema["x-ui-globalOptions"]). */
164
400
  declare function setGlobalOptions(model: DesignerModel, options: UISchemaOptions | undefined): DesignerModel;
165
401
 
@@ -207,7 +443,8 @@ declare function embedUiSchema(schema: JsonSchema, uiSchema: Record<string, UISc
207
443
  * fully-locked shorthand). Every field is optional and defaults to the most
208
444
  * permissive value, so adding a capability never tightens an existing profile.
209
445
  *
210
- * Design: docs/design/schema-designer.md, section "Capability gating".
446
+ * Design: docs/design/schema-designer.md in this package; usage in
447
+ * docs/usage/capabilities.md.
211
448
  */
212
449
 
213
450
  /**
@@ -215,11 +452,17 @@ declare function embedUiSchema(schema: JsonSchema, uiSchema: Record<string, UISc
215
452
  * schema; "capabilities" is a meta-editor for the profile itself (a dev /
216
453
  * power-user affordance), not a preview -- it is opt-in and never in a default.
217
454
  */
218
- type PreviewKind = "form" | "table" | "json" | "capabilities";
455
+ declare const previewKindSchema: z.ZodEnum<{
456
+ form: "form";
457
+ table: "table";
458
+ json: "json";
459
+ capabilities: "capabilities";
460
+ }>;
461
+ type PreviewKind = z.infer<typeof previewKindSchema>;
219
462
  /**
220
463
  * The capabilities schema. Small by design -- it covers the real consumer needs
221
- * (sestina's audit-event and terminology schema editors) without a per-area
222
- * matrix we do not use yet.
464
+ * (locked-down, single-schema editors embedded in an application) without a
465
+ * per-area matrix we do not use yet.
223
466
  */
224
467
  declare const capabilitiesSchema: z.ZodObject<{
225
468
  mode: z.ZodDefault<z.ZodEnum<{
@@ -239,13 +482,15 @@ declare const capabilitiesSchema: z.ZodObject<{
239
482
  * fields optional on input (defaults are applied by resolveCapabilities).
240
483
  */
241
484
  type SchemaDesignerCapabilities = z.input<typeof capabilitiesSchema>;
485
+ /**
486
+ * The schema's own fields once resolved. Derived from the schema rather than
487
+ * restated, so adding or retyping a capability cannot silently drift from it.
488
+ * `Required` because resolveCapabilities defaults every optional field --
489
+ * `previews` by mode, `editablePaths` to empty -- so none is absent downstream.
490
+ */
491
+ type ResolvedSchemaFields = Required<z.output<typeof capabilitiesSchema>>;
242
492
  /** The fully-resolved capabilities, plus the derived flags used for gating. */
243
- interface ResolvedCapabilities {
244
- mode: "data" | "full";
245
- /** Effective preview list (defaulted by mode when not provided). */
246
- previews: PreviewKind[];
247
- /** JSON Pointer paths whose children are editable; empty = no scoping. */
248
- editablePaths: string[];
493
+ interface ResolvedCapabilities extends ResolvedSchemaFields {
249
494
  /** No structural edits at all (readOnly shorthand). */
250
495
  locked: boolean;
251
496
  /** editablePaths is non-empty -- only designated subtrees are editable. */
@@ -293,12 +538,17 @@ interface UseDesignerStateArgs {
293
538
  onChange?: (schema: JsonSchema, uiSchema: UiSchemaMap) => void;
294
539
  readOnly?: boolean;
295
540
  capabilities?: SchemaDesignerCapabilities;
541
+ /** Optional resolver for non-local `$ref`s (registry id / URI); local $defs
542
+ * resolve automatically. Used to expand ref nodes (read-only) in the tree. */
543
+ refResolver?: RefResolver;
296
544
  }
297
545
  interface DesignerState {
298
546
  schema: JsonSchema;
299
547
  uiSchema: UiSchemaMap;
300
548
  selection: PropertyPath | null;
301
549
  readOnly: boolean;
550
+ /** Resolver for $ref expansion in the tree (undefined = local $defs only). */
551
+ refResolver?: RefResolver;
302
552
  /** Resolved capability profile (readOnly + editablePaths applied). */
303
553
  capabilities: ResolvedCapabilities;
304
554
  /** Whether the property at path may be renamed / deleted / retyped / reordered. */
@@ -306,13 +556,43 @@ interface DesignerState {
306
556
  /** Whether a child property may be added to the container at path (root = []). */
307
557
  canAddChild: (path: PropertyPath) => boolean;
308
558
  select: (path: PropertyPath | null) => void;
309
- addProperty: (parentPath: PropertyPath, kind: "scalar" | "object" | "array") => void;
559
+ addProperty: (parentPath: PropertyPath, kind: NewPropertyKind) => void;
560
+ /** Add a branch to the oneOf property at path. */
561
+ addBranch: (path: PropertyPath) => void;
562
+ /** Remove the branch at path (a branch segment). */
563
+ removeBranch: (path: PropertyPath) => void;
564
+ /** Move the branch at path up (-1) or down (+1). */
565
+ moveBranch: (path: PropertyPath, direction: -1 | 1) => void;
566
+ /** Add an if/then/else conditional to the object at path (root when empty). */
567
+ addConditional: (objectPath: PropertyPath, combinator?: ConditionCombinator) => void;
568
+ /** Switch the AND/OR combinator for the conditionals of the object at path. */
569
+ setConditionalBehavior: (objectPath: PropertyPath, combinator: ConditionCombinator) => void;
570
+ /** Remove the conditional at path (a condition segment). */
571
+ removeConditional: (path: PropertyPath) => void;
572
+ /** Write the `if` schema of the conditional at path. */
573
+ setCondition: (path: PropertyPath, ifSchema: JsonSchemaProperty) => void;
574
+ /** Add an empty `else` branch to the conditional at path. */
575
+ addElse: (path: PropertyPath) => void;
576
+ /** Remove the `else` branch from the conditional at path. */
577
+ removeElse: (path: PropertyPath) => void;
310
578
  deleteProperty: (path: PropertyPath) => void;
311
579
  renameProperty: (path: PropertyPath, nextKey: string) => void;
312
580
  moveProperty: (path: PropertyPath, direction: -1 | 1) => void;
313
581
  setRequired: (path: PropertyPath, required: boolean) => void;
314
582
  updateProperty: (path: PropertyPath, patch: Partial<JsonSchemaProperty>) => void;
315
- changePropertyType: (path: PropertyPath, nextType: JsonSchemaProperty["type"]) => void;
583
+ changePropertyType: (path: PropertyPath, next: PropertyTypeChoice) => void;
584
+ /** Point the property at path at a definition (writes a local `$ref`). */
585
+ setReference: (path: PropertyPath, name: string) => void;
586
+ /** Add a new empty object definition to `$defs`. Selects the new node. */
587
+ addDefinition: (name?: string) => void;
588
+ /** Rename a `$defs` entry; rewrites every local `$ref` that targets it. */
589
+ renameDefinition: (oldName: string, newName: string) => void;
590
+ /** Remove an unused `$defs` entry (blocked while any `$ref` targets it). */
591
+ removeDefinition: (name: string) => void;
592
+ /** Replace the whole JSON Schema (from the editable JSON Schema source tab). */
593
+ replaceSchema: (schema: JsonSchema) => void;
594
+ /** Replace the whole UI Schema map (from the editable UI Schema source tab). */
595
+ replaceUiSchema: (uiSchema: UiSchemaMap) => void;
316
596
  setEnum: (path: PropertyPath, choices: EnumChoice[]) => void;
317
597
  setUiEntry: (path: PropertyPath, entry: UISchema | undefined) => void;
318
598
  setTableHints: (path: PropertyPath, hints: TableHints) => void;
@@ -321,6 +601,29 @@ interface DesignerState {
321
601
  setRootLayoutRows: (rows: UILayoutConfig["rows"]) => void;
322
602
  setScopedLayoutRows: (scopePath: PropertyPath, rows: UILayoutConfig["rows"]) => void;
323
603
  setScopedLayoutOptions: (scopePath: PropertyPath, options: Pick<UILayoutConfig, "direction" | "gap" | "labels" | "compact">) => void;
604
+ /** Replace a scope's group definitions (empty clears groups -> flat rows). */
605
+ setScopedLayoutGroups: (scopePath: PropertyPath, groups: UILayoutConfig["groups"]) => void;
606
+ /** Replace the rows of one group within a scope. */
607
+ setScopedGroupRows: (scopePath: PropertyPath, groupIndex: number, rows: UILayoutRowConfig[]) => void;
608
+ /** Switch a scope between flat rows and groups, carrying placements over. */
609
+ setScopedLayoutMode: (scopePath: PropertyPath, mode: "rows" | "groups") => void;
610
+ /** Append an empty group to a scope. */
611
+ addLayoutGroup: (scopePath: PropertyPath, label?: string) => void;
612
+ /** Rename one group of a scope. */
613
+ renameLayoutGroup: (scopePath: PropertyPath, groupIndex: number, label: string) => void;
614
+ /** Remove one group; removing the last flattens its rows back to flat rows. */
615
+ removeLayoutGroup: (scopePath: PropertyPath, groupIndex: number) => void;
616
+ /** Move one group left (-1) or right (+1). */
617
+ moveLayoutGroup: (scopePath: PropertyPath, groupIndex: number, direction: -1 | 1) => void;
618
+ /** Set (or clear) a group's show/hide rule. */
619
+ setLayoutGroupRule: (scopePath: PropertyPath, groupIndex: number, rule: UILayoutGroupConfig["rule"]) => void;
620
+ /** Choose the group presentation ("tabs", "stepper", "sections", ...). */
621
+ setScopedLayoutVariant: (scopePath: PropertyPath, variant: UILayoutConfig["variant"]) => void;
622
+ /** Place a property into a group's row, or back in the pool (null target). */
623
+ placeFieldInGroup: (scopePath: PropertyPath, key: string, target: {
624
+ groupIndex: number;
625
+ rowIndex: number;
626
+ } | null) => void;
324
627
  setGlobalOptions: (options: UISchemaOptions | undefined) => void;
325
628
  }
326
629
  declare function useDesignerState(args: UseDesignerStateArgs): DesignerState;
@@ -359,6 +662,13 @@ interface SchemaDesignerProps {
359
662
  capabilities?: SchemaDesignerCapabilities;
360
663
  /** Option descriptors for custom widgets, keyed by widget name (merged over built-ins). */
361
664
  widgetOptions?: Record<string, WidgetOptionDef[]>;
665
+ /**
666
+ * Optional resolver for non-local `$ref`s (schema-registry id / canonical
667
+ * URI). Local `#/$defs` refs resolve automatically. When provided, ref nodes
668
+ * are expanded read-only in the tree. Synchronous -- pre-load referenced
669
+ * schemas and pass a lookup.
670
+ */
671
+ refResolver?: RefResolver;
362
672
  }
363
673
  declare function SchemaDesigner(props: SchemaDesignerProps): React.ReactElement;
364
674
 
@@ -528,4 +838,4 @@ interface FormSettingsSheetProps {
528
838
  }
529
839
  declare function FormSettingsSheet({ open, onOpenChange, state, }: FormSettingsSheetProps): React.ReactElement;
530
840
 
531
- export { type CapabilitiesValidation, DataTab, type DesignerModel, type DesignerState, type EnumChoice, EnumEditor, FormLayoutDesigner, FormSettingsSheet, Inspector, type LayoutScope, PreviewPane, type PropertyKind, type PropertyNode, type PropertyPath, PropertyTree, type ResolvedCapabilities, SchemaDesigner, type SchemaDesignerCapabilities, type SchemaDesignerProps, type TableHints, TableHintsTab, TablePreview, UITab, type UiSchemaMap, type UseDesignerStateArgs, type WidgetChoice, addProperty, buildPropertyTree, capabilitiesSchema, changePropertyType, childContainer, childrenOf, compatibleWidgets, deleteProperty, embedUiSchema, getProperty, getScopedLayout, getUiEntryAtPath, isContainer, layoutScopes, moveProperty, newProperty, orderedKeys, pathKey, propertyKind, readEnumChoices, readTableHints, renameProperty, resolveCapabilities, samePath, sampleRows, setEnum, setGlobalOptions, setPropertyLayout, setRequired, setRootLayout, setRootLayoutRows, setScopedLayoutOptions, setScopedLayoutRows, setTableHints, setUiEntry, updateProperty, useDesignerState, validateCapabilities };
841
+ export { type CapabilitiesValidation, type ChildSlot, type ConditionCombinator, DataTab, type DesignerModel, type DesignerState, type EnumChoice, EnumEditor, FormLayoutDesigner, FormSettingsSheet, Inspector, type LayoutScope, type NewPropertyKind, type PathSegment, type PathSlot, type PreviewKind, PreviewPane, type PropertyKind, type PropertyNode, type PropertyPath, PropertyTree, type PropertyTypeChoice, type ResolvedCapabilities, SchemaDesigner, type SchemaDesignerCapabilities, type SchemaDesignerProps, type SlotKind, type TableHints, TableHintsTab, TablePreview, UITab, type UiSchemaMap, type UseDesignerStateArgs, type WidgetChoice, addBranch, addConditional, addDefinition, addElse, addLayoutGroup, addProperty, branch, buildPropertyTree, capabilitiesSchema, changePropertyType, childContainer, childSlots, compatibleWidgets, condition, def, definitionUsageCount, definitionsOf, deleteProperty, embedUiSchema, getProperty, getScopedLayout, getUiEntryAtPath, isContainer, isReservedKey, layoutScopes, moveBranch, moveLayoutGroup, moveProperty, newBranch, newCondition, newProperty, orderedKeys, parsePathKey, pathKey, placeFieldInGroup, prop, propertyKind, readEnumChoices, readTableHints, removeBranch, removeConditional, removeDefinition, removeElse, removeLayoutGroup, renameDefinition, renameLayoutGroup, renameProperty, resolveCapabilities, samePath, sameSegment, sampleRows, schemaAsRootProp, setCondition, setConditionalBehavior, setEnum, setGlobalOptions, setLayoutGroupRule, setPropertyLayout, setReference, setRequired, setRootLayout, setRootLayoutRows, setScopedGroupRows, setScopedLayoutGroups, setScopedLayoutMode, setScopedLayoutOptions, setScopedLayoutRows, setScopedLayoutVariant, setTableHints, setUiEntry, slot, updateProperty, useDesignerState, validateCapabilities };