@petrarca/sonnet-schema-designer 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -1,42 +1,258 @@
1
1
  // src/model/paths.ts
2
+ import {
3
+ isOneOf,
4
+ getDiscriminator,
5
+ getBranchLabel
6
+ } from "@petrarca/sonnet-core/schema";
7
+ function isReservedKey(key) {
8
+ return /^#(branch|condition|def|slot):/.test(key);
9
+ }
10
+ function prop(key) {
11
+ return { kind: "prop", key };
12
+ }
13
+ function branch(index) {
14
+ return { kind: "branch", index };
15
+ }
16
+ function condition(index, combinator = "allOf") {
17
+ return { kind: "condition", combinator, index };
18
+ }
19
+ function slot(name) {
20
+ return { kind: "slot", slot: name };
21
+ }
22
+ function def(name) {
23
+ return { kind: "def", name };
24
+ }
25
+ var SEP = "/";
26
+ function escapeToken(raw) {
27
+ return raw.replace(/\\/g, "\\\\").replace(/\//g, "\\/");
28
+ }
29
+ function unescapeToken(raw) {
30
+ return raw.replace(/\\\//g, "/").replace(/\\\\/g, "\\");
31
+ }
32
+ function encodeSegment(seg) {
33
+ switch (seg.kind) {
34
+ case "prop":
35
+ return escapeToken(seg.key);
36
+ case "branch":
37
+ return `#branch:${seg.index}`;
38
+ case "condition":
39
+ return `#condition:${seg.combinator}:${seg.index}`;
40
+ case "slot":
41
+ return `#slot:${seg.slot}`;
42
+ case "def":
43
+ return `#def:${escapeToken(seg.name)}`;
44
+ }
45
+ }
46
+ var VALID_SLOTS = ["then", "else", "items"];
47
+ function safeIndex(raw) {
48
+ const n = Number(raw);
49
+ return Number.isInteger(n) && n >= 0 ? n : 0;
50
+ }
51
+ function decodeCondition(rest) {
52
+ const sep = rest.indexOf(":");
53
+ const combinatorRaw = sep >= 0 ? rest.slice(0, sep) : "allOf";
54
+ const combinator = combinatorRaw === "anyOf" ? "anyOf" : "allOf";
55
+ const index = safeIndex(sep >= 0 ? rest.slice(sep + 1) : rest);
56
+ return { kind: "condition", combinator, index };
57
+ }
58
+ function decodeSegment(token) {
59
+ if (token.startsWith("#branch:")) {
60
+ return { kind: "branch", index: safeIndex(token.slice("#branch:".length)) };
61
+ }
62
+ if (token.startsWith("#condition:")) {
63
+ return decodeCondition(token.slice("#condition:".length));
64
+ }
65
+ if (token.startsWith("#slot:")) {
66
+ const s = token.slice("#slot:".length);
67
+ return { kind: "slot", slot: VALID_SLOTS.includes(s) ? s : "items" };
68
+ }
69
+ if (token.startsWith("#def:")) {
70
+ return { kind: "def", name: unescapeToken(token.slice("#def:".length)) };
71
+ }
72
+ return { kind: "prop", key: unescapeToken(token) };
73
+ }
74
+ function splitTokens(key) {
75
+ const tokens = [];
76
+ let current = "";
77
+ for (let i = 0; i < key.length; i++) {
78
+ const ch = key[i];
79
+ if (ch === "\\" && i + 1 < key.length) {
80
+ current += ch + key[i + 1];
81
+ i++;
82
+ } else if (ch === SEP) {
83
+ tokens.push(current);
84
+ current = "";
85
+ } else {
86
+ current += ch;
87
+ }
88
+ }
89
+ tokens.push(current);
90
+ return tokens;
91
+ }
2
92
  function pathKey(path) {
3
- return path.join(".");
93
+ return path.map(encodeSegment).join(SEP);
94
+ }
95
+ function parsePathKey(key) {
96
+ if (key === "") return [];
97
+ return splitTokens(key).map(decodeSegment);
98
+ }
99
+ function segmentIdentity(seg) {
100
+ switch (seg.kind) {
101
+ case "prop":
102
+ return `prop:${seg.key}`;
103
+ case "branch":
104
+ return `branch:${seg.index}`;
105
+ case "condition":
106
+ return `condition:${seg.combinator}:${seg.index}`;
107
+ case "slot":
108
+ return `slot:${seg.slot}`;
109
+ case "def":
110
+ return `def:${seg.name}`;
111
+ }
112
+ }
113
+ function sameSegment(a, b) {
114
+ return segmentIdentity(a) === segmentIdentity(b);
4
115
  }
5
116
  function samePath(a, b) {
6
117
  if (a === null || b === null) return a === b;
7
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
118
+ return a.length === b.length && a.every((seg, i) => sameSegment(seg, b[i]));
8
119
  }
9
- function childContainer(prop) {
10
- if (prop.type === "object" && prop.properties) return prop.properties;
11
- if (prop.type === "array" && prop.items?.type === "object") {
12
- return prop.items.properties;
120
+ function childContainer(prop2) {
121
+ const typeIsObjectLike = prop2.type === "object" || prop2.type === void 0;
122
+ if (typeIsObjectLike && prop2.properties && !prop2.oneOf)
123
+ return prop2.properties;
124
+ if (prop2.type === "array" && prop2.items?.type === "object") {
125
+ return prop2.items.properties;
13
126
  }
14
127
  return void 0;
15
128
  }
129
+ function isConditionalMember(member) {
130
+ return !!member.if;
131
+ }
132
+ function membersOfCombinator(property, combinator) {
133
+ const arr = property[combinator];
134
+ if (!Array.isArray(arr)) return [];
135
+ return arr.map((member, index) => ({ member, combinator, index })).filter(({ member }) => isConditionalMember(member));
136
+ }
137
+ function conditionalMembers(property) {
138
+ return [
139
+ ...membersOfCombinator(property, "allOf"),
140
+ ...membersOfCombinator(property, "anyOf")
141
+ ];
142
+ }
143
+ function conditionLabel(member) {
144
+ const field = Object.keys(member.if?.properties ?? {})[0] ?? member.if?.required?.[0];
145
+ return field ? `if ${field}` : "if ...";
146
+ }
147
+ function conditionalSlots(member) {
148
+ const slots = [];
149
+ const add = (name, sub) => {
150
+ if (sub) {
151
+ slots.push({
152
+ segment: slot(name),
153
+ label: name,
154
+ property: sub,
155
+ kind: "conditional",
156
+ removable: false,
157
+ reorderable: false
158
+ });
159
+ }
160
+ };
161
+ add("then", member.then);
162
+ add("else", member.else);
163
+ return slots;
164
+ }
165
+ function definitionsOf(property) {
166
+ return property.$defs ?? property.definitions;
167
+ }
168
+ function definitionSlots(property) {
169
+ const defs = definitionsOf(property);
170
+ if (!defs) return [];
171
+ return Object.keys(defs).map((name) => ({
172
+ segment: def(name),
173
+ label: name,
174
+ property: defs[name],
175
+ kind: "definition",
176
+ removable: true,
177
+ reorderable: false
178
+ }));
179
+ }
180
+ function childSlots(property) {
181
+ if (isConditionalMember(property)) return conditionalSlots(property);
182
+ if (isOneOf(property)) {
183
+ const discriminator = getDiscriminator(property);
184
+ return (property.oneOf ?? []).map((b, index) => ({
185
+ segment: branch(index),
186
+ label: getBranchLabel(b, discriminator, index),
187
+ property: b,
188
+ kind: "branch",
189
+ removable: true,
190
+ reorderable: true
191
+ }));
192
+ }
193
+ const container = childContainer(property);
194
+ const propertySlots = container ? Object.keys(container).map((key) => ({
195
+ segment: prop(key),
196
+ label: key,
197
+ property: container[key],
198
+ kind: "property",
199
+ removable: true,
200
+ reorderable: true
201
+ })) : [];
202
+ const conditionSlots = conditionalMembers(property).map(
203
+ ({ member, combinator, index }) => ({
204
+ segment: condition(index, combinator),
205
+ label: conditionLabel(member),
206
+ property: member,
207
+ kind: "conditional",
208
+ removable: true,
209
+ reorderable: false
210
+ })
211
+ );
212
+ return [...propertySlots, ...conditionSlots, ...definitionSlots(property)];
213
+ }
214
+ function stepInto(parent, seg) {
215
+ if (seg.kind === "prop") return childContainer(parent)?.[seg.key];
216
+ if (seg.kind === "branch") return parent.oneOf?.[seg.index];
217
+ if (seg.kind === "condition") return parent[seg.combinator]?.[seg.index];
218
+ if (seg.kind === "def") return definitionsOf(parent)?.[seg.name];
219
+ if (seg.slot === "items") return parent.items;
220
+ return parent[seg.slot];
221
+ }
222
+ function schemaAsRootProp(schema) {
223
+ return {
224
+ type: "object",
225
+ properties: schema.properties,
226
+ required: schema.required,
227
+ "x-ui-order": schema["x-ui-order"],
228
+ allOf: schema.allOf,
229
+ anyOf: schema.anyOf,
230
+ $defs: schema.$defs,
231
+ definitions: schema.definitions
232
+ };
233
+ }
16
234
  function getProperty(schema, path) {
17
235
  if (path.length === 0) return void 0;
18
- let container = schema.properties;
19
- let prop;
236
+ const root = schemaAsRootProp(schema);
237
+ let current = root;
20
238
  for (const seg of path) {
21
- if (!container) return void 0;
22
- prop = container[seg];
23
- if (!prop) return void 0;
24
- container = childContainer(prop);
239
+ if (!current) return void 0;
240
+ current = stepInto(current, seg);
25
241
  }
26
- return prop;
27
- }
28
- function isContainer(prop) {
29
- return childContainer(prop) !== void 0;
242
+ return current;
30
243
  }
31
- function childrenOf(prop) {
32
- return childContainer(prop) ?? {};
244
+ function isContainer(prop2) {
245
+ return childSlots(prop2).length > 0 || childContainer(prop2) !== void 0;
33
246
  }
34
247
 
35
248
  // src/model/view.ts
36
- function propertyKind(prop) {
37
- if (prop.type === "object" && prop.properties) return "object";
38
- if (prop.type === "array") return "array";
39
- if (Array.isArray(prop.enum) && prop.enum.length > 0) return "enum";
249
+ import { isOneOf as isOneOf2 } from "@petrarca/sonnet-core/schema";
250
+ function propertyKind(prop2) {
251
+ if (prop2.if) return "conditional";
252
+ if (isOneOf2(prop2)) return "oneOf";
253
+ if (prop2.type === "object" && prop2.properties) return "object";
254
+ if (prop2.type === "array") return "array";
255
+ if (Array.isArray(prop2.enum) && prop2.enum.length > 0) return "enum";
40
256
  return "scalar";
41
257
  }
42
258
  function arrayCardinality(min, max) {
@@ -44,12 +260,14 @@ function arrayCardinality(min, max) {
44
260
  if (min != null && min === max) return `${min}`;
45
261
  return `${min ?? ""}..${max ?? ""}`;
46
262
  }
47
- function typeLabelOf(prop) {
48
- if (prop.type === "array") {
49
- const itemType = prop.items?.type ?? "any";
50
- return `${itemType}[${arrayCardinality(prop.minItems, prop.maxItems)}]`;
263
+ function typeLabelOf(prop2) {
264
+ if (prop2.if) return "if/then/else";
265
+ if (isOneOf2(prop2)) return "oneOf";
266
+ if (prop2.type === "array") {
267
+ const itemType = prop2.items?.type ?? "any";
268
+ return `${itemType}[${arrayCardinality(prop2.minItems, prop2.maxItems)}]`;
51
269
  }
52
- return prop.type ?? "any";
270
+ return prop2.type ?? "any";
53
271
  }
54
272
  function orderedKeys(properties, order) {
55
273
  const keys = Object.keys(properties);
@@ -58,53 +276,165 @@ function orderedKeys(properties, order) {
58
276
  const rest = keys.filter((k) => !inOrder.includes(k));
59
277
  return [...inOrder, ...rest];
60
278
  }
61
- function buildNode(key, prop, parentPath, requiredKeys) {
62
- const path = [...parentPath, key];
63
- const widget = typeof prop["x-ui-widget"] === "string" ? prop["x-ui-widget"] : void 0;
279
+ function requiredOf(prop2) {
280
+ if (prop2.type === "object") return prop2.required;
281
+ if (prop2.type === "array") return prop2.items?.required;
282
+ return void 0;
283
+ }
284
+ function orderOf(prop2) {
285
+ if (prop2.type === "array") return prop2.items?.["x-ui-order"];
286
+ return prop2["x-ui-order"];
287
+ }
288
+ function nodeKind(slot2, prop2) {
289
+ if (slot2.kind === "branch") return "object";
290
+ if (slot2.segment.kind === "def") return "definition";
291
+ if (slot2.segment.kind === "slot") return "object";
292
+ return propertyKind(prop2);
293
+ }
294
+ function requiredForChildren(prop2, isSlot) {
295
+ return isSlot ? prop2.required : requiredOf(prop2);
296
+ }
297
+ function resolveRefTarget(prop2, resolve) {
298
+ if (!prop2.$ref || !resolve) return void 0;
299
+ const target = resolve(prop2.$ref);
300
+ if (!target) return void 0;
301
+ const { $ref: _drop, ...overrides } = prop2;
302
+ return { ...target, ...overrides };
303
+ }
304
+ function refShapeOf(slot2, prop2, resolve) {
305
+ const refTarget = resolveRefTarget(prop2, resolve);
306
+ if (refTarget) {
307
+ return {
308
+ kind: "reference",
309
+ typeLabel: refLabel(prop2.$ref),
310
+ reference: prop2.$ref,
311
+ container: refTarget,
312
+ childrenFromRef: true
313
+ };
314
+ }
315
+ if (prop2.type === "array" && prop2.items?.$ref) {
316
+ const itemTarget = resolveRefTarget(prop2.items, resolve);
317
+ if (itemTarget) {
318
+ return {
319
+ kind: "array",
320
+ typeLabel: arrayItemLabel(prop2.items.$ref, prop2),
321
+ reference: prop2.items.$ref,
322
+ container: itemTarget,
323
+ childrenFromRef: true
324
+ };
325
+ }
326
+ }
327
+ return {
328
+ kind: nodeKind(slot2, prop2),
329
+ typeLabel: typeLabelOf(prop2),
330
+ container: prop2,
331
+ childrenFromRef: false
332
+ };
333
+ }
334
+ function buildNode(slot2, parentPath, required, resolve, inRef) {
335
+ const path = [...parentPath, slot2.segment];
336
+ const prop2 = slot2.property;
337
+ const widget = typeof prop2["x-ui-widget"] === "string" ? prop2["x-ui-widget"] : void 0;
338
+ const ref = refShapeOf(slot2, prop2, resolve);
64
339
  const node = {
65
340
  path,
66
- key,
67
- property: prop,
68
- kind: propertyKind(prop),
69
- required: !!requiredKeys?.includes(key),
70
- typeLabel: typeLabelOf(prop),
341
+ key: slot2.label,
342
+ property: prop2,
343
+ kind: ref.kind,
344
+ slotKind: slot2.kind,
345
+ required,
346
+ typeLabel: ref.typeLabel,
71
347
  widget,
348
+ reference: ref.reference,
349
+ readOnly: inRef || void 0,
72
350
  children: []
73
351
  };
74
- if (isContainer(prop)) {
75
- const kids = childrenOf(prop);
76
- const childRequired = prop.type === "object" ? prop.required : prop.items?.required;
77
- const childOrder = prop.type === "object" ? prop["x-ui-order"] : prop.items?.["x-ui-order"];
78
- node.children = orderedKeys(kids, childOrder).map(
79
- (k) => buildNode(k, kids[k], path, childRequired)
352
+ if (isContainer(ref.container)) {
353
+ const isSlot = slot2.segment.kind === "slot";
354
+ const req = requiredForChildren(ref.container, isSlot);
355
+ const childrenInRef = inRef || ref.childrenFromRef;
356
+ node.children = orderedSlots(ref.container).map(
357
+ (child) => buildNode(
358
+ child,
359
+ path,
360
+ child.kind === "property" ? !!req?.includes(child.label) : false,
361
+ resolve,
362
+ childrenInRef
363
+ )
80
364
  );
81
365
  }
82
366
  return node;
83
367
  }
84
- function buildPropertyTree(schema) {
85
- return orderedKeys(schema.properties, schema["x-ui-order"]).map(
86
- (key) => buildNode(key, schema.properties[key], [], schema.required)
368
+ function refName(ref) {
369
+ const local = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
370
+ return local ? local[1] : ref.split("/").pop() || ref;
371
+ }
372
+ function refLabel(ref) {
373
+ return `\u2192 ${refName(ref)}`;
374
+ }
375
+ function arrayItemLabel(ref, prop2) {
376
+ return `${refName(ref)}[${arrayCardinality(prop2.minItems, prop2.maxItems)}]`;
377
+ }
378
+ function orderedSlots(prop2) {
379
+ const slots = childSlots(prop2);
380
+ const propertySlots = slots.filter((s) => s.kind === "property");
381
+ const otherSlots = slots.filter((s) => s.kind !== "property");
382
+ const order = orderOf(prop2);
383
+ if (propertySlots.length === 0 || !order || order.length === 0) return slots;
384
+ const byLabel = new Map(propertySlots.map((s) => [s.label, s]));
385
+ const inOrder = order.filter((k) => byLabel.has(k)).map((k) => byLabel.get(k));
386
+ const rest = propertySlots.filter((s) => !order.includes(s.label));
387
+ return [...inOrder, ...rest, ...otherSlots];
388
+ }
389
+ function buildPropertyTree(schema, resolve) {
390
+ const effective = withInDocumentFallback(schema, resolve);
391
+ const rootProp = schemaAsRootProp(schema);
392
+ return orderedSlots(rootProp).map(
393
+ (slot2) => buildNode(
394
+ slot2,
395
+ [],
396
+ !!schema.required?.includes(slot2.label),
397
+ effective,
398
+ false
399
+ )
87
400
  );
88
401
  }
402
+ function withInDocumentFallback(schema, resolve) {
403
+ const defs = schema.$defs ?? schema.definitions;
404
+ if (!defs) return resolve;
405
+ const local = (ref) => {
406
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
407
+ return m ? defs[m[1]] : void 0;
408
+ };
409
+ return resolve ? (ref) => resolve(ref) ?? local(ref) : local;
410
+ }
89
411
  function layoutScopes(schema) {
90
412
  const scopes = [{ path: [], label: "Form (root)" }];
91
- const walk = (nodes) => {
413
+ const walk = (nodes, labelPrefix) => {
92
414
  for (const node of nodes) {
93
- if (node.kind === "object" || node.kind === "array") {
415
+ const allProps = node.path.every((s) => s.kind === "prop");
416
+ if (allProps && (node.kind === "object" || node.kind === "array")) {
417
+ const labels = [...labelPrefix, node.key];
94
418
  const suffix = node.kind === "array" ? " (items)" : "";
95
419
  scopes.push({
96
420
  path: node.path,
97
- label: node.path.join(" \u203A ") + suffix
421
+ label: labels.join(" \u203A ") + suffix
98
422
  });
99
- walk(node.children);
423
+ walk(node.children, labels);
100
424
  }
101
425
  }
102
426
  };
103
- walk(buildPropertyTree(schema));
427
+ walk(buildPropertyTree(schema), []);
104
428
  return scopes;
105
429
  }
106
430
 
107
431
  // src/model/mutate.ts
432
+ function segKey(seg) {
433
+ return seg && seg.kind === "prop" ? seg.key : void 0;
434
+ }
435
+ function propKeys(path) {
436
+ return path.flatMap((s) => s.kind === "prop" ? [s.key] : []);
437
+ }
108
438
  function clone(v) {
109
439
  return structuredClone(v);
110
440
  }
@@ -114,10 +444,22 @@ function newProperty(kind) {
114
444
  return { type: "object", properties: {} };
115
445
  case "array":
116
446
  return { type: "array", items: { type: "object", properties: {} } };
447
+ case "oneOf":
448
+ return {
449
+ type: "object",
450
+ discriminator: { propertyName: "kind" },
451
+ oneOf: [newBranch("option_1"), newBranch("option_2")]
452
+ };
117
453
  default:
118
454
  return { type: "string" };
119
455
  }
120
456
  }
457
+ function newBranch(value) {
458
+ return {
459
+ title: value,
460
+ properties: { kind: { const: value } }
461
+ };
462
+ }
121
463
  function resolveContainer(schema, parentPath) {
122
464
  if (parentPath.length === 0) {
123
465
  return {
@@ -136,9 +478,11 @@ function resolveContainer(schema, parentPath) {
136
478
  }
137
479
  const owner = getProperty(schema, parentPath);
138
480
  if (!owner) return null;
139
- const properties = childContainer(owner);
140
- if (!properties) return null;
481
+ if (owner.oneOf) return null;
141
482
  const target = owner.type === "array" ? owner.items : owner;
483
+ if (!target) return null;
484
+ if (!target.properties) target.properties = {};
485
+ const properties = target.properties;
142
486
  return {
143
487
  properties,
144
488
  getOrder: () => target["x-ui-order"],
@@ -169,14 +513,135 @@ function addProperty(model, parentPath, kind) {
169
513
  if (order) container.setOrder([...order, key]);
170
514
  return {
171
515
  model: { schema, uiSchema: model.uiSchema },
172
- path: [...parentPath, key]
516
+ path: [...parentPath, prop(key)]
517
+ };
518
+ }
519
+ function uniqueBranchValue(oneOf) {
520
+ const existing = new Set(oneOf.map((b) => b.title).filter(Boolean));
521
+ let i = 1;
522
+ while (existing.has(`option_${i}`)) i++;
523
+ return `option_${i}`;
524
+ }
525
+ function addBranch(model, path) {
526
+ const schema = clone(model.schema);
527
+ const owner = getProperty(schema, path);
528
+ if (!owner || !owner.oneOf) return { model, path };
529
+ const index = owner.oneOf.length;
530
+ owner.oneOf.push(newBranch(uniqueBranchValue(owner.oneOf)));
531
+ return {
532
+ model: { schema, uiSchema: model.uiSchema },
533
+ path: [...path, branch(index)]
534
+ };
535
+ }
536
+ function removeBranch(model, path) {
537
+ const last = path[path.length - 1];
538
+ if (!last || last.kind !== "branch") return model;
539
+ const schema = clone(model.schema);
540
+ const owner = getProperty(schema, path.slice(0, -1));
541
+ if (!owner?.oneOf) return model;
542
+ owner.oneOf.splice(last.index, 1);
543
+ return { schema, uiSchema: model.uiSchema };
544
+ }
545
+ function moveBranch(model, path, direction) {
546
+ const last = path[path.length - 1];
547
+ if (!last || last.kind !== "branch") return model;
548
+ const schema = clone(model.schema);
549
+ const owner = getProperty(schema, path.slice(0, -1));
550
+ if (!owner?.oneOf) return model;
551
+ const from = last.index;
552
+ const to = from + direction;
553
+ if (to < 0 || to >= owner.oneOf.length) return model;
554
+ const [moved] = owner.oneOf.splice(from, 1);
555
+ owner.oneOf.splice(to, 0, moved);
556
+ return { schema, uiSchema: model.uiSchema };
557
+ }
558
+ function newCondition() {
559
+ return { if: { properties: {} }, then: { properties: {} } };
560
+ }
561
+ function addConditional(model, objectPath, combinator = "allOf") {
562
+ const schema = clone(model.schema);
563
+ const owner = conditionalOwner(schema, objectPath);
564
+ if (!owner) return { model, path: objectPath };
565
+ if (!Array.isArray(owner[combinator])) owner[combinator] = [];
566
+ const arr = owner[combinator];
567
+ const index = arr.length;
568
+ arr.push(newCondition());
569
+ return {
570
+ model: { schema, uiSchema: model.uiSchema },
571
+ path: [...objectPath, condition(index, combinator)]
173
572
  };
174
573
  }
574
+ function conditionalOwner(schema, objectPath) {
575
+ if (objectPath.length === 0) return schema;
576
+ const owner = getProperty(schema, objectPath);
577
+ if (!owner) return null;
578
+ const objectLike = owner.type === "object" || owner.type === void 0;
579
+ if (!objectLike || owner.oneOf) return null;
580
+ return owner;
581
+ }
582
+ function removeConditional(model, path) {
583
+ const last = path[path.length - 1];
584
+ if (!last || last.kind !== "condition") return model;
585
+ const schema = clone(model.schema);
586
+ const owner = conditionalOwner(schema, path.slice(0, -1));
587
+ const arr = owner?.[last.combinator];
588
+ if (!arr) return model;
589
+ arr.splice(last.index, 1);
590
+ if (arr.length === 0) delete owner[last.combinator];
591
+ return { schema, uiSchema: model.uiSchema };
592
+ }
593
+ function setCondition(model, path, ifSchema) {
594
+ const last = path[path.length - 1];
595
+ if (!last || last.kind !== "condition") return model;
596
+ const schema = clone(model.schema);
597
+ const member = getProperty(schema, path);
598
+ if (!member) return model;
599
+ member.if = ifSchema;
600
+ if (!member.then) member.then = { properties: {} };
601
+ return { schema, uiSchema: model.uiSchema };
602
+ }
603
+ function addElse(model, path) {
604
+ const last = path[path.length - 1];
605
+ if (!last || last.kind !== "condition") return model;
606
+ const schema = clone(model.schema);
607
+ const member = getProperty(schema, path);
608
+ if (!member || member.else) return model;
609
+ member.else = { properties: {} };
610
+ return { schema, uiSchema: model.uiSchema };
611
+ }
612
+ function removeElse(model, path) {
613
+ const last = path[path.length - 1];
614
+ if (!last || last.kind !== "condition") return model;
615
+ const schema = clone(model.schema);
616
+ const member = getProperty(schema, path);
617
+ if (!member || !member.else) return model;
618
+ delete member.else;
619
+ return { schema, uiSchema: model.uiSchema };
620
+ }
621
+ function otherCombinator(c) {
622
+ return c === "allOf" ? "anyOf" : "allOf";
623
+ }
624
+ function setConditionalBehavior(model, objectPath, next) {
625
+ const schema = clone(model.schema);
626
+ const owner = conditionalOwner(schema, objectPath);
627
+ if (!owner) return model;
628
+ const from = otherCombinator(next);
629
+ const source = owner[from];
630
+ if (!Array.isArray(source)) return model;
631
+ const conditionals = source.filter((m) => !!m.if);
632
+ if (conditionals.length === 0) return model;
633
+ const rest = source.filter((m) => !m.if);
634
+ if (rest.length > 0) owner[from] = rest;
635
+ else delete owner[from];
636
+ owner[next] = [...owner[next] ?? [], ...conditionals];
637
+ return { schema, uiSchema: model.uiSchema };
638
+ }
175
639
  function deleteProperty(model, path) {
176
640
  if (path.length === 0) return model;
177
641
  const schema = clone(model.schema);
178
642
  const parentPath = path.slice(0, -1);
179
- const key = path[path.length - 1];
643
+ const key = segKey(path[path.length - 1]);
644
+ if (key === void 0) return model;
180
645
  const container = resolveContainer(schema, parentPath);
181
646
  if (!container || !(key in container.properties)) return model;
182
647
  delete container.properties[key];
@@ -206,32 +671,38 @@ function renameUiEntry(uiSchema, parentPath, prevKey, nextKey) {
206
671
  delete next[prevKey];
207
672
  return next;
208
673
  }
674
+ function updateContainerRefs(container, prevKey, nextKey) {
675
+ const rename = (k) => k === prevKey ? nextKey : k;
676
+ const order = container.getOrder();
677
+ if (order) container.setOrder(order.map(rename));
678
+ const required = container.getRequired();
679
+ if (required) container.setRequired(required.map(rename));
680
+ }
209
681
  function renameProperty(model, path, nextKey) {
210
682
  if (path.length === 0) return { model, path };
211
683
  const parentPath = path.slice(0, -1);
212
- const prevKey = path[path.length - 1];
213
- if (nextKey === prevKey || !nextKey) return { model, path };
684
+ const prevKey = segKey(path[path.length - 1]);
685
+ if (prevKey === void 0) return { model, path };
686
+ if (nextKey === prevKey || !nextKey || isReservedKey(nextKey))
687
+ return { model, path };
214
688
  const schema = clone(model.schema);
215
689
  const container = resolveContainer(schema, parentPath);
216
690
  if (!container || !(prevKey in container.properties)) return { model, path };
217
691
  if (nextKey in container.properties) return { model, path };
218
692
  renameKeyInPlace(container.properties, prevKey, nextKey);
219
- const rename = (k) => k === prevKey ? nextKey : k;
220
- const order = container.getOrder();
221
- if (order) container.setOrder(order.map(rename));
222
- const required = container.getRequired();
223
- if (required) container.setRequired(required.map(rename));
693
+ updateContainerRefs(container, prevKey, nextKey);
224
694
  const uiSchema = renameUiEntry(model.uiSchema, parentPath, prevKey, nextKey);
225
695
  return {
226
696
  model: { schema, uiSchema },
227
- path: [...parentPath, nextKey]
697
+ path: [...parentPath, prop(nextKey)]
228
698
  };
229
699
  }
230
700
  function moveProperty(model, path, direction) {
231
701
  if (path.length === 0) return model;
232
702
  const schema = clone(model.schema);
233
703
  const parentPath = path.slice(0, -1);
234
- const key = path[path.length - 1];
704
+ const key = segKey(path[path.length - 1]);
705
+ if (key === void 0) return model;
235
706
  const container = resolveContainer(schema, parentPath);
236
707
  if (!container) return model;
237
708
  const keys = Object.keys(container.properties);
@@ -249,7 +720,8 @@ function setRequired(model, path, required) {
249
720
  if (path.length === 0) return model;
250
721
  const schema = clone(model.schema);
251
722
  const parentPath = path.slice(0, -1);
252
- const key = path[path.length - 1];
723
+ const key = segKey(path[path.length - 1]);
724
+ if (key === void 0) return model;
253
725
  const container = resolveContainer(schema, parentPath);
254
726
  if (!container) return model;
255
727
  const current = new Set(container.getRequired() ?? []);
@@ -261,32 +733,159 @@ function setRequired(model, path, required) {
261
733
  function updateProperty(model, path, patch) {
262
734
  if (path.length === 0) return model;
263
735
  const schema = clone(model.schema);
264
- const prop = getProperty(schema, path);
265
- if (!prop) return model;
266
- Object.assign(prop, patch);
736
+ const prop2 = getProperty(schema, path);
737
+ if (!prop2) return model;
738
+ for (const [k, v] of Object.entries(patch)) {
739
+ if (v === void 0) delete prop2[k];
740
+ else prop2[k] = v;
741
+ }
742
+ return { schema, uiSchema: model.uiSchema };
743
+ }
744
+ function currentTypeChoice(prop2) {
745
+ return prop2.oneOf ? "oneOf" : prop2.type ?? "string";
746
+ }
747
+ function changePropertyType(model, path, next) {
748
+ if (path.length === 0 || !next) return model;
749
+ const schema = clone(model.schema);
750
+ const prop2 = getProperty(schema, path);
751
+ if (!prop2 || !prop2.$ref && currentTypeChoice(prop2) === next) return model;
752
+ delete prop2.properties;
753
+ delete prop2.items;
754
+ delete prop2.enum;
755
+ delete prop2.oneOf;
756
+ delete prop2.discriminator;
757
+ delete prop2.const;
758
+ delete prop2.$ref;
759
+ if (next !== "object") {
760
+ delete prop2.allOf;
761
+ delete prop2.anyOf;
762
+ }
763
+ delete prop2.minLength;
764
+ delete prop2.maxLength;
765
+ delete prop2.pattern;
766
+ delete prop2.format;
767
+ delete prop2.minimum;
768
+ delete prop2.maximum;
769
+ delete prop2.minItems;
770
+ delete prop2.maxItems;
771
+ if (next === "oneOf") {
772
+ const seed = newProperty("oneOf");
773
+ prop2.type = seed.type;
774
+ prop2.discriminator = seed.discriminator;
775
+ prop2.oneOf = seed.oneOf;
776
+ } else {
777
+ prop2.type = next;
778
+ if (next === "object") prop2.properties = {};
779
+ if (next === "array") prop2.items = { type: "string" };
780
+ }
267
781
  return { schema, uiSchema: model.uiSchema };
268
782
  }
269
- function changePropertyType(model, path, nextType) {
270
- if (path.length === 0 || !nextType) return model;
783
+ function defPointer(name) {
784
+ return `#/$defs/${name}`;
785
+ }
786
+ function setReference(model, path, name) {
787
+ if (path.length === 0 || !name) return model;
788
+ const schema = clone(model.schema);
789
+ const prop2 = getProperty(schema, path);
790
+ if (!prop2) return model;
791
+ const defs = schema.$defs ?? schema.definitions;
792
+ if (!defs || !(name in defs)) return model;
793
+ if (prop2.type === "array") {
794
+ prop2.items = { $ref: defPointer(name) };
795
+ return { schema, uiSchema: model.uiSchema };
796
+ }
797
+ makeReference(prop2, name);
798
+ return { schema, uiSchema: model.uiSchema };
799
+ }
800
+ function makeReference(prop2, name) {
801
+ delete prop2.properties;
802
+ delete prop2.items;
803
+ delete prop2.enum;
804
+ delete prop2.oneOf;
805
+ delete prop2.discriminator;
806
+ delete prop2.const;
807
+ delete prop2.allOf;
808
+ delete prop2.anyOf;
809
+ delete prop2.type;
810
+ delete prop2.minLength;
811
+ delete prop2.maxLength;
812
+ delete prop2.pattern;
813
+ delete prop2.format;
814
+ delete prop2.minimum;
815
+ delete prop2.maximum;
816
+ delete prop2.minItems;
817
+ delete prop2.maxItems;
818
+ prop2.$ref = defPointer(name);
819
+ }
820
+ function walkSchemaNodes(node, visitor, parentKey) {
821
+ if (Array.isArray(node)) {
822
+ for (const item of node) walkSchemaNodes(item, visitor, parentKey);
823
+ return;
824
+ }
825
+ if (!node || typeof node !== "object") return;
826
+ const obj = node;
827
+ visitor(obj, parentKey);
828
+ for (const key of Object.keys(obj)) {
829
+ walkSchemaNodes(obj[key], visitor, key);
830
+ }
831
+ }
832
+ function rewriteRefs(node, from, to) {
833
+ walkSchemaNodes(node, (obj, key) => {
834
+ if (key !== "$ref" && obj.$ref === from) obj.$ref = to;
835
+ });
836
+ }
837
+ function countRefUsages(schema, name) {
838
+ let count = 0;
839
+ const pointers = /* @__PURE__ */ new Set([`#/$defs/${name}`, `#/definitions/${name}`]);
840
+ walkSchemaNodes(schema, (obj) => {
841
+ if (typeof obj.$ref === "string" && pointers.has(obj.$ref)) count++;
842
+ });
843
+ return count;
844
+ }
845
+ function addDefinition(model, name) {
846
+ const schema = clone(model.schema);
847
+ if (!schema.$defs) schema.$defs = {};
848
+ const key = uniqueKey(name ?? "Definition", schema.$defs);
849
+ schema.$defs[key] = { type: "object", properties: {} };
850
+ return {
851
+ model: { schema, uiSchema: model.uiSchema },
852
+ path: [def(key)]
853
+ };
854
+ }
855
+ function renameDefinition(model, oldName, newName) {
856
+ const path = [def(oldName)];
857
+ if (!newName || newName === oldName || isReservedKey(newName))
858
+ return { model, path };
859
+ const schema = clone(model.schema);
860
+ const defs = schema.$defs ?? schema.definitions;
861
+ if (!defs || !(oldName in defs) || newName in defs) return { model, path };
862
+ renameKeyInPlace(defs, oldName, newName);
863
+ rewriteRefs(schema, `#/$defs/${oldName}`, `#/$defs/${newName}`);
864
+ rewriteRefs(schema, `#/definitions/${oldName}`, `#/$defs/${newName}`);
865
+ return {
866
+ model: { schema, uiSchema: model.uiSchema },
867
+ path: [def(newName)]
868
+ };
869
+ }
870
+ function removeDefinition(model, name) {
271
871
  const schema = clone(model.schema);
272
- const prop = getProperty(schema, path);
273
- if (!prop || prop.type === nextType) return model;
274
- delete prop.properties;
275
- delete prop.items;
276
- delete prop.enum;
277
- delete prop.minLength;
278
- delete prop.maxLength;
279
- delete prop.pattern;
280
- delete prop.format;
281
- delete prop.minimum;
282
- delete prop.maximum;
283
- delete prop.minItems;
284
- delete prop.maxItems;
285
- prop.type = nextType;
286
- if (nextType === "object") prop.properties = {};
287
- if (nextType === "array") prop.items = { type: "string" };
872
+ const inDefs = schema.$defs && name in schema.$defs;
873
+ const inDefs7 = schema.definitions && name in schema.definitions;
874
+ if (!inDefs && !inDefs7) return model;
875
+ if (countRefUsages(schema, name) > 0) return model;
876
+ if (inDefs) {
877
+ delete schema.$defs[name];
878
+ if (Object.keys(schema.$defs).length === 0) delete schema.$defs;
879
+ } else {
880
+ delete schema.definitions[name];
881
+ if (Object.keys(schema.definitions).length === 0)
882
+ delete schema.definitions;
883
+ }
288
884
  return { schema, uiSchema: model.uiSchema };
289
885
  }
886
+ function definitionUsageCount(schema, name) {
887
+ return countRefUsages(schema, name);
888
+ }
290
889
  function normalizeChoices(choices) {
291
890
  const seen = /* @__PURE__ */ new Set();
292
891
  const clean = [];
@@ -314,22 +913,22 @@ function withEnumNames(entry, choices) {
314
913
  function setEnum(model, path, choices) {
315
914
  if (path.length === 0) return model;
316
915
  const schema = clone(model.schema);
317
- const prop = getProperty(schema, path);
318
- if (!prop) return model;
916
+ const prop2 = getProperty(schema, path);
917
+ if (!prop2) return model;
319
918
  const clean = normalizeChoices(choices);
320
- if (clean.length === 0) delete prop.enum;
321
- else prop.enum = clean.map((c) => c.value);
919
+ if (clean.length === 0) delete prop2.enum;
920
+ else prop2.enum = clean.map((c) => c.value);
322
921
  const nextEntry = withEnumNames(
323
922
  getUiEntryAtPath(model.uiSchema, path),
324
923
  clean
325
924
  );
326
- const uiSchema = setEntryIn(model.uiSchema, path, nextEntry);
925
+ const uiSchema = setEntryIn(model.uiSchema, propKeys(path), nextEntry);
327
926
  return { schema, uiSchema };
328
927
  }
329
- function readEnumChoices(prop, uiEntry) {
330
- if (!Array.isArray(prop.enum)) return [];
928
+ function readEnumChoices(prop2, uiEntry) {
929
+ if (!Array.isArray(prop2.enum)) return [];
331
930
  const enumNames = uiEntry?.["x-ui-options"]?.enumNames ?? {};
332
- return prop.enum.map((v) => {
931
+ return prop2.enum.map((v) => {
333
932
  const value = String(v);
334
933
  return { value, label: enumNames[value] };
335
934
  });
@@ -337,40 +936,41 @@ function readEnumChoices(prop, uiEntry) {
337
936
  function setTableHints(model, path, hints) {
338
937
  if (path.length === 0) return model;
339
938
  const schema = clone(model.schema);
340
- const prop = getProperty(schema, path);
341
- if (!prop) return model;
342
- if (hints.cell) prop["x-ui-cell"] = hints.cell;
343
- else delete prop["x-ui-cell"];
939
+ const prop2 = getProperty(schema, path);
940
+ if (!prop2) return model;
941
+ if (hints.cell) prop2["x-ui-cell"] = hints.cell;
942
+ else delete prop2["x-ui-cell"];
344
943
  if (hints.cellOptions && Object.keys(hints.cellOptions).length > 0) {
345
- prop["x-ui-cell-options"] = hints.cellOptions;
944
+ prop2["x-ui-cell-options"] = hints.cellOptions;
346
945
  } else {
347
- delete prop["x-ui-cell-options"];
946
+ delete prop2["x-ui-cell-options"];
348
947
  }
349
948
  if (hints.column && Object.keys(hints.column).length > 0) {
350
- prop["x-ui-column"] = hints.column;
949
+ prop2["x-ui-column"] = hints.column;
351
950
  } else {
352
- delete prop["x-ui-column"];
951
+ delete prop2["x-ui-column"];
353
952
  }
354
953
  return { schema, uiSchema: model.uiSchema };
355
954
  }
356
- function readTableHints(prop) {
955
+ function readTableHints(prop2) {
357
956
  return {
358
- cell: typeof prop["x-ui-cell"] === "string" ? prop["x-ui-cell"] : void 0,
359
- cellOptions: prop["x-ui-cell-options"],
360
- column: prop["x-ui-column"]
957
+ cell: typeof prop2["x-ui-cell"] === "string" ? prop2["x-ui-cell"] : void 0,
958
+ cellOptions: prop2["x-ui-cell-options"],
959
+ column: prop2["x-ui-column"]
361
960
  };
362
961
  }
363
962
  function getUiEntryAtPath(uiSchema, path) {
364
- if (path.length === 0) return void 0;
365
- let entry = uiSchema[path[0]];
366
- for (let i = 1; i < path.length && entry; i++) {
963
+ const keys = propKeys(path);
964
+ if (keys.length === 0) return void 0;
965
+ let entry = uiSchema[keys[0]];
966
+ for (let i = 1; i < keys.length && entry; i++) {
367
967
  const fields = entry["x-ui-fields"];
368
- entry = fields?.[path[i]];
968
+ entry = fields?.[keys[i]];
369
969
  }
370
970
  return entry;
371
971
  }
372
- function setEntryIn(map, path, entry) {
373
- const [head, ...rest] = path;
972
+ function setEntryIn(map, keys, entry) {
973
+ const [head, ...rest] = keys;
374
974
  const next = { ...map };
375
975
  if (rest.length === 0) {
376
976
  if (entry && Object.keys(entry).length > 0) next[head] = entry;
@@ -391,15 +991,15 @@ function setEntryIn(map, path, entry) {
391
991
  }
392
992
  function setUiEntry(model, path, entry) {
393
993
  if (path.length === 0) return model;
394
- const uiSchema = setEntryIn(model.uiSchema, path, entry);
994
+ const uiSchema = setEntryIn(model.uiSchema, propKeys(path), entry);
395
995
  return { schema: model.schema, uiSchema };
396
996
  }
397
997
  function setPropertyLayout(model, path, layout) {
398
998
  if (path.length === 0) return model;
399
999
  const schema = clone(model.schema);
400
- const prop = getProperty(schema, path);
401
- if (!prop) return model;
402
- const target = prop.type === "array" ? prop.items : prop;
1000
+ const prop2 = getProperty(schema, path);
1001
+ if (!prop2) return model;
1002
+ const target = prop2.type === "array" ? prop2.items : prop2;
403
1003
  if (!target) return model;
404
1004
  if (layout) target["x-ui-layout"] = layout;
405
1005
  else delete target["x-ui-layout"];
@@ -423,9 +1023,9 @@ function setRootLayoutRows(model, rows) {
423
1023
  function setScopedLayoutRows(model, scopePath, rows) {
424
1024
  if (scopePath.length === 0) return setRootLayoutRows(model, rows);
425
1025
  const schema = clone(model.schema);
426
- const prop = getProperty(schema, scopePath);
427
- if (!prop) return model;
428
- const target = prop.type === "array" ? prop.items : prop;
1026
+ const prop2 = getProperty(schema, scopePath);
1027
+ if (!prop2) return model;
1028
+ const target = prop2.type === "array" ? prop2.items : prop2;
429
1029
  if (!target) return model;
430
1030
  const layout = { ...target["x-ui-layout"] ?? {} };
431
1031
  if (rows && rows.length > 0) layout.rows = rows;
@@ -438,8 +1038,8 @@ function getScopedLayout(schema, uiSchema, scopePath) {
438
1038
  if (scopePath.length === 0) {
439
1039
  return uiSchema["x-ui-layout"];
440
1040
  }
441
- const prop = getProperty(schema, scopePath);
442
- const target = prop?.type === "array" ? prop.items : prop;
1041
+ const prop2 = getProperty(schema, scopePath);
1042
+ const target = prop2?.type === "array" ? prop2.items : prop2;
443
1043
  return target?.["x-ui-layout"];
444
1044
  }
445
1045
  function setScopedLayoutOptions(model, scopePath, options) {
@@ -455,6 +1055,120 @@ function setScopedLayoutOptions(model, scopePath, options) {
455
1055
  if (scopePath.length === 0) return setRootLayout(model, layout);
456
1056
  return setPropertyLayout(model, scopePath, layout);
457
1057
  }
1058
+ function setScopedLayoutGroups(model, scopePath, groups) {
1059
+ const current = getScopedLayout(model.schema, model.uiSchema, scopePath) ?? {};
1060
+ const merged = { ...current };
1061
+ if (groups && groups.length > 0) {
1062
+ merged.groups = groups;
1063
+ delete merged.rows;
1064
+ } else {
1065
+ delete merged.groups;
1066
+ }
1067
+ const layout = Object.keys(merged).length > 0 ? merged : void 0;
1068
+ if (scopePath.length === 0) return setRootLayout(model, layout);
1069
+ return setPropertyLayout(model, scopePath, layout);
1070
+ }
1071
+ function groupsOf(model, scopePath) {
1072
+ return getScopedLayout(model.schema, model.uiSchema, scopePath)?.groups ?? [];
1073
+ }
1074
+ function addLayoutGroup(model, scopePath, label) {
1075
+ const groups = groupsOf(model, scopePath);
1076
+ const next = { label: label ?? `Group ${groups.length + 1}`, rows: [] };
1077
+ return setScopedLayoutGroups(model, scopePath, [...groups, next]);
1078
+ }
1079
+ function renameLayoutGroup(model, scopePath, groupIndex, label) {
1080
+ const groups = groupsOf(model, scopePath);
1081
+ if (!groups[groupIndex] || !label.trim()) return model;
1082
+ return setScopedLayoutGroups(
1083
+ model,
1084
+ scopePath,
1085
+ groups.map((group, i) => i === groupIndex ? { ...group, label } : group)
1086
+ );
1087
+ }
1088
+ function placeFieldInGroup(model, scopePath, key, target) {
1089
+ const groups = groupsOf(model, scopePath);
1090
+ if (!key || groups.length === 0) return model;
1091
+ const stripped = groups.map((group) => ({
1092
+ ...group,
1093
+ rows: group.rows.map((row2) => ({
1094
+ ...row2,
1095
+ fields: row2.fields.filter((f) => f !== key)
1096
+ }))
1097
+ }));
1098
+ if (!target) return setScopedLayoutGroups(model, scopePath, stripped);
1099
+ const { groupIndex, rowIndex } = target;
1100
+ const row = stripped[groupIndex]?.rows[rowIndex];
1101
+ if (!row) return setScopedLayoutGroups(model, scopePath, stripped);
1102
+ stripped[groupIndex].rows = stripped[groupIndex].rows.map(
1103
+ (r, i) => i === rowIndex ? { ...r, fields: [...r.fields, key] } : r
1104
+ );
1105
+ return setScopedLayoutGroups(model, scopePath, stripped);
1106
+ }
1107
+ function moveLayoutGroup(model, scopePath, groupIndex, direction) {
1108
+ const groups = groupsOf(model, scopePath);
1109
+ const target = groupIndex + direction;
1110
+ if (!groups[groupIndex] || target < 0 || target >= groups.length)
1111
+ return model;
1112
+ const next = [...groups];
1113
+ [next[groupIndex], next[target]] = [next[target], next[groupIndex]];
1114
+ return setScopedLayoutGroups(model, scopePath, next);
1115
+ }
1116
+ function setLayoutGroupRule(model, scopePath, groupIndex, rule) {
1117
+ const groups = groupsOf(model, scopePath);
1118
+ if (!groups[groupIndex]) return model;
1119
+ const next = groups.map((group, i) => {
1120
+ if (i !== groupIndex) return group;
1121
+ const { rule: _drop, ...rest } = group;
1122
+ return rule ? { ...rest, rule } : rest;
1123
+ });
1124
+ return setScopedLayoutGroups(model, scopePath, next);
1125
+ }
1126
+ function setScopedLayoutVariant(model, scopePath, variant) {
1127
+ const current = getScopedLayout(model.schema, model.uiSchema, scopePath) ?? {};
1128
+ const merged = { ...current };
1129
+ if (variant && variant !== "groups") merged.variant = variant;
1130
+ else delete merged.variant;
1131
+ const layout = Object.keys(merged).length > 0 ? merged : void 0;
1132
+ if (scopePath.length === 0) return setRootLayout(model, layout);
1133
+ return setPropertyLayout(model, scopePath, layout);
1134
+ }
1135
+ function removeLayoutGroup(model, scopePath, groupIndex) {
1136
+ const groups = groupsOf(model, scopePath);
1137
+ if (!groups[groupIndex]) return model;
1138
+ const remaining = groups.filter((_, i) => i !== groupIndex);
1139
+ if (remaining.length > 0) {
1140
+ return setScopedLayoutGroups(model, scopePath, remaining);
1141
+ }
1142
+ const cleared = setScopedLayoutGroups(model, scopePath, []);
1143
+ return setScopedLayoutRows(cleared, scopePath, groups[groupIndex].rows);
1144
+ }
1145
+ function setScopedGroupRows(model, scopePath, groupIndex, rows) {
1146
+ const groups = getScopedLayout(
1147
+ model.schema,
1148
+ model.uiSchema,
1149
+ scopePath
1150
+ )?.groups;
1151
+ if (!groups?.[groupIndex]) return model;
1152
+ const next = groups.map(
1153
+ (group, i) => i === groupIndex ? { ...group, rows } : group
1154
+ );
1155
+ return setScopedLayoutGroups(model, scopePath, next);
1156
+ }
1157
+ function setScopedLayoutMode(model, scopePath, mode) {
1158
+ const layout = getScopedLayout(model.schema, model.uiSchema, scopePath) ?? {};
1159
+ const groups = layout.groups ?? [];
1160
+ const rows = layout.rows ?? [];
1161
+ if (mode === "groups") {
1162
+ if (groups.length > 0) return model;
1163
+ return setScopedLayoutGroups(model, scopePath, [
1164
+ { label: "Group 1", rows }
1165
+ ]);
1166
+ }
1167
+ if (groups.length === 0) return model;
1168
+ const flattened = [...rows, ...groups.flatMap((group) => group.rows)];
1169
+ const cleared = setScopedLayoutGroups(model, scopePath, []);
1170
+ return setScopedLayoutRows(cleared, scopePath, flattened);
1171
+ }
458
1172
  function setGlobalOptions(model, options) {
459
1173
  const uiSchema = { ...model.uiSchema };
460
1174
  if (options) uiSchema["x-ui-globalOptions"] = options;
@@ -468,6 +1182,9 @@ var STRING_WIDGETS = [
468
1182
  { value: "text", label: "Text input" },
469
1183
  { value: "textarea", label: "Textarea" },
470
1184
  { value: "select", label: "Select" },
1185
+ { value: "date", label: "Date" },
1186
+ { value: "date-time", label: "Date & time" },
1187
+ { value: "time", label: "Time" },
471
1188
  { value: "tags", label: "Tags" },
472
1189
  { value: "entity-select", label: "Entity select" },
473
1190
  { value: "json-editor", label: "JSON editor" }
@@ -489,8 +1206,15 @@ var ARRAY_WIDGETS = [
489
1206
  { value: "select", label: "Multi-select" },
490
1207
  { value: "tags", label: "Tags" }
491
1208
  ];
492
- function compatibleWidgets(prop) {
493
- switch (prop.type) {
1209
+ var NULL_WIDGETS = [{ value: "display", label: "Display" }];
1210
+ var ONEOF_WIDGETS = [
1211
+ { value: "one-of", label: "One of (variant)" }
1212
+ ];
1213
+ function compatibleWidgets(prop2) {
1214
+ if (Array.isArray(prop2.oneOf) && prop2.oneOf.length > 0) {
1215
+ return [AUTO, ...ONEOF_WIDGETS];
1216
+ }
1217
+ switch (prop2.type) {
494
1218
  case "string":
495
1219
  return [AUTO, ...STRING_WIDGETS];
496
1220
  case "number":
@@ -502,6 +1226,8 @@ function compatibleWidgets(prop) {
502
1226
  return [AUTO, ...OBJECT_WIDGETS];
503
1227
  case "array":
504
1228
  return [AUTO, ...ARRAY_WIDGETS];
1229
+ case "null":
1230
+ return [AUTO, ...NULL_WIDGETS];
505
1231
  default:
506
1232
  return [AUTO];
507
1233
  }
@@ -518,11 +1244,11 @@ var EMBEDDABLE_KEYS = [
518
1244
  function embedUiSchema(schema, uiSchema) {
519
1245
  const next = structuredClone(schema);
520
1246
  for (const [fieldName, entry] of Object.entries(uiSchema)) {
521
- const prop = next.properties[fieldName];
522
- if (!prop || !entry) continue;
1247
+ const prop2 = next.properties[fieldName];
1248
+ if (!prop2 || !entry) continue;
523
1249
  for (const key of EMBEDDABLE_KEYS) {
524
1250
  if (entry[key] !== void 0) {
525
- prop[key] = entry[key];
1251
+ prop2[key] = entry[key];
526
1252
  }
527
1253
  }
528
1254
  }
@@ -586,7 +1312,7 @@ function validateCapabilities(input) {
586
1312
  }
587
1313
 
588
1314
  // src/model/scoping.ts
589
- function escapeToken(token) {
1315
+ function escapeToken2(token) {
590
1316
  return token.replace(/~/g, "~0").replace(/\//g, "~1");
591
1317
  }
592
1318
  function parentContainerPointer(schema, path) {
@@ -595,20 +1321,27 @@ function parentContainerPointer(schema, path) {
595
1321
  let pointer = "/properties";
596
1322
  for (let i = 0; i < path.length; i++) {
597
1323
  if (!container) return void 0;
598
- const seg = path[i];
599
- const prop = container[seg];
600
- if (!prop) return void 0;
1324
+ const segment = path[i];
1325
+ if (segment.kind !== "prop") return void 0;
1326
+ const key = segment.key;
1327
+ const prop2 = container[key];
1328
+ if (!prop2) return void 0;
601
1329
  if (i === path.length - 1) return pointer;
602
- const next = childContainer(prop);
1330
+ const next = childContainer(prop2);
603
1331
  if (!next) return void 0;
604
- pointer = prop.type === "array" ? `${pointer}/${escapeToken(seg)}/items/properties` : `${pointer}/${escapeToken(seg)}/properties`;
1332
+ pointer = prop2.type === "array" ? `${pointer}/${escapeToken2(key)}/items/properties` : `${pointer}/${escapeToken2(key)}/properties`;
605
1333
  container = next;
606
1334
  }
607
1335
  return pointer;
608
1336
  }
609
1337
  function isPropertyEditable(schema, path, editablePaths) {
610
1338
  if (editablePaths.length === 0) return true;
611
- const parent = parentContainerPointer(schema, path);
1339
+ const allPropPrefix = path.slice(
1340
+ 0,
1341
+ path.findIndex((s) => s.kind !== "prop") + 1 || path.length
1342
+ );
1343
+ const effectivePath = allPropPrefix.length < path.length ? allPropPrefix : path;
1344
+ const parent = parentContainerPointer(schema, effectivePath);
612
1345
  if (parent === void 0) return false;
613
1346
  return editablePaths.includes(parent);
614
1347
  }
@@ -623,18 +1356,89 @@ function ownChildContainerPointer(schema, path) {
623
1356
  if (parent === void 0) return void 0;
624
1357
  let container = schema.properties;
625
1358
  for (let i = 0; i < path.length; i++) {
626
- const prop = container?.[path[i]];
627
- if (!prop) return void 0;
1359
+ const segment = path[i];
1360
+ if (segment.kind !== "prop") return void 0;
1361
+ const prop2 = container?.[segment.key];
1362
+ if (!prop2) return void 0;
628
1363
  if (i === path.length - 1) {
629
- const seg = escapeToken(path[i]);
630
- return prop.type === "array" ? `${parent}/${seg}/items/properties` : `${parent}/${seg}/properties`;
1364
+ const seg = escapeToken2(segment.key);
1365
+ return prop2.type === "array" ? `${parent}/${seg}/items/properties` : `${parent}/${seg}/properties`;
631
1366
  }
632
- container = childContainer(prop);
1367
+ container = childContainer(prop2);
633
1368
  }
634
1369
  return void 0;
635
1370
  }
636
1371
 
637
1372
  // src/hooks/useDesignerState.ts
1373
+ function pathStartsWith(p, prefix) {
1374
+ if (p.length < prefix.length) return false;
1375
+ return prefix.every((seg, i) => sameSegment(seg, p[i]));
1376
+ }
1377
+ function sharesParentPrefix(p, removed) {
1378
+ if (p.length < removed.length) return false;
1379
+ const parentLen = removed.length - 1;
1380
+ for (let i = 0; i < parentLen; i++) {
1381
+ if (!sameSegment(p[i], removed[i])) return false;
1382
+ }
1383
+ return true;
1384
+ }
1385
+ function sameIndexedArray(a, b) {
1386
+ if (a.kind !== b.kind) return false;
1387
+ if (a.kind === "branch" && b.kind === "branch") return true;
1388
+ if (a.kind === "condition" && b.kind === "condition")
1389
+ return a.combinator === b.combinator;
1390
+ return false;
1391
+ }
1392
+ function affectedSibling(prev, removed, kind) {
1393
+ const last = removed[removed.length - 1];
1394
+ if (!last || last.kind !== kind) return null;
1395
+ if (!sharesParentPrefix(prev, removed)) return null;
1396
+ const at = removed.length - 1;
1397
+ const seg = prev[at];
1398
+ if (!seg || !sameIndexedArray(seg, last) || !("index" in seg)) return null;
1399
+ return { at, seg };
1400
+ }
1401
+ function adjustIndexedSelection(prev, removed, kind) {
1402
+ if (!prev) return null;
1403
+ const hit = affectedSibling(prev, removed, kind);
1404
+ if (!hit) return prev;
1405
+ const removedIndex = removed[removed.length - 1].index;
1406
+ if (hit.seg.index === removedIndex) return null;
1407
+ if (hit.seg.index < removedIndex) return prev;
1408
+ const shifted = { ...hit.seg, index: hit.seg.index - 1 };
1409
+ return [...prev.slice(0, hit.at), shifted, ...prev.slice(hit.at + 1)];
1410
+ }
1411
+ function adjustBranchSelection(prev, removed) {
1412
+ return adjustIndexedSelection(prev, removed, "branch");
1413
+ }
1414
+ function countConditionals(arr) {
1415
+ return Array.isArray(arr) ? arr.filter((m) => !!m.if).length : 0;
1416
+ }
1417
+ function selectedConditionInCombinator(prev, objectPath, from) {
1418
+ if (!prev) return null;
1419
+ const at = objectPath.length;
1420
+ const seg = prev[at];
1421
+ if (!seg || seg.kind !== "condition" || seg.combinator !== from) return null;
1422
+ if (!samePath(prev.slice(0, at), objectPath)) return null;
1423
+ return { at, index: seg.index };
1424
+ }
1425
+ function remapConditionSelection(before, prev, objectPath, next) {
1426
+ const from = next === "allOf" ? "anyOf" : "allOf";
1427
+ const hit = selectedConditionInCombinator(prev, objectPath, from);
1428
+ if (!hit || !prev) return prev;
1429
+ const owner = objectPath.length === 0 ? { allOf: before.schema.allOf, anyOf: before.schema.anyOf } : getProperty(before.schema, objectPath);
1430
+ const sourceArr = owner?.[from];
1431
+ if (!Array.isArray(sourceArr)) return prev;
1432
+ const posAmongSource = countConditionals(sourceArr.slice(0, hit.index));
1433
+ const targetOffset = countConditionals(
1434
+ owner?.[next]
1435
+ );
1436
+ return [
1437
+ ...prev.slice(0, hit.at),
1438
+ condition(targetOffset + posAmongSource, next),
1439
+ ...prev.slice(hit.at + 1)
1440
+ ];
1441
+ }
638
1442
  var EMPTY_SCHEMA = {
639
1443
  title: "",
640
1444
  type: "object",
@@ -695,6 +1499,56 @@ function useDesignerState(args) {
695
1499
  commit(res.model);
696
1500
  setSelection(res.path);
697
1501
  },
1502
+ addBranch(path) {
1503
+ if (!canEditProperty(path)) return;
1504
+ const res = addBranch(modelRef.current, path);
1505
+ commit(res.model);
1506
+ setSelection(res.path);
1507
+ },
1508
+ removeBranch(path) {
1509
+ if (!canEditProperty(path)) return;
1510
+ commit(removeBranch(modelRef.current, path));
1511
+ setSelection((prev) => adjustBranchSelection(prev, path));
1512
+ },
1513
+ moveBranch(path, direction) {
1514
+ if (!canEditProperty(path)) return;
1515
+ commit(moveBranch(modelRef.current, path, direction));
1516
+ },
1517
+ addConditional(objectPath, combinator = "allOf") {
1518
+ if (!canAddChild(objectPath)) return;
1519
+ const res = addConditional(modelRef.current, objectPath, combinator);
1520
+ commit(res.model);
1521
+ setSelection(res.path);
1522
+ },
1523
+ setConditionalBehavior(objectPath, combinator) {
1524
+ if (!canAddChild(objectPath)) return;
1525
+ const before = modelRef.current;
1526
+ commit(setConditionalBehavior(before, objectPath, combinator));
1527
+ setSelection(
1528
+ (prev) => remapConditionSelection(before, prev, objectPath, combinator)
1529
+ );
1530
+ },
1531
+ removeConditional(path) {
1532
+ if (!canAddChild(path.slice(0, -1))) return;
1533
+ commit(removeConditional(modelRef.current, path));
1534
+ setSelection((prev) => adjustIndexedSelection(prev, path, "condition"));
1535
+ },
1536
+ setCondition(path, ifSchema) {
1537
+ if (!canAddChild(path.slice(0, -1))) return;
1538
+ commit(setCondition(modelRef.current, path, ifSchema));
1539
+ },
1540
+ addElse(path) {
1541
+ if (!canAddChild(path.slice(0, -1))) return;
1542
+ commit(addElse(modelRef.current, path));
1543
+ },
1544
+ removeElse(path) {
1545
+ if (!canAddChild(path.slice(0, -1))) return;
1546
+ commit(removeElse(modelRef.current, path));
1547
+ const elsePath = [...path, slot("else")];
1548
+ setSelection(
1549
+ (prev) => prev && pathStartsWith(prev, elsePath) ? null : prev
1550
+ );
1551
+ },
698
1552
  deleteProperty(path) {
699
1553
  if (!canEditProperty(path)) return;
700
1554
  commit(deleteProperty(modelRef.current, path));
@@ -718,9 +1572,48 @@ function useDesignerState(args) {
718
1572
  if (!canEditProperty(path)) return;
719
1573
  commit(updateProperty(modelRef.current, path, patch));
720
1574
  },
721
- changePropertyType(path, nextType) {
1575
+ changePropertyType(path, next) {
722
1576
  if (!canEditProperty(path)) return;
723
- commit(changePropertyType(modelRef.current, path, nextType));
1577
+ commit(changePropertyType(modelRef.current, path, next));
1578
+ },
1579
+ setReference(path, name) {
1580
+ if (!canEditProperty(path)) return;
1581
+ commit(setReference(modelRef.current, path, name));
1582
+ },
1583
+ // Definitions are document-level, not path-scoped: gate on lock only.
1584
+ addDefinition(name) {
1585
+ if (capabilities.locked) return;
1586
+ const res = addDefinition(modelRef.current, name);
1587
+ commit(res.model);
1588
+ setSelection(res.path);
1589
+ },
1590
+ renameDefinition(oldName, newName) {
1591
+ if (capabilities.locked) return;
1592
+ const res = renameDefinition(modelRef.current, oldName, newName);
1593
+ commit(res.model);
1594
+ setSelection(
1595
+ (prev) => prev && samePath(prev, [def(oldName)]) ? res.path : prev
1596
+ );
1597
+ },
1598
+ removeDefinition(name) {
1599
+ if (capabilities.locked) return;
1600
+ commit(removeDefinition(modelRef.current, name));
1601
+ setSelection(
1602
+ (prev) => prev && pathStartsWith(prev, [def(name)]) ? null : prev
1603
+ );
1604
+ },
1605
+ // Replace the whole JSON Schema (from the editable JSON Schema tab).
1606
+ // The uiSchema is preserved. Gated by capabilities.locked only -- a
1607
+ // wholesale replace is not subject to per-path scoping.
1608
+ replaceSchema(schema) {
1609
+ if (capabilities.locked) return;
1610
+ commit({ schema, uiSchema: modelRef.current.uiSchema });
1611
+ setSelection(null);
1612
+ },
1613
+ // Replace the whole UI Schema map (from the editable UI Schema tab).
1614
+ replaceUiSchema(uiSchema) {
1615
+ if (readOnly) return;
1616
+ commit({ schema: modelRef.current.schema, uiSchema });
724
1617
  },
725
1618
  setEnum(path, choices) {
726
1619
  if (!canEditProperty(path)) return;
@@ -757,18 +1650,67 @@ function useDesignerState(args) {
757
1650
  if (readOnly) return;
758
1651
  commit(setScopedLayoutOptions(modelRef.current, scopePath, options));
759
1652
  },
1653
+ setScopedLayoutGroups(scopePath, groups) {
1654
+ if (readOnly) return;
1655
+ commit(setScopedLayoutGroups(modelRef.current, scopePath, groups));
1656
+ },
1657
+ setScopedGroupRows(scopePath, groupIndex, rows) {
1658
+ if (readOnly) return;
1659
+ commit(
1660
+ setScopedGroupRows(modelRef.current, scopePath, groupIndex, rows)
1661
+ );
1662
+ },
1663
+ setScopedLayoutMode(scopePath, mode) {
1664
+ if (readOnly) return;
1665
+ commit(setScopedLayoutMode(modelRef.current, scopePath, mode));
1666
+ },
1667
+ addLayoutGroup(scopePath, label) {
1668
+ if (readOnly) return;
1669
+ commit(addLayoutGroup(modelRef.current, scopePath, label));
1670
+ },
1671
+ renameLayoutGroup(scopePath, groupIndex, label) {
1672
+ if (readOnly) return;
1673
+ commit(
1674
+ renameLayoutGroup(modelRef.current, scopePath, groupIndex, label)
1675
+ );
1676
+ },
1677
+ removeLayoutGroup(scopePath, groupIndex) {
1678
+ if (readOnly) return;
1679
+ commit(removeLayoutGroup(modelRef.current, scopePath, groupIndex));
1680
+ },
1681
+ moveLayoutGroup(scopePath, groupIndex, direction) {
1682
+ if (readOnly) return;
1683
+ commit(
1684
+ moveLayoutGroup(modelRef.current, scopePath, groupIndex, direction)
1685
+ );
1686
+ },
1687
+ setLayoutGroupRule(scopePath, groupIndex, rule) {
1688
+ if (readOnly) return;
1689
+ commit(
1690
+ setLayoutGroupRule(modelRef.current, scopePath, groupIndex, rule)
1691
+ );
1692
+ },
1693
+ setScopedLayoutVariant(scopePath, variant) {
1694
+ if (readOnly) return;
1695
+ commit(setScopedLayoutVariant(modelRef.current, scopePath, variant));
1696
+ },
1697
+ placeFieldInGroup(scopePath, key, target) {
1698
+ if (readOnly) return;
1699
+ commit(placeFieldInGroup(modelRef.current, scopePath, key, target));
1700
+ },
760
1701
  setGlobalOptions(options) {
761
1702
  if (readOnly) return;
762
1703
  commit(setGlobalOptions(modelRef.current, options));
763
1704
  }
764
1705
  }),
765
- [commit, readOnly, canEditProperty, canAddChild]
1706
+ [commit, readOnly, capabilities.locked, canEditProperty, canAddChild]
766
1707
  );
767
1708
  return {
768
1709
  schema: model.schema,
769
1710
  uiSchema: model.uiSchema,
770
1711
  selection,
771
1712
  readOnly,
1713
+ refResolver: args.refResolver,
772
1714
  capabilities,
773
1715
  canEditProperty,
774
1716
  canAddChild,
@@ -778,7 +1720,7 @@ function useDesignerState(args) {
778
1720
  }
779
1721
 
780
1722
  // src/SchemaDesigner.tsx
781
- import { useEffect as useEffect5, useState as useState8 } from "react";
1723
+ import { useEffect as useEffect6, useState as useState9 } from "react";
782
1724
  import { ResizeHandle, IconButton as IconButton5 } from "@petrarca/sonnet-ui";
783
1725
  import { useResizablePanel } from "@petrarca/sonnet-core/hooks";
784
1726
  import {
@@ -951,7 +1893,7 @@ function FormSettingsSheet({
951
1893
  }
952
1894
 
953
1895
  // src/tree/PropertyTree.tsx
954
- import { useEffect, useRef as useRef2, useState as useState2 } from "react";
1896
+ import { useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
955
1897
  import { cn } from "@petrarca/sonnet-core";
956
1898
  import { IconButton, Input } from "@petrarca/sonnet-ui";
957
1899
  import {
@@ -960,30 +1902,90 @@ import {
960
1902
  ChevronUp,
961
1903
  ChevronDown,
962
1904
  PanelLeftClose,
963
- Lock
1905
+ Lock,
1906
+ GitBranch,
1907
+ SplitSquareHorizontal,
1908
+ Eye,
1909
+ Braces,
1910
+ Brackets,
1911
+ GitFork,
1912
+ CornerDownRight,
1913
+ Type,
1914
+ List,
1915
+ ToggleLeft,
1916
+ Hash,
1917
+ Link2,
1918
+ Puzzle,
1919
+ ChevronRight
964
1920
  } from "lucide-react";
965
1921
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1922
+ function buildInUseDefs(schema, names) {
1923
+ return new Set(names.filter((n) => definitionUsageCount(schema, n) > 0));
1924
+ }
966
1925
  function PropertyTree({
967
1926
  state,
968
1927
  onCollapse
969
1928
  }) {
970
- const nodes = buildPropertyTree(state.schema);
971
- return /* @__PURE__ */ jsxs2("div", { children: [
972
- /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center gap-2", children: [
973
- /* @__PURE__ */ jsx2("span", { className: "heading-meta", children: "Properties" }),
1929
+ const nodes = useMemo2(
1930
+ () => buildPropertyTree(state.schema, state.refResolver),
1931
+ [state.schema, state.refResolver]
1932
+ );
1933
+ const propertyNodes = useMemo2(
1934
+ () => nodes.filter((n) => n.kind !== "definition"),
1935
+ [nodes]
1936
+ );
1937
+ const definitionNodes = useMemo2(
1938
+ () => nodes.filter((n) => n.kind === "definition"),
1939
+ [nodes]
1940
+ );
1941
+ const canEditDefs = !state.capabilities.locked;
1942
+ const [propsOpen, setPropsOpen] = useState2(true);
1943
+ return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col min-h-0", children: [
1944
+ /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center gap-2 shrink-0", children: [
1945
+ /* @__PURE__ */ jsxs2(
1946
+ "button",
1947
+ {
1948
+ type: "button",
1949
+ className: "flex items-center gap-1 heading-meta",
1950
+ onClick: () => setPropsOpen((v) => !v),
1951
+ children: [
1952
+ propsOpen ? /* @__PURE__ */ jsx2(ChevronDown, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx2(ChevronRight, { className: "h-3 w-3" }),
1953
+ "Properties",
1954
+ propertyNodes.length > 0 && /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground/60", children: [
1955
+ "(",
1956
+ propertyNodes.length,
1957
+ ")"
1958
+ ] })
1959
+ ]
1960
+ }
1961
+ ),
974
1962
  /* @__PURE__ */ jsxs2("span", { className: "ml-auto flex gap-1", children: [
975
1963
  state.canAddChild([]) && /* @__PURE__ */ jsx2(
976
1964
  IconButton,
977
1965
  {
978
1966
  label: "Add property",
979
- onClick: () => state.addProperty([], "scalar"),
1967
+ onClick: () => {
1968
+ setPropsOpen(true);
1969
+ state.addProperty([], "scalar");
1970
+ },
980
1971
  children: /* @__PURE__ */ jsx2(Plus, {})
981
1972
  }
982
1973
  ),
1974
+ state.canAddChild([]) && /* @__PURE__ */ jsx2(
1975
+ IconButton,
1976
+ {
1977
+ label: "Add condition",
1978
+ onClick: () => {
1979
+ setPropsOpen(true);
1980
+ state.addConditional([]);
1981
+ },
1982
+ children: /* @__PURE__ */ jsx2(GitBranch, {})
1983
+ }
1984
+ ),
983
1985
  onCollapse && /* @__PURE__ */ jsx2(IconButton, { label: "Collapse properties", onClick: onCollapse, children: /* @__PURE__ */ jsx2(PanelLeftClose, {}) })
984
1986
  ] })
985
1987
  ] }),
986
- nodes.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: "No properties yet." }) : /* @__PURE__ */ jsx2("div", { className: "rounded-lg border divide-y", children: nodes.map((node) => /* @__PURE__ */ jsx2(
1988
+ propsOpen && (propertyNodes.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: "No properties yet." }) : /* @__PURE__ */ jsx2("div", { className: "flex-1 min-h-0 overflow-auto rounded-lg border divide-y", children: propertyNodes.map((node) => /* @__PURE__ */ jsx2(
987
1989
  PropertyRow,
988
1990
  {
989
1991
  node,
@@ -991,7 +1993,72 @@ function PropertyTree({
991
1993
  depth: 0
992
1994
  },
993
1995
  pathKey(node.path)
994
- )) })
1996
+ )) })),
1997
+ /* @__PURE__ */ jsx2(
1998
+ DefinitionsRegion,
1999
+ {
2000
+ nodes: definitionNodes,
2001
+ state,
2002
+ canEdit: canEditDefs
2003
+ }
2004
+ )
2005
+ ] });
2006
+ }
2007
+ function DefinitionsRegion({
2008
+ nodes,
2009
+ state,
2010
+ canEdit
2011
+ }) {
2012
+ const [open, setOpen] = useState2(false);
2013
+ const inUseDefs = useMemo2(
2014
+ () => buildInUseDefs(
2015
+ state.schema,
2016
+ nodes.map((n) => n.key)
2017
+ ),
2018
+ [state.schema, nodes]
2019
+ );
2020
+ if (nodes.length === 0 && !canEdit) return null;
2021
+ return /* @__PURE__ */ jsxs2("div", { className: "mt-3 shrink-0", children: [
2022
+ /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center gap-2", children: [
2023
+ /* @__PURE__ */ jsxs2(
2024
+ "button",
2025
+ {
2026
+ type: "button",
2027
+ className: "flex items-center gap-1 heading-meta",
2028
+ onClick: () => setOpen((v) => !v),
2029
+ children: [
2030
+ open ? /* @__PURE__ */ jsx2(ChevronDown, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx2(ChevronRight, { className: "h-3 w-3" }),
2031
+ "Definitions",
2032
+ nodes.length > 0 && /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground/60", children: [
2033
+ "(",
2034
+ nodes.length,
2035
+ ")"
2036
+ ] })
2037
+ ]
2038
+ }
2039
+ ),
2040
+ canEdit && /* @__PURE__ */ jsx2("span", { className: "ml-auto flex gap-1", children: /* @__PURE__ */ jsx2(
2041
+ IconButton,
2042
+ {
2043
+ label: "Add definition",
2044
+ onClick: () => {
2045
+ setOpen(true);
2046
+ state.addDefinition();
2047
+ },
2048
+ children: /* @__PURE__ */ jsx2(Plus, {})
2049
+ }
2050
+ ) })
2051
+ ] }),
2052
+ open && (nodes.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: "No definitions yet." }) : /* @__PURE__ */ jsx2("div", { className: "max-h-64 overflow-auto rounded-lg border divide-y", children: nodes.map((node) => /* @__PURE__ */ jsx2(
2053
+ PropertyRow,
2054
+ {
2055
+ node,
2056
+ state,
2057
+ depth: 0,
2058
+ inUseDefs
2059
+ },
2060
+ pathKey(node.path)
2061
+ )) }))
995
2062
  ] });
996
2063
  }
997
2064
  function NameEditor({
@@ -1031,23 +2098,82 @@ function NameEditor({
1031
2098
  }
1032
2099
  );
1033
2100
  }
2101
+ var KIND_ICONS = {
2102
+ reference: { Icon: Link2, label: "reference" },
2103
+ definition: { Icon: Puzzle, label: "definition" },
2104
+ conditional: { Icon: GitBranch, label: "condition" },
2105
+ object: { Icon: Braces, label: "object" },
2106
+ array: { Icon: Brackets, label: "array" },
2107
+ oneOf: { Icon: GitFork, label: "one of" },
2108
+ enum: { Icon: List, label: "choice" }
2109
+ };
2110
+ function kindIconFor(node) {
2111
+ const last = node.path[node.path.length - 1];
2112
+ if (last?.kind === "slot") return { Icon: CornerDownRight, label: "branch" };
2113
+ if (node.slotKind === "branch") return { Icon: GitFork, label: "variant" };
2114
+ return KIND_ICONS[node.kind] ?? scalarIcon(node.property.type);
2115
+ }
2116
+ function scalarIcon(type) {
2117
+ if (type === "number" || type === "integer")
2118
+ return { Icon: Hash, label: "number" };
2119
+ if (type === "boolean") return { Icon: ToggleLeft, label: "boolean" };
2120
+ return { Icon: Type, label: "text" };
2121
+ }
2122
+ function KindIcon({ node }) {
2123
+ const { Icon, label } = kindIconFor(node);
2124
+ return /* @__PURE__ */ jsx2(
2125
+ Icon,
2126
+ {
2127
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/60",
2128
+ "aria-label": label
2129
+ }
2130
+ );
2131
+ }
2132
+ function Markers({ node }) {
2133
+ const rule = node.property["x-ui-rule"];
2134
+ if (!node.required && !rule) return null;
2135
+ const scopeField = rule?.condition.scope.replace(/^\//, "");
2136
+ const ruleTip = rule ? `${rule.effect} when ${scopeField || rule.condition.scope}` : "";
2137
+ return /* @__PURE__ */ jsxs2("span", { className: "shrink-0 flex items-center gap-1 group-hover/row:hidden", children: [
2138
+ node.required && /* @__PURE__ */ jsx2(
2139
+ "span",
2140
+ {
2141
+ className: "text-destructive text-xs leading-none",
2142
+ title: "required",
2143
+ "aria-label": "required",
2144
+ children: "*"
2145
+ }
2146
+ ),
2147
+ rule && /* @__PURE__ */ jsx2("span", { title: ruleTip, "aria-label": ruleTip, className: "flex", children: /* @__PURE__ */ jsx2(Eye, { className: "h-3 w-3 text-primary" }) })
2148
+ ] });
2149
+ }
2150
+ function canAddChildHere(node, state, isConditional) {
2151
+ if (!acceptsChildren(node)) return false;
2152
+ return node.kind === "oneOf" || isConditional ? state.canEditProperty(node.path) : state.canAddChild(node.path);
2153
+ }
2154
+ function isRuleStyled(node) {
2155
+ const last = node.path[node.path.length - 1];
2156
+ return node.kind === "conditional" || last?.kind === "slot";
2157
+ }
1034
2158
  function RowLabel({
1035
2159
  node,
1036
2160
  editable,
1037
2161
  onStartRename
1038
2162
  }) {
1039
2163
  const title = editable ? `${node.key}${node.required ? " (required)" : ""} -- double-click to rename` : `${node.key} -- fixed (not editable)`;
2164
+ const ruleStyled = isRuleStyled(node);
1040
2165
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
1041
- /* @__PURE__ */ jsxs2(
2166
+ /* @__PURE__ */ jsx2(KindIcon, { node }),
2167
+ /* @__PURE__ */ jsx2(
1042
2168
  "span",
1043
2169
  {
1044
- className: "flex-1 min-w-0 truncate text-sm font-medium",
2170
+ className: cn(
2171
+ "flex-1 min-w-0 truncate text-sm",
2172
+ ruleStyled ? "font-mono text-xs uppercase tracking-wide text-muted-foreground" : "font-medium"
2173
+ ),
1045
2174
  onDoubleClick: onStartRename,
1046
2175
  title,
1047
- children: [
1048
- node.key,
1049
- node.required && /* @__PURE__ */ jsx2("span", { className: "ml-0.5 text-destructive", "aria-label": "required", children: "*" })
1050
- ]
2176
+ children: node.key
1051
2177
  }
1052
2178
  ),
1053
2179
  !editable && /* @__PURE__ */ jsx2(
@@ -1057,89 +2183,192 @@ function RowLabel({
1057
2183
  "aria-label": "fixed"
1058
2184
  }
1059
2185
  ),
2186
+ /* @__PURE__ */ jsx2(Markers, { node }),
1060
2187
  /* @__PURE__ */ jsx2(
1061
2188
  "span",
1062
2189
  {
1063
- className: "shrink-0 max-w-[45%] truncate font-mono text-[11px] text-muted-foreground/70 group-hover/row:hidden",
2190
+ className: "shrink-0 max-w-[40%] truncate font-mono text-[11px] text-muted-foreground/70 group-hover/row:hidden",
1064
2191
  title: node.typeLabel,
1065
2192
  children: node.typeLabel
1066
2193
  }
1067
2194
  )
1068
2195
  ] });
1069
2196
  }
1070
- function RowActions({
2197
+ function isConditionalMember2(node) {
2198
+ return node.kind === "conditional";
2199
+ }
2200
+ function isConditionalSlot(node) {
2201
+ const last = node.path[node.path.length - 1];
2202
+ return node.slotKind === "conditional" && last?.kind === "slot";
2203
+ }
2204
+ function acceptsChildren(node) {
2205
+ return node.kind === "object" || node.kind === "array" || node.kind === "oneOf" || node.kind === "definition" || node.slotKind === "branch" || isConditionalSlot(node);
2206
+ }
2207
+ function acceptsConditions(node) {
2208
+ return node.kind === "object" || isConditionalSlot(node);
2209
+ }
2210
+ function ActionButton({
2211
+ label,
2212
+ tone,
2213
+ disabled,
2214
+ onAct,
2215
+ children
2216
+ }) {
2217
+ return /* @__PURE__ */ jsx2(
2218
+ IconButton,
2219
+ {
2220
+ label,
2221
+ tone,
2222
+ disabled,
2223
+ revealOnHover: true,
2224
+ onClick: (e) => {
2225
+ e.stopPropagation();
2226
+ onAct();
2227
+ },
2228
+ children
2229
+ }
2230
+ );
2231
+ }
2232
+ function MoveActions({
2233
+ node,
2234
+ state
2235
+ }) {
2236
+ const isBranch = node.slotKind === "branch";
2237
+ const move = (dir) => isBranch ? state.moveBranch(node.path, dir) : state.moveProperty(node.path, dir);
2238
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2239
+ /* @__PURE__ */ jsx2(ActionButton, { label: "Move up", onAct: () => move(-1), children: /* @__PURE__ */ jsx2(ChevronUp, {}) }),
2240
+ /* @__PURE__ */ jsx2(ActionButton, { label: "Move down", onAct: () => move(1), children: /* @__PURE__ */ jsx2(ChevronDown, {}) })
2241
+ ] });
2242
+ }
2243
+ function AddActions({
1071
2244
  node,
1072
2245
  state,
1073
2246
  editable,
1074
2247
  canAddHere
1075
2248
  }) {
2249
+ const isOneOf3 = node.kind === "oneOf";
2250
+ const addChild = () => isOneOf3 ? state.addBranch(node.path) : state.addProperty(node.path, "scalar");
2251
+ const condMember = isConditionalMember2(node) && editable;
2252
+ const hasElse = !!node.property.else;
1076
2253
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
1077
- editable && /* @__PURE__ */ jsxs2(Fragment, { children: [
1078
- /* @__PURE__ */ jsx2(
1079
- IconButton,
1080
- {
1081
- label: "Move up",
1082
- revealOnHover: true,
1083
- onClick: (e) => {
1084
- e.stopPropagation();
1085
- state.moveProperty(node.path, -1);
1086
- },
1087
- children: /* @__PURE__ */ jsx2(ChevronUp, {})
1088
- }
1089
- ),
1090
- /* @__PURE__ */ jsx2(
1091
- IconButton,
1092
- {
1093
- label: "Move down",
1094
- revealOnHover: true,
1095
- onClick: (e) => {
1096
- e.stopPropagation();
1097
- state.moveProperty(node.path, 1);
1098
- },
1099
- children: /* @__PURE__ */ jsx2(ChevronDown, {})
1100
- }
1101
- )
1102
- ] }),
1103
2254
  canAddHere && /* @__PURE__ */ jsx2(
1104
- IconButton,
2255
+ ActionButton,
1105
2256
  {
1106
- label: "Add child property",
1107
- revealOnHover: true,
1108
- onClick: (e) => {
1109
- e.stopPropagation();
1110
- state.addProperty(node.path, "scalar");
1111
- },
2257
+ label: isOneOf3 ? "Add branch" : "Add child property",
2258
+ onAct: addChild,
1112
2259
  children: /* @__PURE__ */ jsx2(Plus, {})
1113
2260
  }
1114
2261
  ),
1115
- editable && /* @__PURE__ */ jsx2(
1116
- IconButton,
2262
+ editable && acceptsConditions(node) && /* @__PURE__ */ jsx2(
2263
+ ActionButton,
1117
2264
  {
1118
- label: "Delete property",
1119
- tone: "destructive",
1120
- revealOnHover: true,
1121
- onClick: (e) => {
1122
- e.stopPropagation();
1123
- state.deleteProperty(node.path);
1124
- },
1125
- children: /* @__PURE__ */ jsx2(Trash2, {})
2265
+ label: "Add condition",
2266
+ onAct: () => state.addConditional(node.path),
2267
+ children: /* @__PURE__ */ jsx2(GitBranch, {})
1126
2268
  }
1127
- )
2269
+ ),
2270
+ condMember && !hasElse && /* @__PURE__ */ jsx2(ActionButton, { label: "Add else", onAct: () => state.addElse(node.path), children: /* @__PURE__ */ jsx2(SplitSquareHorizontal, {}) })
2271
+ ] });
2272
+ }
2273
+ function lastSegment(node) {
2274
+ return node.path[node.path.length - 1];
2275
+ }
2276
+ function removeDescriptor(node, state, inUseDefs) {
2277
+ const last = lastSegment(node);
2278
+ const slotName = last?.kind === "slot" ? last.slot : void 0;
2279
+ if (slotName === "then") return null;
2280
+ if (node.slotKind === "branch") {
2281
+ return simpleRemove("Remove branch", () => state.removeBranch(node.path));
2282
+ }
2283
+ if (isConditionalMember2(node)) {
2284
+ return simpleRemove(
2285
+ "Remove condition",
2286
+ () => state.removeConditional(node.path)
2287
+ );
2288
+ }
2289
+ if (node.kind === "definition") {
2290
+ const inUse = inUseDefs.has(node.key);
2291
+ return {
2292
+ label: inUse ? "In use -- remove references first" : "Delete definition",
2293
+ disabled: inUse,
2294
+ remove: () => state.removeDefinition(node.key)
2295
+ };
2296
+ }
2297
+ if (slotName === "else") {
2298
+ return simpleRemove(
2299
+ "Remove else",
2300
+ () => state.removeElse(node.path.slice(0, -1))
2301
+ );
2302
+ }
2303
+ return simpleRemove("Delete property", () => state.deleteProperty(node.path));
2304
+ }
2305
+ function simpleRemove(label, remove) {
2306
+ return { label, disabled: false, remove };
2307
+ }
2308
+ function RemoveAction({
2309
+ node,
2310
+ state,
2311
+ inUseDefs
2312
+ }) {
2313
+ const desc = removeDescriptor(node, state, inUseDefs);
2314
+ if (!desc) return null;
2315
+ return /* @__PURE__ */ jsx2(
2316
+ ActionButton,
2317
+ {
2318
+ label: desc.label,
2319
+ tone: "destructive",
2320
+ disabled: desc.disabled,
2321
+ onAct: desc.remove,
2322
+ children: /* @__PURE__ */ jsx2(Trash2, {})
2323
+ }
2324
+ );
2325
+ }
2326
+ var EMPTY_SET = /* @__PURE__ */ new Set();
2327
+ function RowActions({
2328
+ node,
2329
+ state,
2330
+ editable,
2331
+ canAddHere,
2332
+ inUseDefs = EMPTY_SET
2333
+ }) {
2334
+ const reorderable = editable && node.slotKind !== "conditional" && node.slotKind !== "definition";
2335
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2336
+ reorderable && /* @__PURE__ */ jsx2(MoveActions, { node, state }),
2337
+ /* @__PURE__ */ jsx2(
2338
+ AddActions,
2339
+ {
2340
+ node,
2341
+ state,
2342
+ editable,
2343
+ canAddHere
2344
+ }
2345
+ ),
2346
+ editable && /* @__PURE__ */ jsx2(RemoveAction, { node, state, inUseDefs })
1128
2347
  ] });
1129
2348
  }
1130
2349
  function PropertyRow({
1131
2350
  node,
1132
2351
  state,
1133
- depth
2352
+ depth,
2353
+ inUseDefs = EMPTY_SET
1134
2354
  }) {
1135
2355
  const selected = samePath(state.selection, node.path);
1136
2356
  const [editing, setEditing] = useState2(false);
1137
- const editable = state.canEditProperty(node.path);
1138
- const canAddHere = (node.kind === "object" || node.kind === "array") && state.canAddChild(node.path);
1139
- const showActions = editable || canAddHere;
2357
+ const editable = !node.readOnly && state.canEditProperty(node.path);
2358
+ const isBranch = node.slotKind === "branch";
2359
+ const isConditional = node.slotKind === "conditional";
2360
+ const isDefinition = node.kind === "definition";
2361
+ const editableName = isDefinition ? !state.capabilities.locked : editable;
2362
+ const canAddHere = canAddChildHere(node, state, isConditional);
2363
+ const showActions = editableName || canAddHere;
1140
2364
  const startRename = (e) => {
1141
2365
  e.stopPropagation();
1142
- if (editable) setEditing(true);
2366
+ if (editableName && !isBranch && !isConditional) setEditing(true);
2367
+ };
2368
+ const commitRename = (next) => {
2369
+ setEditing(false);
2370
+ if (isDefinition) state.renameDefinition(node.key, next);
2371
+ else state.renameProperty(node.path, next);
1143
2372
  };
1144
2373
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
1145
2374
  /* @__PURE__ */ jsxs2(
@@ -1156,17 +2385,14 @@ function PropertyRow({
1156
2385
  NameEditor,
1157
2386
  {
1158
2387
  initial: node.key,
1159
- onCommit: (next) => {
1160
- setEditing(false);
1161
- state.renameProperty(node.path, next);
1162
- },
2388
+ onCommit: commitRename,
1163
2389
  onCancel: () => setEditing(false)
1164
2390
  }
1165
2391
  ) : /* @__PURE__ */ jsx2(
1166
2392
  RowLabel,
1167
2393
  {
1168
2394
  node,
1169
- editable,
2395
+ editable: editableName,
1170
2396
  onStartRename: startRename
1171
2397
  }
1172
2398
  ),
@@ -1187,8 +2413,9 @@ function PropertyRow({
1187
2413
  {
1188
2414
  node,
1189
2415
  state,
1190
- editable,
1191
- canAddHere
2416
+ editable: editableName,
2417
+ canAddHere,
2418
+ inUseDefs
1192
2419
  }
1193
2420
  )
1194
2421
  }
@@ -1201,7 +2428,8 @@ function PropertyRow({
1201
2428
  {
1202
2429
  node: child,
1203
2430
  state,
1204
- depth: depth + 1
2431
+ depth: depth + 1,
2432
+ inUseDefs
1205
2433
  },
1206
2434
  pathKey(child.path)
1207
2435
  ))
@@ -1209,29 +2437,27 @@ function PropertyRow({
1209
2437
  }
1210
2438
 
1211
2439
  // src/preview/PreviewPane.tsx
1212
- import { useMemo as useMemo4, useState as useState5 } from "react";
1213
- import { Check, Copy } from "lucide-react";
2440
+ import { useMemo as useMemo7, useState as useState6 } from "react";
1214
2441
  import {
1215
- Button as Button3,
1216
2442
  Tabs,
1217
2443
  TabsList,
1218
2444
  TabsTrigger,
1219
2445
  TabsContent,
1220
2446
  TabsSeparator
1221
2447
  } from "@petrarca/sonnet-ui";
1222
- import { JsonEditor as JsonEditor2 } from "@petrarca/sonnet-ui/json-editor";
1223
2448
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer3 } from "@petrarca/sonnet-forms";
1224
- import { extractSchemaDefaults } from "@petrarca/sonnet-core/schema";
1225
- import { copyTextToClipboard } from "@petrarca/sonnet-core";
2449
+ import {
2450
+ extractSchemaDefaults
2451
+ } from "@petrarca/sonnet-core/schema";
1226
2452
 
1227
2453
  // src/preview/TablePreview.tsx
1228
- import { useMemo as useMemo2 } from "react";
2454
+ import { useMemo as useMemo3 } from "react";
1229
2455
  import { EntityTable } from "@petrarca/sonnet-ui";
1230
2456
  import { toPageResult } from "@petrarca/sonnet-core";
1231
2457
 
1232
2458
  // src/preview/sampleRows.ts
1233
- function sampleString(prop, key, i) {
1234
- switch (prop.format) {
2459
+ function sampleString(prop2, key, i) {
2460
+ switch (prop2.format) {
1235
2461
  case "email":
1236
2462
  return `user${i + 1}@example.com`;
1237
2463
  case "date":
@@ -1251,13 +2477,13 @@ function titleCase(key) {
1251
2477
  const s = key.replace(/[_-]+/g, " ").trim();
1252
2478
  return s.charAt(0).toUpperCase() + s.slice(1);
1253
2479
  }
1254
- function sampleValue(prop, key, i) {
1255
- if (Array.isArray(prop.enum) && prop.enum.length > 0) {
1256
- return prop.enum[i % prop.enum.length];
2480
+ function sampleValue(prop2, key, i) {
2481
+ if (Array.isArray(prop2.enum) && prop2.enum.length > 0) {
2482
+ return prop2.enum[i % prop2.enum.length];
1257
2483
  }
1258
- switch (prop.type) {
2484
+ switch (prop2.type) {
1259
2485
  case "string":
1260
- return sampleString(prop, key, i);
2486
+ return sampleString(prop2, key, i);
1261
2487
  case "number":
1262
2488
  return Math.round((i + 1) * 1.5 * 100) / 100;
1263
2489
  case "integer":
@@ -1276,8 +2502,8 @@ function sampleRows(schema, count = 3) {
1276
2502
  const entries = Object.entries(schema.properties);
1277
2503
  return Array.from({ length: count }, (_, i) => {
1278
2504
  const row = {};
1279
- for (const [key, prop] of entries) {
1280
- row[key] = sampleValue(prop, key, i);
2505
+ for (const [key, prop2] of entries) {
2506
+ row[key] = sampleValue(prop2, key, i);
1281
2507
  }
1282
2508
  return row;
1283
2509
  });
@@ -1289,7 +2515,7 @@ function TablePreview({
1289
2515
  schema,
1290
2516
  uiSchema
1291
2517
  }) {
1292
- const data = useMemo2(() => toPageResult(sampleRows(schema)), [schema]);
2518
+ const data = useMemo3(() => toPageResult(sampleRows(schema)), [schema]);
1293
2519
  return /* @__PURE__ */ jsx3(
1294
2520
  EntityTable,
1295
2521
  {
@@ -1304,17 +2530,282 @@ function TablePreview({
1304
2530
  }
1305
2531
 
1306
2532
  // src/designers/FormLayoutDesigner.tsx
1307
- import { useMemo as useMemo3, useState as useState3 } from "react";
1308
- import { Input as Input2, IconButton as IconButton2, Button } from "@petrarca/sonnet-ui";
1309
- import { FormSelect as FormSelect2, JsonSchemaFormRenderer as JsonSchemaFormRenderer2 } from "@petrarca/sonnet-forms";
2533
+ import { useMemo as useMemo5, useState as useState4 } from "react";
2534
+ import { cn as cn2 } from "@petrarca/sonnet-core";
2535
+ import { Input as Input3, IconButton as IconButton2, Button } from "@petrarca/sonnet-ui";
2536
+ import { FormSelect as FormSelect3, JsonSchemaFormRenderer as JsonSchemaFormRenderer2 } from "@petrarca/sonnet-forms";
1310
2537
  import {
1311
2538
  Plus as Plus2,
1312
2539
  Trash2 as Trash22,
1313
2540
  ChevronUp as ChevronUp2,
1314
2541
  ChevronDown as ChevronDown2,
2542
+ ChevronLeft,
2543
+ ChevronRight as ChevronRight2,
1315
2544
  GripVertical
1316
2545
  } from "lucide-react";
2546
+
2547
+ // src/inspector/RuleFields.tsx
2548
+ import { useEffect as useEffect2, useMemo as useMemo4, useState as useState3 } from "react";
2549
+ import { FormSelect as FormSelect2 } from "@petrarca/sonnet-forms";
2550
+
2551
+ // src/model/conditions.ts
2552
+ var CONDITION_OPERATORS = [
2553
+ "=",
2554
+ "!=",
2555
+ ">",
2556
+ ">=",
2557
+ "<",
2558
+ "<=",
2559
+ "exists"
2560
+ ];
2561
+ var CONDITION_OPERATOR_LABELS = {
2562
+ "=": "equals",
2563
+ "!=": "not equals",
2564
+ ">": "greater than",
2565
+ ">=": "greater or equal",
2566
+ "<": "less than",
2567
+ "<=": "less or equal",
2568
+ exists: "exists"
2569
+ };
2570
+ var OPERATOR_OPTIONS = CONDITION_OPERATORS.map((op) => ({
2571
+ value: op,
2572
+ label: CONDITION_OPERATOR_LABELS[op]
2573
+ }));
2574
+ var NUMERIC_OPS = /* @__PURE__ */ new Set([
2575
+ ">",
2576
+ ">=",
2577
+ "<",
2578
+ "<="
2579
+ ]);
2580
+ function coerceValue(value) {
2581
+ if (value.trim() === "") return value;
2582
+ const n = Number(value);
2583
+ return Number.isFinite(n) ? n : value;
2584
+ }
2585
+ function coerceForScope(value, operator, type) {
2586
+ if (NUMERIC_OPS.has(operator)) return coerceValue(value);
2587
+ if (type === "boolean") return value === "true";
2588
+ if (type === "number" || type === "integer") return coerceValue(value);
2589
+ return value;
2590
+ }
2591
+ function operatorToConstraint(operator, value, type) {
2592
+ const v = coerceForScope(value, operator, type);
2593
+ switch (operator) {
2594
+ case "=":
2595
+ return { const: v };
2596
+ case "!=":
2597
+ return { not: { const: v } };
2598
+ case ">":
2599
+ return { exclusiveMinimum: v };
2600
+ case ">=":
2601
+ return { minimum: v };
2602
+ case "<":
2603
+ return { exclusiveMaximum: v };
2604
+ case "<=":
2605
+ return { maximum: v };
2606
+ case "exists":
2607
+ return {};
2608
+ }
2609
+ }
2610
+ function conditionToIf(condition2, type) {
2611
+ const { field, operator } = condition2;
2612
+ if (!field) return void 0;
2613
+ if (operator === "exists") {
2614
+ return { required: [field] };
2615
+ }
2616
+ return {
2617
+ properties: {
2618
+ [field]: operatorToConstraint(operator, condition2.value, type)
2619
+ },
2620
+ required: [field]
2621
+ };
2622
+ }
2623
+ function constraintToOperator(c) {
2624
+ if (c.not && "const" in c.not)
2625
+ return { operator: "!=", value: String(c.not.const) };
2626
+ if (c.exclusiveMinimum !== void 0)
2627
+ return { operator: ">", value: String(c.exclusiveMinimum) };
2628
+ if (c.minimum !== void 0)
2629
+ return { operator: ">=", value: String(c.minimum) };
2630
+ if (c.exclusiveMaximum !== void 0)
2631
+ return { operator: "<", value: String(c.exclusiveMaximum) };
2632
+ if (c.maximum !== void 0)
2633
+ return { operator: "<=", value: String(c.maximum) };
2634
+ if ("const" in c) return { operator: "=", value: String(c.const) };
2635
+ return { operator: "exists", value: "" };
2636
+ }
2637
+ function ifToCondition(ifSchema) {
2638
+ const empty = { field: "", operator: "=", value: "" };
2639
+ if (!ifSchema) return empty;
2640
+ const props = ifSchema.properties ?? {};
2641
+ const field = Object.keys(props)[0] ?? ifSchema.required?.[0] ?? "";
2642
+ if (!field) return empty;
2643
+ const constraint = props[field];
2644
+ if (!constraint) return { field, operator: "exists", value: "" };
2645
+ const { operator, value } = constraintToOperator(constraint);
2646
+ return { field, operator, value };
2647
+ }
2648
+
2649
+ // src/inspector/OperatorValueInput.tsx
2650
+ import { Input as Input2 } from "@petrarca/sonnet-ui";
1317
2651
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2652
+ function OperatorValueInput({
2653
+ id,
2654
+ cacheKey,
2655
+ value,
2656
+ onCommit,
2657
+ disabled
2658
+ }) {
2659
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
2660
+ /* @__PURE__ */ jsx4("label", { htmlFor: id, className: "text-sm font-medium", children: "Value" }),
2661
+ /* @__PURE__ */ jsx4(
2662
+ Input2,
2663
+ {
2664
+ id,
2665
+ defaultValue: value,
2666
+ onBlur: (e) => onCommit(e.target.value),
2667
+ disabled,
2668
+ placeholder: "Value to compare"
2669
+ },
2670
+ cacheKey
2671
+ )
2672
+ ] });
2673
+ }
2674
+
2675
+ // src/inspector/RuleFields.tsx
2676
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2677
+ function ruleToForm(rule) {
2678
+ if (!rule) return { effect: "", scope: "", operator: "=", value: "" };
2679
+ const { operator, value } = constraintToOperator(rule.condition.schema);
2680
+ return { effect: rule.effect, scope: rule.condition.scope, operator, value };
2681
+ }
2682
+ function formToRule(form, scopeType) {
2683
+ if (!form.effect || !form.scope) return void 0;
2684
+ return {
2685
+ effect: form.effect,
2686
+ condition: {
2687
+ scope: form.scope,
2688
+ schema: operatorToConstraint(form.operator, form.value, scopeType)
2689
+ }
2690
+ };
2691
+ }
2692
+ var BOOLEAN_VALUE_OPTIONS = [
2693
+ { value: "true", label: "true" },
2694
+ { value: "false", label: "false" }
2695
+ ];
2696
+ function RuleFields({
2697
+ rule,
2698
+ scopes,
2699
+ effects,
2700
+ idPrefix,
2701
+ disabled = false,
2702
+ onChange
2703
+ }) {
2704
+ const [form, setForm] = useState3(() => ruleToForm(rule));
2705
+ const seed = JSON.stringify(rule ?? null);
2706
+ useEffect2(() => {
2707
+ setForm(ruleToForm(JSON.parse(seed) ?? void 0));
2708
+ }, [seed]);
2709
+ const scopeType = useMemo4(
2710
+ () => scopes.find((s) => s.pointer === form.scope)?.type,
2711
+ [scopes, form.scope]
2712
+ );
2713
+ const commit = (next) => {
2714
+ setForm(next);
2715
+ const nextType = scopes.find((s) => s.pointer === next.scope)?.type;
2716
+ const built = formToRule(next, nextType);
2717
+ if (built || next.effect === "") onChange(built);
2718
+ };
2719
+ const scopeOptions = useMemo4(
2720
+ () => scopes.map((s) => ({ value: s.pointer, label: s.pointer.slice(1) })),
2721
+ [scopes]
2722
+ );
2723
+ const needsValue = form.operator !== "exists";
2724
+ const isBoolean = scopeType === "boolean";
2725
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
2726
+ /* @__PURE__ */ jsx5(
2727
+ FormSelect2,
2728
+ {
2729
+ id: `${idPrefix}-effect`,
2730
+ name: "effect",
2731
+ label: "Effect",
2732
+ options: effects,
2733
+ value: form.effect,
2734
+ onChange: (v) => commit({ ...form, effect: v ?? "" }),
2735
+ clearable: false,
2736
+ disabled
2737
+ }
2738
+ ),
2739
+ form.effect !== "" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
2740
+ /* @__PURE__ */ jsx5(
2741
+ FormSelect2,
2742
+ {
2743
+ id: `${idPrefix}-scope`,
2744
+ name: "scope",
2745
+ label: "When field",
2746
+ options: scopeOptions,
2747
+ value: form.scope || null,
2748
+ onChange: (v) => commit({ ...form, scope: v ?? "" }),
2749
+ placeholder: "Select a field...",
2750
+ clearable: false,
2751
+ disabled
2752
+ }
2753
+ ),
2754
+ /* @__PURE__ */ jsx5(
2755
+ FormSelect2,
2756
+ {
2757
+ id: `${idPrefix}-operator`,
2758
+ name: "operator",
2759
+ label: "Operator",
2760
+ options: OPERATOR_OPTIONS,
2761
+ value: form.operator,
2762
+ onChange: (v) => commit({ ...form, operator: v ?? "=" }),
2763
+ clearable: false,
2764
+ disabled
2765
+ }
2766
+ ),
2767
+ needsValue && (isBoolean ? /* @__PURE__ */ jsx5(
2768
+ FormSelect2,
2769
+ {
2770
+ id: `${idPrefix}-value`,
2771
+ name: "value",
2772
+ label: "Value",
2773
+ options: BOOLEAN_VALUE_OPTIONS,
2774
+ value: form.value === "true" ? "true" : "false",
2775
+ onChange: (v) => commit({ ...form, value: v ?? "false" }),
2776
+ clearable: false,
2777
+ disabled: disabled || !form.scope
2778
+ }
2779
+ ) : /* @__PURE__ */ jsx5(
2780
+ OperatorValueInput,
2781
+ {
2782
+ id: `${idPrefix}-value`,
2783
+ cacheKey: `${form.scope}:${form.operator}`,
2784
+ value: form.value,
2785
+ onCommit: (v) => commit({ ...form, value: v }),
2786
+ disabled: disabled || !form.scope
2787
+ }
2788
+ ))
2789
+ ] })
2790
+ ] });
2791
+ }
2792
+
2793
+ // src/inspector/ruleEffects.ts
2794
+ var VISIBILITY_EFFECT_OPTIONS = [
2795
+ { value: "", label: "None (always shown)" },
2796
+ { value: "show", label: "Show when" },
2797
+ { value: "hide", label: "Hide when" }
2798
+ ];
2799
+ var FIELD_EFFECT_OPTIONS = [
2800
+ { value: "", label: "None (always visible)" },
2801
+ { value: "show", label: "Show when" },
2802
+ { value: "hide", label: "Hide when" },
2803
+ { value: "enable", label: "Enable when" },
2804
+ { value: "disable", label: "Disable when" }
2805
+ ];
2806
+
2807
+ // src/designers/FormLayoutDesigner.tsx
2808
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1318
2809
  var DRAG_MIME = "application/x-sonnet-property";
1319
2810
  function orderedKeysOf(properties, order) {
1320
2811
  const keys = Object.keys(properties);
@@ -1323,21 +2814,43 @@ function orderedKeysOf(properties, order) {
1323
2814
  return [...inOrder, ...keys.filter((k) => !inOrder.includes(k))];
1324
2815
  }
1325
2816
  function scopeTarget(schema, scopePath) {
1326
- const prop = getProperty(schema, scopePath);
1327
- if (!prop) return void 0;
1328
- return prop.type === "array" ? prop.items : prop;
2817
+ const prop2 = getProperty(schema, scopePath);
2818
+ if (!prop2) return void 0;
2819
+ return prop2.type === "array" ? prop2.items : prop2;
2820
+ }
2821
+ function scopeContainer(schema, uiSchema, scopePath) {
2822
+ if (scopePath.length === 0) {
2823
+ return {
2824
+ container: schema,
2825
+ layout: uiSchema["x-ui-layout"]
2826
+ };
2827
+ }
2828
+ const container = scopeTarget(schema, scopePath);
2829
+ return {
2830
+ container,
2831
+ layout: container?.["x-ui-layout"]
2832
+ };
1329
2833
  }
1330
2834
  function resolveScope(schema, uiSchema, scopePath) {
1331
- const container = scopePath.length === 0 ? schema : scopeTarget(schema, scopePath);
1332
- const layout = scopePath.length === 0 ? uiSchema["x-ui-layout"] : container?.["x-ui-layout"];
1333
- const isArray = scopeIsArray(schema, scopePath);
2835
+ const { container, layout } = scopeContainer(schema, uiSchema, scopePath);
2836
+ const props = container?.properties ?? {};
1334
2837
  return {
1335
- keys: orderedKeysOf(container?.properties ?? {}, container?.["x-ui-order"]),
2838
+ keys: orderedKeysOf(props, container?.["x-ui-order"]),
2839
+ props,
1336
2840
  rows: layout?.rows ?? [],
2841
+ groups: layout?.groups ?? [],
1337
2842
  layout,
1338
- isArray
2843
+ isArray: scopeIsArray(schema, scopePath)
1339
2844
  };
1340
2845
  }
2846
+ function ownsField(rows, key) {
2847
+ return rows.some((r) => r.fields.includes(key));
2848
+ }
2849
+ function rowTarget(grouped, groups, groupIndex, flatRows) {
2850
+ const rows = grouped ? groups[groupIndex]?.rows ?? [] : flatRows;
2851
+ const all = grouped ? groups.flatMap((t) => t.rows) : flatRows;
2852
+ return { rows, assigned: new Set(all.flatMap((r) => r.fields)) };
2853
+ }
1341
2854
  function scopeIsArray(schema, scopePath) {
1342
2855
  if (scopePath.length === 0) return false;
1343
2856
  return getProperty(schema, scopePath)?.type === "array";
@@ -1377,9 +2890,9 @@ function ArrayItemLayoutBar({
1377
2890
  readOnly,
1378
2891
  onChange
1379
2892
  }) {
1380
- return /* @__PURE__ */ jsxs3("div", { className: "shrink-0 rounded-lg border p-3", children: [
1381
- /* @__PURE__ */ jsx4("p", { className: "heading-meta mb-2", children: "Array item layout" }),
1382
- /* @__PURE__ */ jsx4(
2893
+ return /* @__PURE__ */ jsxs5("div", { className: "shrink-0 rounded-lg border p-3", children: [
2894
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta mb-2", children: "Array item layout" }),
2895
+ /* @__PURE__ */ jsx6(
1383
2896
  JsonSchemaFormRenderer2,
1384
2897
  {
1385
2898
  schema: SCALAR_META_SCHEMA,
@@ -1396,23 +2909,37 @@ function ArrayItemLayoutBar({
1396
2909
  function FormLayoutDesigner({
1397
2910
  state
1398
2911
  }) {
1399
- const scopes = useMemo3(() => layoutScopes(state.schema), [state.schema]);
1400
- const [scopePath, setScopePath] = useState3([]);
1401
- const scopeKey = scopePath.join(".");
1402
- const activeScope = scopes.find((s) => s.path.join(".") === scopeKey) ?? scopes[0];
2912
+ const scopes = useMemo5(() => layoutScopes(state.schema), [state.schema]);
2913
+ const [scopePath, setScopePath] = useState4([]);
2914
+ const scopeKey = pathKey(scopePath);
2915
+ const activeScope = scopes.find((s) => pathKey(s.path) === scopeKey) ?? scopes[0];
1403
2916
  const effectivePath = activeScope.path;
1404
2917
  const {
1405
2918
  keys: allKeys,
1406
- rows,
2919
+ props: scopeProps,
2920
+ rows: flatRows,
2921
+ groups,
1407
2922
  layout,
1408
2923
  isArray
1409
2924
  } = resolveScope(state.schema, state.uiSchema, effectivePath);
1410
- const assigned = new Set(rows.flatMap((r) => r.fields));
2925
+ const grouped = groups.length > 0;
2926
+ const [activeGroup, setActiveGroup] = useState4(0);
2927
+ const groupIndex = grouped ? Math.min(activeGroup, groups.length - 1) : 0;
2928
+ const { rows, assigned } = rowTarget(grouped, groups, groupIndex, flatRows);
1411
2929
  const available = allKeys.filter((k) => !assigned.has(k));
1412
- const [dragKey, setDragKey] = useState3(null);
1413
- const commit = (next) => state.setScopedLayoutRows(effectivePath, next);
2930
+ const inOtherGroups = grouped ? allKeys.filter((k) => assigned.has(k) && !ownsField(rows, k)) : [];
2931
+ const [dragKey, setDragKey] = useState4(null);
2932
+ const commit = makeCommitRows(state, effectivePath, grouped, groupIndex);
1414
2933
  const place = (key, rowIndex) => {
1415
2934
  if (!key) return;
2935
+ if (grouped) {
2936
+ state.placeFieldInGroup(
2937
+ effectivePath,
2938
+ key,
2939
+ rowIndex === null ? null : { groupIndex, rowIndex }
2940
+ );
2941
+ return;
2942
+ }
1416
2943
  const next = rows.map((r) => ({
1417
2944
  ...r,
1418
2945
  fields: r.fields.filter((f) => f !== key)
@@ -1466,26 +2993,57 @@ function FormLayoutDesigner({
1466
2993
  });
1467
2994
  commit(next);
1468
2995
  };
2996
+ const scopeOptions = allKeys.map((k) => ({
2997
+ pointer: `/${k}`,
2998
+ type: scopeProps[k]?.type
2999
+ }));
3000
+ const groupOps = makeGroupOps(
3001
+ state,
3002
+ effectivePath,
3003
+ groups.length,
3004
+ groupIndex,
3005
+ setActiveGroup
3006
+ );
1469
3007
  const readOnly = state.readOnly;
1470
- return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-3 h-full min-h-0", children: [
1471
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 shrink-0", children: [
1472
- /* @__PURE__ */ jsx4("label", { className: "heading-meta", children: "Scope" }),
1473
- /* @__PURE__ */ jsx4("div", { className: "w-64", children: /* @__PURE__ */ jsx4(
1474
- FormSelect2,
3008
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-3 h-full min-h-0", children: [
3009
+ /* @__PURE__ */ jsx6(
3010
+ LayoutToolbar,
3011
+ {
3012
+ scopes,
3013
+ scopePath: effectivePath,
3014
+ onScope: setScopePath,
3015
+ grouped,
3016
+ variant: layout?.variant,
3017
+ readOnly,
3018
+ onMode: groupOps.setMode,
3019
+ onVariant: (v) => state.setScopedLayoutVariant(effectivePath, v)
3020
+ }
3021
+ ),
3022
+ grouped && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3023
+ /* @__PURE__ */ jsx6(
3024
+ GroupBar,
1475
3025
  {
1476
- id: "layout-scope",
1477
- name: "scope",
1478
- value: effectivePath.join("."),
1479
- options: scopes.map((s) => ({
1480
- value: s.path.join("."),
1481
- label: s.label
1482
- })),
1483
- onChange: (v) => setScopePath(v ? v.split(".").filter(Boolean) : []),
1484
- clearable: false
3026
+ groups,
3027
+ activeIndex: groupIndex,
3028
+ readOnly,
3029
+ onSelect: setActiveGroup,
3030
+ onAdd: groupOps.add,
3031
+ onRename: groupOps.rename,
3032
+ onRemove: groupOps.remove,
3033
+ onMove: groupOps.move
1485
3034
  }
1486
- ) })
3035
+ ),
3036
+ /* @__PURE__ */ jsx6(
3037
+ GroupSettings,
3038
+ {
3039
+ group: groups[groupIndex],
3040
+ scopes: scopeOptions,
3041
+ readOnly,
3042
+ onRule: (rule) => state.setLayoutGroupRule(effectivePath, groupIndex, rule)
3043
+ }
3044
+ )
1487
3045
  ] }),
1488
- isArray && /* @__PURE__ */ jsx4(
3046
+ isArray && /* @__PURE__ */ jsx6(
1489
3047
  ArrayItemLayoutBar,
1490
3048
  {
1491
3049
  layout,
@@ -1493,38 +3051,24 @@ function FormLayoutDesigner({
1493
3051
  onChange: (opts) => state.setScopedLayoutOptions(effectivePath, opts)
1494
3052
  }
1495
3053
  ),
1496
- /* @__PURE__ */ jsxs3("div", { className: "flex gap-4 flex-1 min-h-0", children: [
1497
- /* @__PURE__ */ jsxs3("div", { className: "w-56 shrink-0 flex flex-col min-h-0", children: [
1498
- /* @__PURE__ */ jsx4("p", { className: "heading-meta mb-2", children: "Available" }),
1499
- /* @__PURE__ */ jsx4(
1500
- "div",
1501
- {
1502
- className: "flex-1 min-h-0 overflow-auto rounded-lg border p-2 space-y-1 bg-muted/20",
1503
- onDragOver: (e) => {
1504
- if (dragKey) e.preventDefault();
1505
- },
1506
- onDrop: (e) => {
1507
- const key = e.dataTransfer.getData(DRAG_MIME) || dragKey;
1508
- if (key) place(key, null);
1509
- },
1510
- children: available.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground p-1", children: "All properties are placed in rows." }) : available.map((key) => /* @__PURE__ */ jsx4(
1511
- Chip,
1512
- {
1513
- label: key,
1514
- readOnly,
1515
- onDragStart: () => setDragKey(key),
1516
- onDragEnd: () => setDragKey(null)
1517
- },
1518
- key
1519
- ))
1520
- }
1521
- ),
1522
- /* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground mt-2", children: "Unplaced properties render stacked below the rows." })
1523
- ] }),
1524
- /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [
1525
- /* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center gap-2", children: [
1526
- /* @__PURE__ */ jsx4("p", { className: "heading-meta", children: "Rows" }),
1527
- !readOnly && /* @__PURE__ */ jsx4("span", { className: "ml-auto", children: /* @__PURE__ */ jsxs3(
3054
+ /* @__PURE__ */ jsxs5("div", { className: "flex gap-4 flex-1 min-h-0", children: [
3055
+ /* @__PURE__ */ jsx6(
3056
+ AvailablePool,
3057
+ {
3058
+ available,
3059
+ inOtherGroups,
3060
+ grouped,
3061
+ readOnly,
3062
+ dragActive: !!dragKey,
3063
+ onDropToPool: (key) => place(key, null),
3064
+ onDragStartKey: setDragKey,
3065
+ onDragEndKey: () => setDragKey(null)
3066
+ }
3067
+ ),
3068
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [
3069
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3070
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta", children: rowsHeading(grouped, groups, groupIndex) }),
3071
+ !readOnly && /* @__PURE__ */ jsx6("span", { className: "ml-auto", children: /* @__PURE__ */ jsxs5(
1528
3072
  Button,
1529
3073
  {
1530
3074
  size: "sm",
@@ -1532,13 +3076,13 @@ function FormLayoutDesigner({
1532
3076
  className: "gap-1.5",
1533
3077
  onClick: addRow,
1534
3078
  children: [
1535
- /* @__PURE__ */ jsx4(Plus2, { className: "h-4 w-4" }),
3079
+ /* @__PURE__ */ jsx6(Plus2, { className: "h-4 w-4" }),
1536
3080
  "Add row"
1537
3081
  ]
1538
3082
  }
1539
3083
  ) })
1540
3084
  ] }),
1541
- /* @__PURE__ */ jsx4("div", { className: "flex-1 min-h-0 overflow-auto space-y-3", children: rows.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: "No rows yet. Add a row, then drag properties into it." }) : rows.map((row, i) => /* @__PURE__ */ jsx4(
3085
+ /* @__PURE__ */ jsx6("div", { className: "flex-1 min-h-0 overflow-auto space-y-3", children: rows.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground", children: "No rows yet. Add a row, then drag properties into it." }) : rows.map((row, i) => /* @__PURE__ */ jsx6(
1542
3086
  RowCard,
1543
3087
  {
1544
3088
  row,
@@ -1564,6 +3108,352 @@ function FormLayoutDesigner({
1564
3108
  ] })
1565
3109
  ] });
1566
3110
  }
3111
+ function makeCommitRows(state, scopePath, grouped, groupIndex) {
3112
+ if (grouped) {
3113
+ return (rows) => state.setScopedGroupRows(scopePath, groupIndex, rows);
3114
+ }
3115
+ return (rows) => state.setScopedLayoutRows(scopePath, rows);
3116
+ }
3117
+ function rowsHeading(grouped, groups, groupIndex) {
3118
+ if (!grouped) return "Rows";
3119
+ return `Rows in ${groups[groupIndex]?.label ?? ""}`;
3120
+ }
3121
+ var VARIANT_OPTIONS = [
3122
+ { value: "tabs", label: "Tabs" },
3123
+ { value: "stepper", label: "Stepper" },
3124
+ { value: "sections", label: "Sections" }
3125
+ ];
3126
+ function makeGroupOps(state, scopePath, groupCount, activeIndex, setActiveGroup) {
3127
+ return {
3128
+ add: () => {
3129
+ state.addLayoutGroup(scopePath);
3130
+ setActiveGroup(groupCount);
3131
+ },
3132
+ rename: (index, label) => state.renameLayoutGroup(scopePath, index, label),
3133
+ move: (index, direction) => {
3134
+ state.moveLayoutGroup(scopePath, index, direction);
3135
+ if (index === activeIndex) setActiveGroup(activeIndex + direction);
3136
+ },
3137
+ remove: (index) => {
3138
+ state.removeLayoutGroup(scopePath, index);
3139
+ if (index <= activeIndex && activeIndex > 0)
3140
+ setActiveGroup(activeIndex - 1);
3141
+ },
3142
+ setMode: (mode) => {
3143
+ state.setScopedLayoutMode(scopePath, mode);
3144
+ setActiveGroup(0);
3145
+ }
3146
+ };
3147
+ }
3148
+ function AvailablePool({
3149
+ available,
3150
+ inOtherGroups,
3151
+ grouped,
3152
+ readOnly,
3153
+ dragActive,
3154
+ onDropToPool,
3155
+ onDragStartKey,
3156
+ onDragEndKey
3157
+ }) {
3158
+ const empty = available.length === 0 && inOtherGroups.length === 0;
3159
+ return /* @__PURE__ */ jsxs5("div", { className: "w-56 shrink-0 flex flex-col min-h-0", children: [
3160
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta mb-2", children: "Available" }),
3161
+ /* @__PURE__ */ jsxs5(
3162
+ "div",
3163
+ {
3164
+ className: "flex-1 min-h-0 overflow-auto rounded-lg border p-2 space-y-1 bg-muted/20",
3165
+ onDragOver: (e) => {
3166
+ if (dragActive) e.preventDefault();
3167
+ },
3168
+ onDrop: (e) => {
3169
+ const key = e.dataTransfer.getData(DRAG_MIME);
3170
+ if (key) onDropToPool(key);
3171
+ },
3172
+ children: [
3173
+ empty ? /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground p-1", children: grouped ? "All properties are placed in groups." : "All properties are placed in rows." }) : available.map((key) => /* @__PURE__ */ jsx6(
3174
+ Chip,
3175
+ {
3176
+ label: key,
3177
+ readOnly,
3178
+ onDragStart: () => onDragStartKey(key),
3179
+ onDragEnd: onDragEndKey
3180
+ },
3181
+ key
3182
+ )),
3183
+ inOtherGroups.length > 0 && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3184
+ /* @__PURE__ */ jsx6("p", { className: "pt-2 pb-1 text-[11px] uppercase tracking-wide text-muted-foreground/70", children: "In other groups" }),
3185
+ inOtherGroups.map((key) => /* @__PURE__ */ jsx6(
3186
+ Chip,
3187
+ {
3188
+ label: key,
3189
+ readOnly,
3190
+ muted: true,
3191
+ onDragStart: () => onDragStartKey(key),
3192
+ onDragEnd: onDragEndKey
3193
+ },
3194
+ key
3195
+ ))
3196
+ ] })
3197
+ ]
3198
+ }
3199
+ ),
3200
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground mt-2", children: grouped ? "Drag from \u201CIn other groups\u201D to move a property here. Unplaced properties render below the groups." : "Unplaced properties render stacked below the rows." })
3201
+ ] });
3202
+ }
3203
+ function LayoutToolbar({
3204
+ scopes,
3205
+ scopePath,
3206
+ onScope,
3207
+ grouped,
3208
+ variant,
3209
+ readOnly,
3210
+ onMode,
3211
+ onVariant
3212
+ }) {
3213
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 shrink-0", children: [
3214
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta", children: "Scope" }),
3215
+ /* @__PURE__ */ jsx6("div", { className: "w-64", children: /* @__PURE__ */ jsx6(
3216
+ FormSelect3,
3217
+ {
3218
+ id: "layout-scope",
3219
+ name: "scope",
3220
+ value: pathKey(scopePath),
3221
+ options: scopes.map((s) => ({
3222
+ value: pathKey(s.path),
3223
+ label: s.label
3224
+ })),
3225
+ onChange: (v) => onScope(v ? parsePathKey(v) : []),
3226
+ clearable: false
3227
+ }
3228
+ ) }),
3229
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta ml-4", children: "Group by" }),
3230
+ /* @__PURE__ */ jsx6(
3231
+ ModeToggle,
3232
+ {
3233
+ mode: grouped ? "groups" : "rows",
3234
+ readOnly,
3235
+ onChange: onMode
3236
+ }
3237
+ ),
3238
+ grouped && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3239
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta ml-4", children: "Show as" }),
3240
+ /* @__PURE__ */ jsx6("div", { className: "w-36", children: /* @__PURE__ */ jsx6(
3241
+ FormSelect3,
3242
+ {
3243
+ id: "layout-variant",
3244
+ name: "variant",
3245
+ value: variant ?? "tabs",
3246
+ options: VARIANT_OPTIONS,
3247
+ onChange: (v) => onVariant(v ?? "tabs"),
3248
+ clearable: false,
3249
+ disabled: readOnly
3250
+ }
3251
+ ) })
3252
+ ] })
3253
+ ] });
3254
+ }
3255
+ function ModeToggle({
3256
+ mode,
3257
+ readOnly,
3258
+ onChange
3259
+ }) {
3260
+ return /* @__PURE__ */ jsx6("div", { className: "inline-flex rounded-md border p-0.5", "aria-label": "Group by", children: ["rows", "groups"].map((m) => /* @__PURE__ */ jsx6(
3261
+ "button",
3262
+ {
3263
+ type: "button",
3264
+ disabled: readOnly,
3265
+ "aria-pressed": mode === m,
3266
+ onClick: () => onChange(m),
3267
+ className: cn2(
3268
+ "rounded px-2 py-0.5 text-xs capitalize disabled:opacity-50",
3269
+ mode === m ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:text-foreground"
3270
+ ),
3271
+ children: m
3272
+ },
3273
+ m
3274
+ )) });
3275
+ }
3276
+ function GroupSettings({
3277
+ group,
3278
+ scopes,
3279
+ readOnly,
3280
+ onRule
3281
+ }) {
3282
+ const [open, setOpen] = useState4(false);
3283
+ if (!group) return null;
3284
+ const hasRule = !!group.rule;
3285
+ return /* @__PURE__ */ jsxs5("div", { className: "shrink-0 rounded-lg border px-3 py-2", children: [
3286
+ /* @__PURE__ */ jsxs5(
3287
+ "button",
3288
+ {
3289
+ type: "button",
3290
+ className: "flex w-full items-center gap-1 heading-meta",
3291
+ onClick: () => setOpen((v) => !v),
3292
+ "aria-expanded": open,
3293
+ children: [
3294
+ open ? /* @__PURE__ */ jsx6(ChevronDown2, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx6(ChevronRight2, { className: "h-3 w-3" }),
3295
+ "Visibility of ",
3296
+ group.label,
3297
+ /* @__PURE__ */ jsx6("span", { className: "ml-1 normal-case text-muted-foreground/70", children: hasRule ? "conditional" : "always shown" })
3298
+ ]
3299
+ }
3300
+ ),
3301
+ open && /* @__PURE__ */ jsx6("div", { className: "mt-3 max-w-sm", children: /* @__PURE__ */ jsx6(
3302
+ RuleFields,
3303
+ {
3304
+ rule: group.rule,
3305
+ scopes,
3306
+ effects: VISIBILITY_EFFECT_OPTIONS,
3307
+ idPrefix: "group-rule",
3308
+ disabled: readOnly,
3309
+ onChange: (rule) => onRule(rule)
3310
+ }
3311
+ ) })
3312
+ ] });
3313
+ }
3314
+ function GroupBar({
3315
+ groups,
3316
+ activeIndex,
3317
+ readOnly,
3318
+ onSelect,
3319
+ onAdd,
3320
+ onRename,
3321
+ onRemove,
3322
+ onMove
3323
+ }) {
3324
+ return /* @__PURE__ */ jsxs5(
3325
+ "div",
3326
+ {
3327
+ role: "tablist",
3328
+ "aria-label": "Layout groups",
3329
+ className: "shrink-0 flex items-center gap-1 rounded-lg border p-1.5 overflow-x-auto",
3330
+ children: [
3331
+ groups.map((group, i) => /* @__PURE__ */ jsx6(
3332
+ GroupChip,
3333
+ {
3334
+ group,
3335
+ active: i === activeIndex,
3336
+ readOnly,
3337
+ onSelect: () => onSelect(i),
3338
+ onRename: (label) => onRename(i, label),
3339
+ onRemove: () => onRemove(i),
3340
+ onMoveLeft: i > 0 ? () => onMove(i, -1) : void 0,
3341
+ onMoveRight: i < groups.length - 1 ? () => onMove(i, 1) : void 0
3342
+ },
3343
+ `${i}:${group.label}`
3344
+ )),
3345
+ !readOnly && /* @__PURE__ */ jsxs5(
3346
+ Button,
3347
+ {
3348
+ size: "sm",
3349
+ variant: "outline",
3350
+ className: "gap-1.5 shrink-0",
3351
+ onClick: onAdd,
3352
+ children: [
3353
+ /* @__PURE__ */ jsx6(Plus2, { className: "h-4 w-4" }),
3354
+ "Add group"
3355
+ ]
3356
+ }
3357
+ )
3358
+ ]
3359
+ }
3360
+ );
3361
+ }
3362
+ function GroupChip({
3363
+ group,
3364
+ active,
3365
+ readOnly,
3366
+ onSelect,
3367
+ onRename,
3368
+ onRemove,
3369
+ onMoveLeft,
3370
+ onMoveRight
3371
+ }) {
3372
+ const [editing, setEditing] = useState4(false);
3373
+ if (editing) {
3374
+ return /* @__PURE__ */ jsx6(
3375
+ Input3,
3376
+ {
3377
+ autoFocus: true,
3378
+ defaultValue: group.label,
3379
+ className: "h-7 w-32 text-sm",
3380
+ onBlur: (e) => {
3381
+ const next = e.target.value.trim();
3382
+ if (next && next !== group.label) onRename(next);
3383
+ setEditing(false);
3384
+ },
3385
+ onKeyDown: (e) => {
3386
+ if (e.key === "Enter") e.currentTarget.blur();
3387
+ if (e.key === "Escape") setEditing(false);
3388
+ }
3389
+ }
3390
+ );
3391
+ }
3392
+ const fieldCount = group.rows.reduce((n, r) => n + r.fields.length, 0);
3393
+ return /* @__PURE__ */ jsxs5(
3394
+ "span",
3395
+ {
3396
+ className: cn2(
3397
+ "group/group inline-flex shrink-0 items-center gap-1 rounded-md border pl-2 pr-1 py-0.5 text-sm",
3398
+ active ? "bg-accent border-accent-foreground/20" : "hover:bg-muted"
3399
+ ),
3400
+ children: [
3401
+ /* @__PURE__ */ jsxs5(
3402
+ "button",
3403
+ {
3404
+ type: "button",
3405
+ role: "group",
3406
+ "aria-selected": active,
3407
+ className: "inline-flex items-center gap-1 py-0.5",
3408
+ onClick: onSelect,
3409
+ onDoubleClick: () => !readOnly && setEditing(true),
3410
+ onKeyDown: (e) => {
3411
+ if (e.key === "F2" && !readOnly) {
3412
+ e.preventDefault();
3413
+ setEditing(true);
3414
+ }
3415
+ },
3416
+ title: readOnly ? group.label : `${group.label} -- F2 to rename`,
3417
+ children: [
3418
+ group.label,
3419
+ /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: fieldCount })
3420
+ ]
3421
+ }
3422
+ ),
3423
+ !readOnly && /* @__PURE__ */ jsxs5("span", { className: "flex items-center opacity-0 group-hover/group:opacity-100 focus-within:opacity-100", children: [
3424
+ onMoveLeft && /* @__PURE__ */ jsx6(
3425
+ IconButton2,
3426
+ {
3427
+ label: `Move ${group.label} left`,
3428
+ revealOnHover: true,
3429
+ onClick: onMoveLeft,
3430
+ children: /* @__PURE__ */ jsx6(ChevronLeft, {})
3431
+ }
3432
+ ),
3433
+ onMoveRight && /* @__PURE__ */ jsx6(
3434
+ IconButton2,
3435
+ {
3436
+ label: `Move ${group.label} right`,
3437
+ revealOnHover: true,
3438
+ onClick: onMoveRight,
3439
+ children: /* @__PURE__ */ jsx6(ChevronRight2, {})
3440
+ }
3441
+ ),
3442
+ /* @__PURE__ */ jsx6(
3443
+ IconButton2,
3444
+ {
3445
+ label: `Remove ${group.label}`,
3446
+ tone: "destructive",
3447
+ revealOnHover: true,
3448
+ onClick: onRemove,
3449
+ children: /* @__PURE__ */ jsx6(Trash22, {})
3450
+ }
3451
+ )
3452
+ ] })
3453
+ ]
3454
+ }
3455
+ );
3456
+ }
1567
3457
  var WIDTH_LABELS = {
1568
3458
  full: "Full",
1569
3459
  sm: "Small",
@@ -1584,9 +3474,10 @@ function Chip({
1584
3474
  onDragEnd,
1585
3475
  width,
1586
3476
  onWidth,
1587
- widthDisabled
3477
+ widthDisabled,
3478
+ muted = false
1588
3479
  }) {
1589
- return /* @__PURE__ */ jsxs3(
3480
+ return /* @__PURE__ */ jsxs5(
1590
3481
  "div",
1591
3482
  {
1592
3483
  draggable: !readOnly,
@@ -1596,11 +3487,14 @@ function Chip({
1596
3487
  onDragStart();
1597
3488
  },
1598
3489
  onDragEnd,
1599
- className: "group/chip flex items-center gap-1 rounded-md border bg-background px-2 py-1 text-sm cursor-grab active:cursor-grabbing",
3490
+ className: cn2(
3491
+ "group/chip flex items-center gap-1 rounded-md border bg-background px-2 py-1 text-sm cursor-grab active:cursor-grabbing",
3492
+ muted && "border-dashed text-muted-foreground"
3493
+ ),
1600
3494
  children: [
1601
- /* @__PURE__ */ jsx4(GripVertical, { className: "h-3.5 w-3.5 text-muted-foreground/60 shrink-0" }),
1602
- /* @__PURE__ */ jsx4("span", { className: "truncate", children: label }),
1603
- onWidth && /* @__PURE__ */ jsx4(
3495
+ /* @__PURE__ */ jsx6(GripVertical, { className: "h-3.5 w-3.5 text-muted-foreground/60 shrink-0" }),
3496
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: label }),
3497
+ onWidth && /* @__PURE__ */ jsx6(
1604
3498
  "select",
1605
3499
  {
1606
3500
  value: width ?? "full",
@@ -1608,10 +3502,10 @@ function Chip({
1608
3502
  disabled: readOnly || widthDisabled,
1609
3503
  title: widthDisabled ? "Overridden by raw CSS columns" : "Column width",
1610
3504
  className: "h-6 rounded border bg-background px-1 text-xs text-muted-foreground disabled:opacity-50",
1611
- children: ["full", "sm", "md", "lg"].map((w) => /* @__PURE__ */ jsx4("option", { value: w, title: WIDTH_LABELS[w], children: WIDTH_SHORT[w] }, w))
3505
+ children: ["full", "sm", "md", "lg"].map((w) => /* @__PURE__ */ jsx6("option", { value: w, title: WIDTH_LABELS[w], children: WIDTH_SHORT[w] }, w))
1612
3506
  }
1613
3507
  ),
1614
- onRemove && !readOnly && /* @__PURE__ */ jsx4(IconButton2, { label: "Remove from row", onClick: onRemove, children: /* @__PURE__ */ jsx4(Trash22, {}) })
3508
+ onRemove && !readOnly && /* @__PURE__ */ jsx6(IconButton2, { label: "Remove from row", onClick: onRemove, children: /* @__PURE__ */ jsx6(Trash22, {}) })
1615
3509
  ]
1616
3510
  }
1617
3511
  );
@@ -1634,48 +3528,48 @@ function RowCard({
1634
3528
  onWidth,
1635
3529
  onArrange
1636
3530
  }) {
1637
- const [over, setOver] = useState3(false);
3531
+ const [over, setOver] = useState4(false);
1638
3532
  const isFlow = row.arrange === "flow";
1639
- return /* @__PURE__ */ jsxs3("div", { className: "rounded-lg border p-3", children: [
1640
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 mb-2", children: [
1641
- /* @__PURE__ */ jsxs3("span", { className: "heading-meta", children: [
3533
+ return /* @__PURE__ */ jsxs5("div", { className: "rounded-lg border p-3", children: [
3534
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 mb-2", children: [
3535
+ /* @__PURE__ */ jsxs5("span", { className: "heading-meta", children: [
1642
3536
  "Row ",
1643
3537
  index + 1
1644
3538
  ] }),
1645
- !readOnly && /* @__PURE__ */ jsxs3("span", { className: "ml-auto flex gap-0.5", children: [
1646
- /* @__PURE__ */ jsx4(
3539
+ !readOnly && /* @__PURE__ */ jsxs5("span", { className: "ml-auto flex gap-0.5", children: [
3540
+ /* @__PURE__ */ jsx6(
1647
3541
  IconButton2,
1648
3542
  {
1649
3543
  label: "Move row up",
1650
3544
  disabled: index === 0,
1651
3545
  onClick: onMoveUp,
1652
- children: /* @__PURE__ */ jsx4(ChevronUp2, {})
3546
+ children: /* @__PURE__ */ jsx6(ChevronUp2, {})
1653
3547
  }
1654
3548
  ),
1655
- /* @__PURE__ */ jsx4(
3549
+ /* @__PURE__ */ jsx6(
1656
3550
  IconButton2,
1657
3551
  {
1658
3552
  label: "Move row down",
1659
3553
  disabled: index === total - 1,
1660
3554
  onClick: onMoveDown,
1661
- children: /* @__PURE__ */ jsx4(ChevronDown2, {})
3555
+ children: /* @__PURE__ */ jsx6(ChevronDown2, {})
1662
3556
  }
1663
3557
  ),
1664
- /* @__PURE__ */ jsx4(
3558
+ /* @__PURE__ */ jsx6(
1665
3559
  IconButton2,
1666
3560
  {
1667
3561
  label: "Delete row",
1668
3562
  tone: "destructive",
1669
3563
  onClick: onDelete,
1670
- children: /* @__PURE__ */ jsx4(Trash22, {})
3564
+ children: /* @__PURE__ */ jsx6(Trash22, {})
1671
3565
  }
1672
3566
  )
1673
3567
  ] })
1674
3568
  ] }),
1675
- /* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center gap-2", children: [
1676
- /* @__PURE__ */ jsx4("label", { className: "text-xs text-muted-foreground w-16", children: "Group" }),
1677
- /* @__PURE__ */ jsx4(
1678
- Input2,
3569
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3570
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Group" }),
3571
+ /* @__PURE__ */ jsx6(
3572
+ Input3,
1679
3573
  {
1680
3574
  value: row.group ?? "",
1681
3575
  onChange: (e) => onGroup(e.target.value),
@@ -1685,9 +3579,9 @@ function RowCard({
1685
3579
  }
1686
3580
  )
1687
3581
  ] }),
1688
- /* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center gap-2", children: [
1689
- /* @__PURE__ */ jsx4("label", { className: "text-xs text-muted-foreground w-16", children: "Arrange" }),
1690
- /* @__PURE__ */ jsxs3(
3582
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3583
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Arrange" }),
3584
+ /* @__PURE__ */ jsxs5(
1691
3585
  "select",
1692
3586
  {
1693
3587
  value: isFlow ? "flow" : "grid",
@@ -1696,13 +3590,13 @@ function RowCard({
1696
3590
  title: "Grid: sized columns. Inline: fields at natural width, wrapping.",
1697
3591
  className: "h-7 rounded-md border bg-background px-2 text-sm text-foreground disabled:opacity-50",
1698
3592
  children: [
1699
- /* @__PURE__ */ jsx4("option", { value: "grid", children: "Grid (columns)" }),
1700
- /* @__PURE__ */ jsx4("option", { value: "flow", children: "Inline (natural width)" })
3593
+ /* @__PURE__ */ jsx6("option", { value: "grid", children: "Grid (columns)" }),
3594
+ /* @__PURE__ */ jsx6("option", { value: "flow", children: "Inline (natural width)" })
1701
3595
  ]
1702
3596
  }
1703
3597
  )
1704
3598
  ] }),
1705
- /* @__PURE__ */ jsx4(
3599
+ /* @__PURE__ */ jsx6(
1706
3600
  "div",
1707
3601
  {
1708
3602
  className: `min-h-10 rounded-md border border-dashed p-2 flex flex-wrap gap-1 ${over && dragActive ? "border-primary bg-accent" : "border-border"}`,
@@ -1717,7 +3611,7 @@ function RowCard({
1717
3611
  setOver(false);
1718
3612
  onDropKey(e.dataTransfer.getData(DRAG_MIME) || null);
1719
3613
  },
1720
- children: row.fields.length === 0 ? /* @__PURE__ */ jsx4("span", { className: "text-xs text-muted-foreground p-1", children: "Drop properties here" }) : row.fields.map((key, cell) => /* @__PURE__ */ jsx4(
3614
+ children: row.fields.length === 0 ? /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground p-1", children: "Drop properties here" }) : row.fields.map((key, cell) => /* @__PURE__ */ jsx6(
1721
3615
  Chip,
1722
3616
  {
1723
3617
  label: key,
@@ -1733,12 +3627,12 @@ function RowCard({
1733
3627
  ))
1734
3628
  }
1735
3629
  ),
1736
- !isFlow && /* @__PURE__ */ jsxs3("details", { className: "mt-2", children: [
1737
- /* @__PURE__ */ jsx4("summary", { className: "cursor-pointer text-xs text-muted-foreground", children: "Advanced: raw CSS columns" }),
1738
- /* @__PURE__ */ jsxs3("div", { className: "mt-2 flex items-center gap-2", children: [
1739
- /* @__PURE__ */ jsx4("label", { className: "text-xs text-muted-foreground w-16", children: "Columns" }),
1740
- /* @__PURE__ */ jsx4(
1741
- Input2,
3630
+ !isFlow && /* @__PURE__ */ jsxs5("details", { className: "mt-2", children: [
3631
+ /* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-xs text-muted-foreground", children: "Advanced: raw CSS columns" }),
3632
+ /* @__PURE__ */ jsxs5("div", { className: "mt-2 flex items-center gap-2", children: [
3633
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Columns" }),
3634
+ /* @__PURE__ */ jsx6(
3635
+ Input3,
1742
3636
  {
1743
3637
  value: row.columns ?? "",
1744
3638
  onChange: (e) => onColumns(e.target.value),
@@ -1748,9 +3642,9 @@ function RowCard({
1748
3642
  }
1749
3643
  )
1750
3644
  ] }),
1751
- /* @__PURE__ */ jsxs3("p", { className: "mt-1 pl-[4.5rem] text-xs text-muted-foreground", children: [
3645
+ /* @__PURE__ */ jsxs5("p", { className: "mt-1 pl-[4.5rem] text-xs text-muted-foreground", children: [
1752
3646
  "Raw ",
1753
- /* @__PURE__ */ jsx4("span", { className: "font-mono", children: "grid-template-columns" }),
3647
+ /* @__PURE__ */ jsx6("span", { className: "font-mono", children: "grid-template-columns" }),
1754
3648
  ". When set, it overrides the per-property widths. Blank = use widths."
1755
3649
  ] })
1756
3650
  ] })
@@ -1758,21 +3652,44 @@ function RowCard({
1758
3652
  }
1759
3653
 
1760
3654
  // src/preview/CapabilitiesPanel.tsx
1761
- import { useEffect as useEffect2, useState as useState4 } from "react";
3655
+ import { useMemo as useMemo6 } from "react";
3656
+
3657
+ // src/preview/SchemaSourcePanel.tsx
3658
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState5 } from "react";
3659
+ import { Check, Copy } from "lucide-react";
1762
3660
  import { Button as Button2 } from "@petrarca/sonnet-ui";
1763
3661
  import { JsonEditor } from "@petrarca/sonnet-ui/json-editor";
1764
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1765
- function CapabilitiesPanel({
1766
- capabilities,
1767
- onApply
3662
+ import { copyTextToClipboard } from "@petrarca/sonnet-core";
3663
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3664
+ function SchemaSourcePanel({
3665
+ value,
3666
+ editable,
3667
+ onApply,
3668
+ validate
1768
3669
  }) {
1769
- const [text, setText] = useState4(
1770
- () => JSON.stringify(capabilities ?? {}, null, 2)
1771
- );
1772
- const [error, setError] = useState4(null);
1773
- useEffect2(() => {
1774
- setText(JSON.stringify(capabilities ?? {}, null, 2));
1775
- }, [capabilities]);
3670
+ const [text, setText] = useState5(value);
3671
+ const [error, setError] = useState5(null);
3672
+ const [copied, setCopied] = useState5(false);
3673
+ const [dirty, setDirty] = useState5(false);
3674
+ const lastValueRef = useRef3(value);
3675
+ useEffect3(() => {
3676
+ if (value === lastValueRef.current) return;
3677
+ if (!dirty) {
3678
+ lastValueRef.current = value;
3679
+ setText(value);
3680
+ setError(null);
3681
+ }
3682
+ }, [value, dirty]);
3683
+ const handleEditorChange = (next) => {
3684
+ setText(next);
3685
+ setDirty(next !== value);
3686
+ };
3687
+ const handleCopy = () => {
3688
+ void copyTextToClipboard(editable ? text : value).then(() => {
3689
+ setCopied(true);
3690
+ setTimeout(() => setCopied(false), 1500);
3691
+ });
3692
+ };
1776
3693
  const handleApply = () => {
1777
3694
  let parsed;
1778
3695
  try {
@@ -1781,81 +3698,121 @@ function CapabilitiesPanel({
1781
3698
  setError("Invalid JSON.");
1782
3699
  return;
1783
3700
  }
1784
- const result = validateCapabilities(parsed);
1785
- if (!result.ok) {
1786
- setError(result.errors.join("; "));
3701
+ const validationError = validate?.(parsed);
3702
+ if (validationError) {
3703
+ setError(validationError);
1787
3704
  return;
1788
3705
  }
1789
3706
  setError(null);
1790
- onApply?.(result.value);
3707
+ setDirty(false);
3708
+ onApply(parsed);
1791
3709
  };
1792
- return /* @__PURE__ */ jsxs4("div", { className: "absolute inset-0 flex flex-col gap-2", children: [
1793
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1794
- /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: "Edit the capability profile and apply it live." }),
1795
- /* @__PURE__ */ jsx5(
3710
+ return /* @__PURE__ */ jsxs6("div", { className: "absolute inset-0 rounded-md border overflow-hidden", children: [
3711
+ /* @__PURE__ */ jsxs6("div", { className: "absolute right-2 top-2 z-10 flex items-center gap-2", children: [
3712
+ error && /* @__PURE__ */ jsx7("span", { className: "rounded bg-destructive px-2 py-1 text-xs font-medium text-destructive-foreground", children: error }),
3713
+ editable && /* @__PURE__ */ jsx7(
1796
3714
  Button2,
1797
3715
  {
1798
3716
  type: "button",
1799
3717
  size: "sm",
1800
3718
  onClick: handleApply,
1801
- className: "ml-auto",
3719
+ disabled: !dirty,
1802
3720
  children: "Apply"
1803
3721
  }
3722
+ ),
3723
+ /* @__PURE__ */ jsxs6(
3724
+ Button2,
3725
+ {
3726
+ type: "button",
3727
+ variant: "outline",
3728
+ size: "sm",
3729
+ onClick: handleCopy,
3730
+ className: "gap-1.5",
3731
+ children: [
3732
+ copied ? /* @__PURE__ */ jsx7(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx7(Copy, { className: "h-3.5 w-3.5" }),
3733
+ copied ? "Copied" : "Copy"
3734
+ ]
3735
+ }
1804
3736
  )
1805
3737
  ] }),
1806
- error && /* @__PURE__ */ jsx5("p", { className: "text-sm font-medium text-destructive", children: error }),
1807
- /* @__PURE__ */ jsx5("div", { className: "relative flex-1 min-h-0 rounded-md border overflow-hidden", children: /* @__PURE__ */ jsx5(
3738
+ editable ? /* @__PURE__ */ jsx7(
1808
3739
  JsonEditor,
1809
3740
  {
1810
3741
  value: text,
1811
3742
  editablePaths: ["/"],
1812
3743
  fillContainer: true,
1813
- onChange: setText
3744
+ onChange: handleEditorChange
1814
3745
  }
1815
- ) })
3746
+ ) : /* @__PURE__ */ jsx7(
3747
+ JsonEditor,
3748
+ {
3749
+ value,
3750
+ editablePaths: [],
3751
+ fillContainer: true
3752
+ },
3753
+ value
3754
+ )
1816
3755
  ] });
1817
3756
  }
1818
3757
 
1819
- // src/preview/PreviewPane.tsx
1820
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1821
- function ReadonlyJson({ value }) {
1822
- const [copied, setCopied] = useState5(false);
1823
- const handleCopy = () => {
1824
- void copyTextToClipboard(value).then(() => {
1825
- setCopied(true);
1826
- setTimeout(() => setCopied(false), 1500);
1827
- });
1828
- };
1829
- return /* @__PURE__ */ jsxs5("div", { className: "absolute inset-0 rounded-md border overflow-hidden", children: [
1830
- /* @__PURE__ */ jsxs5(
1831
- Button3,
1832
- {
1833
- type: "button",
1834
- variant: "outline",
1835
- size: "sm",
1836
- onClick: handleCopy,
1837
- className: "absolute right-2 top-2 z-10 gap-1.5",
1838
- children: [
1839
- copied ? /* @__PURE__ */ jsx6(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx6(Copy, { className: "h-3.5 w-3.5" }),
1840
- copied ? "Copied" : "Copy"
1841
- ]
3758
+ // src/preview/CapabilitiesPanel.tsx
3759
+ import { jsx as jsx8 } from "react/jsx-runtime";
3760
+ function CapabilitiesPanel({
3761
+ capabilities,
3762
+ onApply
3763
+ }) {
3764
+ const value = useMemo6(
3765
+ () => JSON.stringify(capabilities ?? {}, null, 2),
3766
+ [capabilities]
3767
+ );
3768
+ return /* @__PURE__ */ jsx8(
3769
+ SchemaSourcePanel,
3770
+ {
3771
+ value,
3772
+ editable: true,
3773
+ validate: (parsed) => {
3774
+ const result = validateCapabilities(parsed);
3775
+ return result.ok ? null : result.errors.join("; ");
3776
+ },
3777
+ onApply: (parsed) => {
3778
+ const result = validateCapabilities(parsed);
3779
+ if (result.ok) onApply?.(result.value);
1842
3780
  }
1843
- ),
1844
- /* @__PURE__ */ jsx6(JsonEditor2, { value, editablePaths: [], fillContainer: true }, value)
1845
- ] });
3781
+ }
3782
+ );
3783
+ }
3784
+
3785
+ // src/preview/PreviewPane.tsx
3786
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3787
+ function validateRootSchema(parsed) {
3788
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3789
+ return "Schema must be a JSON object.";
3790
+ }
3791
+ const s = parsed;
3792
+ if (s.type !== "object") return 'Root schema must have type: "object".';
3793
+ if (typeof s.properties !== "object" || s.properties === null) {
3794
+ return "Root schema must have a properties object.";
3795
+ }
3796
+ return null;
3797
+ }
3798
+ function validateUiSchema(parsed) {
3799
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3800
+ return "UI Schema must be a JSON object.";
3801
+ }
3802
+ return null;
1846
3803
  }
1847
3804
  function PreviewTabList({ flags }) {
1848
- return /* @__PURE__ */ jsxs5(TabsList, { className: "mb-3", children: [
1849
- flags.showForm && /* @__PURE__ */ jsx6(TabsTrigger, { value: "form", children: "Form" }),
1850
- flags.isFull && flags.showForm && /* @__PURE__ */ jsx6(TabsTrigger, { value: "layout", children: "Form Layout" }),
1851
- flags.showTable && /* @__PURE__ */ jsx6(TabsTrigger, { value: "table", children: "Table" }),
1852
- /* @__PURE__ */ jsx6(TabsSeparator, {}),
1853
- flags.showJson && /* @__PURE__ */ jsx6(TabsTrigger, { value: "schema", children: "JSON Schema" }),
1854
- flags.isFull && /* @__PURE__ */ jsx6(TabsTrigger, { value: "ui", children: "UI Schema" }),
1855
- flags.isFull && /* @__PURE__ */ jsx6(TabsTrigger, { value: "combined", children: "Combined" }),
1856
- flags.showCapabilities && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1857
- /* @__PURE__ */ jsx6(TabsSeparator, {}),
1858
- /* @__PURE__ */ jsx6(TabsTrigger, { value: "capabilities", children: "Capabilities" })
3805
+ return /* @__PURE__ */ jsxs7(TabsList, { className: "mb-3", children: [
3806
+ flags.showForm && /* @__PURE__ */ jsx9(TabsTrigger, { value: "form", children: "Form" }),
3807
+ flags.isFull && flags.showForm && /* @__PURE__ */ jsx9(TabsTrigger, { value: "layout", children: "Form Layout" }),
3808
+ flags.showTable && /* @__PURE__ */ jsx9(TabsTrigger, { value: "table", children: "Table" }),
3809
+ /* @__PURE__ */ jsx9(TabsSeparator, {}),
3810
+ flags.showJson && /* @__PURE__ */ jsx9(TabsTrigger, { value: "schema", children: "JSON Schema" }),
3811
+ flags.isFull && /* @__PURE__ */ jsx9(TabsTrigger, { value: "ui", children: "UI Schema" }),
3812
+ flags.isFull && /* @__PURE__ */ jsx9(TabsTrigger, { value: "combined", children: "Combined" }),
3813
+ flags.showCapabilities && /* @__PURE__ */ jsxs7(Fragment4, { children: [
3814
+ /* @__PURE__ */ jsx9(TabsSeparator, {}),
3815
+ /* @__PURE__ */ jsx9(TabsTrigger, { value: "capabilities", children: "Capabilities" })
1859
3816
  ] })
1860
3817
  ] });
1861
3818
  }
@@ -1873,22 +3830,22 @@ function PreviewPane({
1873
3830
  showJson: caps.previews.includes("json"),
1874
3831
  showCapabilities: caps.showCapabilities
1875
3832
  };
1876
- const [data, setData] = useState5(
3833
+ const [data, setData] = useState6(
1877
3834
  () => extractSchemaDefaults(schema)
1878
3835
  );
1879
- const schemaText = useMemo4(() => JSON.stringify(schema, null, 2), [schema]);
1880
- const uiSchemaText = useMemo4(
3836
+ const schemaText = useMemo7(() => JSON.stringify(schema, null, 2), [schema]);
3837
+ const uiSchemaText = useMemo7(
1881
3838
  () => JSON.stringify(uiSchema ?? {}, null, 2),
1882
3839
  [uiSchema]
1883
3840
  );
1884
- const embeddedText = useMemo4(
3841
+ const embeddedText = useMemo7(
1885
3842
  () => JSON.stringify(embedUiSchema(schema, uiSchema ?? {}), null, 2),
1886
3843
  [schema, uiSchema]
1887
3844
  );
1888
3845
  const defaultTab = flags.showForm ? "form" : flags.showTable ? "table" : "schema";
1889
- return /* @__PURE__ */ jsxs5(Tabs, { defaultValue: defaultTab, className: "flex flex-col h-full min-h-0", children: [
1890
- /* @__PURE__ */ jsx6(PreviewTabList, { flags }),
1891
- flags.showForm && /* @__PURE__ */ jsx6(TabsContent, { value: "form", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx6(
3846
+ return /* @__PURE__ */ jsxs7(Tabs, { defaultValue: defaultTab, className: "flex flex-col h-full min-h-0", children: [
3847
+ /* @__PURE__ */ jsx9(PreviewTabList, { flags }),
3848
+ flags.showForm && /* @__PURE__ */ jsx9(TabsContent, { value: "form", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx9(
1892
3849
  JsonSchemaFormRenderer3,
1893
3850
  {
1894
3851
  schema,
@@ -1899,11 +3856,12 @@ function PreviewPane({
1899
3856
  showExtraProperties: false
1900
3857
  }
1901
3858
  ) }),
1902
- flags.isFull && flags.showForm && /* @__PURE__ */ jsx6(TabsContent, { value: "layout", className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx6(FormLayoutDesigner, { state }) }),
1903
- flags.showTable && /* @__PURE__ */ jsx6(TabsContent, { value: "table", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx6(TablePreview, { schema }) }),
1904
- /* @__PURE__ */ jsx6(
3859
+ flags.isFull && flags.showForm && /* @__PURE__ */ jsx9(TabsContent, { value: "layout", className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx9(FormLayoutDesigner, { state }) }),
3860
+ flags.showTable && /* @__PURE__ */ jsx9(TabsContent, { value: "table", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx9(TablePreview, { schema }) }),
3861
+ /* @__PURE__ */ jsx9(
1905
3862
  SourceTabPanels,
1906
3863
  {
3864
+ state,
1907
3865
  flags,
1908
3866
  schemaText,
1909
3867
  uiSchemaText,
@@ -1915,6 +3873,7 @@ function PreviewPane({
1915
3873
  ] });
1916
3874
  }
1917
3875
  function SourceTabPanels({
3876
+ state,
1918
3877
  flags,
1919
3878
  schemaText,
1920
3879
  uiSchemaText,
@@ -1922,11 +3881,36 @@ function SourceTabPanels({
1922
3881
  capabilities,
1923
3882
  onCapabilitiesChange
1924
3883
  }) {
1925
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1926
- flags.showJson && /* @__PURE__ */ jsx6(TabsContent, { value: "schema", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx6(ReadonlyJson, { value: schemaText }) }),
1927
- flags.isFull && /* @__PURE__ */ jsx6(TabsContent, { value: "ui", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx6(ReadonlyJson, { value: uiSchemaText }) }),
1928
- flags.isFull && /* @__PURE__ */ jsx6(TabsContent, { value: "combined", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx6(ReadonlyJson, { value: embeddedText }) }),
1929
- flags.showCapabilities && /* @__PURE__ */ jsx6(TabsContent, { value: "capabilities", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx6(
3884
+ const canEditSource = !state.capabilities.locked && !state.capabilities.scoped;
3885
+ return /* @__PURE__ */ jsxs7(Fragment4, { children: [
3886
+ flags.showJson && /* @__PURE__ */ jsx9(TabsContent, { value: "schema", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3887
+ SchemaSourcePanel,
3888
+ {
3889
+ value: schemaText,
3890
+ editable: canEditSource,
3891
+ validate: validateRootSchema,
3892
+ onApply: (parsed) => state.replaceSchema(parsed)
3893
+ }
3894
+ ) }),
3895
+ flags.isFull && /* @__PURE__ */ jsx9(TabsContent, { value: "ui", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3896
+ SchemaSourcePanel,
3897
+ {
3898
+ value: uiSchemaText,
3899
+ editable: canEditSource,
3900
+ validate: validateUiSchema,
3901
+ onApply: (parsed) => state.replaceUiSchema(parsed)
3902
+ }
3903
+ ) }),
3904
+ flags.isFull && /* @__PURE__ */ jsx9(TabsContent, { value: "combined", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3905
+ SchemaSourcePanel,
3906
+ {
3907
+ value: embeddedText,
3908
+ editable: false,
3909
+ onApply: () => {
3910
+ }
3911
+ }
3912
+ ) }),
3913
+ flags.showCapabilities && /* @__PURE__ */ jsx9(TabsContent, { value: "capabilities", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
1930
3914
  CapabilitiesPanel,
1931
3915
  {
1932
3916
  capabilities,
@@ -1937,20 +3921,27 @@ function SourceTabPanels({
1937
3921
  }
1938
3922
 
1939
3923
  // src/inspector/Inspector.tsx
1940
- import { useEffect as useEffect4, useMemo as useMemo8, useRef as useRef4, useState as useState7 } from "react";
3924
+ import { useEffect as useEffect5, useMemo as useMemo14, useRef as useRef6, useState as useState8 } from "react";
1941
3925
  import {
1942
3926
  Tabs as Tabs2,
1943
3927
  TabsList as TabsList2,
1944
3928
  TabsTrigger as TabsTrigger2,
1945
3929
  TabsContent as TabsContent2,
1946
- Input as Input4,
3930
+ Input as Input5,
1947
3931
  IconButton as IconButton4
1948
3932
  } from "@petrarca/sonnet-ui";
1949
3933
  import { Pencil, Check as Check2, X } from "lucide-react";
1950
3934
 
1951
3935
  // src/inspector/DataTab.tsx
1952
- import { useMemo as useMemo5, useRef as useRef3 } from "react";
3936
+ import { useMemo as useMemo8, useRef as useRef4 } from "react";
1953
3937
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer4 } from "@petrarca/sonnet-forms";
3938
+ import {
3939
+ Select,
3940
+ SelectTrigger,
3941
+ SelectValue,
3942
+ SelectContent,
3943
+ SelectItem
3944
+ } from "@petrarca/sonnet-ui";
1954
3945
 
1955
3946
  // src/inspector/metaSchemas.ts
1956
3947
  var PROPERTY_TYPES = [
@@ -1959,8 +3950,20 @@ var PROPERTY_TYPES = [
1959
3950
  "integer",
1960
3951
  "boolean",
1961
3952
  "object",
1962
- "array"
3953
+ "array",
3954
+ "null"
1963
3955
  ];
3956
+ var PROPERTY_TYPE_CHOICES = [...PROPERTY_TYPES, "oneOf"];
3957
+ var PROPERTY_TYPE_NAMES = {
3958
+ string: "String",
3959
+ number: "Number",
3960
+ integer: "Integer",
3961
+ boolean: "Boolean",
3962
+ object: "Object",
3963
+ array: "Array",
3964
+ null: "Display (text only)",
3965
+ oneOf: "One of (variant)"
3966
+ };
1964
3967
  var STRING_FORMATS = [
1965
3968
  "",
1966
3969
  "email",
@@ -1995,37 +3998,100 @@ function validationProps(type) {
1995
3998
  return {};
1996
3999
  }
1997
4000
  }
1998
- function buildDataMetaSchema(type) {
4001
+ var BRANCH_META_TYPE = "branch";
4002
+ function typeTraits(type) {
4003
+ const isDisplay = type === "null";
4004
+ const isOneOf3 = type === "oneOf";
1999
4005
  const isContainer2 = type === "object" || type === "array";
4006
+ return {
4007
+ isDisplay,
4008
+ isOneOf: isOneOf3,
4009
+ // Containers, display (null), and oneOf have no meaningful default value.
4010
+ noDefault: isContainer2 || isDisplay || isOneOf3,
4011
+ // Display notes and oneOf unions are never "required" on their own.
4012
+ noRequired: isDisplay || isOneOf3,
4013
+ // A fixed value (const) is only meaningful on scalar properties.
4014
+ isScalar: type === "string" || type === "number" || type === "integer" || type === "boolean"
4015
+ };
4016
+ }
4017
+ function buildReferenceMetaSchema() {
4018
+ return {
4019
+ title: "",
4020
+ type: "object",
4021
+ properties: {
4022
+ title: { type: "string", title: "Label (title)" },
4023
+ description: {
4024
+ type: "string",
4025
+ title: "Description",
4026
+ "x-ui-widget": "textarea"
4027
+ },
4028
+ required: { type: "boolean", title: "Required" },
4029
+ default: { type: "string", title: "Default" }
4030
+ }
4031
+ };
4032
+ }
4033
+ function buildDataMetaSchema(type) {
4034
+ if (type === BRANCH_META_TYPE) {
4035
+ return {
4036
+ title: "",
4037
+ type: "object",
4038
+ properties: {
4039
+ title: { type: "string", title: "Branch label (title)" }
4040
+ }
4041
+ };
4042
+ }
4043
+ const { isDisplay, noDefault, noRequired, isScalar, isOneOf: isOneOf3 } = typeTraits(type);
2000
4044
  const properties = {
2001
- type: { type: "string", title: "Type", enum: [...PROPERTY_TYPES] },
2002
- title: { type: "string", title: "Label (title)" },
4045
+ type: {
4046
+ type: "string",
4047
+ title: "Type",
4048
+ enum: [...PROPERTY_TYPE_CHOICES],
4049
+ "x-ui-options": { enumNames: PROPERTY_TYPE_NAMES }
4050
+ },
4051
+ title: { type: "string", title: isDisplay ? "Text" : "Label (title)" },
2003
4052
  description: {
2004
4053
  type: "string",
2005
4054
  title: "Description",
2006
4055
  "x-ui-widget": "textarea"
2007
4056
  },
2008
- required: { type: "boolean", title: "Required" },
2009
- ...isContainer2 ? {} : { default: { type: "string", title: "Default" } },
4057
+ ...isOneOf3 ? {
4058
+ discriminator: {
4059
+ type: "string",
4060
+ title: "Discriminator property",
4061
+ "x-ui-description": "Shared property whose const value tags each branch. Auto-detected when blank."
4062
+ }
4063
+ } : {},
4064
+ ...noRequired ? {} : { required: { type: "boolean", title: "Required" } },
4065
+ ...noDefault ? {} : { default: { type: "string", title: "Default" } },
4066
+ ...isScalar ? {
4067
+ const: {
4068
+ type: "string",
4069
+ title: "Const (fixed value)",
4070
+ "x-ui-description": "Fixed value this property must equal. Leave blank for none."
4071
+ }
4072
+ } : {},
2010
4073
  ...validationProps(type)
2011
4074
  };
2012
4075
  return { title: "", type: "object", properties };
2013
4076
  }
2014
- function propertyToDataValues(prop, required) {
4077
+ function propertyToDataValues(prop2, required) {
2015
4078
  return {
2016
- type: prop.type ?? "string",
2017
- title: prop.title,
2018
- description: prop.description,
2019
- default: prop.default,
4079
+ type: prop2.oneOf ? "oneOf" : prop2.type ?? "string",
4080
+ title: prop2.title,
4081
+ description: prop2.description,
4082
+ default: prop2.default,
4083
+ // const may be any JSON type; the Data-tab field edits it as a string.
4084
+ const: prop2.const == null ? void 0 : String(prop2.const),
2020
4085
  required,
2021
- minLength: prop.minLength,
2022
- maxLength: prop.maxLength,
2023
- pattern: prop.pattern,
2024
- format: prop.format,
2025
- minimum: prop.minimum,
2026
- maximum: prop.maximum,
2027
- minItems: prop.minItems,
2028
- maxItems: prop.maxItems
4086
+ minLength: prop2.minLength,
4087
+ maxLength: prop2.maxLength,
4088
+ pattern: prop2.pattern,
4089
+ format: prop2.format,
4090
+ minimum: prop2.minimum,
4091
+ maximum: prop2.maximum,
4092
+ minItems: prop2.minItems,
4093
+ maxItems: prop2.maxItems,
4094
+ discriminator: prop2.discriminator?.propertyName
2029
4095
  };
2030
4096
  }
2031
4097
  function dataValuesToPatch(values) {
@@ -2043,6 +4109,7 @@ function dataValuesToPatch(values) {
2043
4109
  setStr("title", values.title);
2044
4110
  setStr("description", values.description);
2045
4111
  setStr("default", values.default);
4112
+ setStr("const", values.const);
2046
4113
  setStr("format", values.format);
2047
4114
  setStr("pattern", values.pattern);
2048
4115
  setNum("minLength", values.minLength);
@@ -2051,28 +4118,31 @@ function dataValuesToPatch(values) {
2051
4118
  setNum("maximum", values.maximum);
2052
4119
  setNum("minItems", values.minItems);
2053
4120
  setNum("maxItems", values.maxItems);
4121
+ if (typeof values.discriminator === "string" && values.discriminator.length) {
4122
+ patch.discriminator = { propertyName: values.discriminator };
4123
+ } else {
4124
+ patch.discriminator = void 0;
4125
+ }
2054
4126
  return patch;
2055
4127
  }
2056
4128
 
2057
4129
  // src/inspector/DataTab.tsx
2058
- import { jsx as jsx7 } from "react/jsx-runtime";
4130
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2059
4131
  function DataTab({ node, state }) {
2060
- const type = node.property.type ?? "string";
2061
- const schema = useMemo5(() => buildDataMetaSchema(type), [type]);
2062
- const values = useMemo5(
4132
+ const isBranch = node.slotKind === "branch";
4133
+ const type = isBranch ? BRANCH_META_TYPE : node.property.oneOf ? "oneOf" : node.property.type ?? "string";
4134
+ const schema = useMemo8(() => buildDataMetaSchema(type), [type]);
4135
+ const values = useMemo8(
2063
4136
  () => propertyToDataValues(node.property, node.required),
2064
4137
  [node.property, node.required]
2065
4138
  );
2066
- const prevRef = useRef3(values);
4139
+ const prevRef = useRef4(values);
2067
4140
  prevRef.current = values;
2068
4141
  const handleChange = (data) => {
2069
4142
  const next = data;
2070
4143
  const prev = prevRef.current;
2071
- if (next.type !== prev.type) {
2072
- state.changePropertyType(
2073
- node.path,
2074
- next.type
2075
- );
4144
+ if (!isBranch && next.type !== prev.type) {
4145
+ state.changePropertyType(node.path, next.type);
2076
4146
  return;
2077
4147
  }
2078
4148
  if (next.required !== prev.required) {
@@ -2081,30 +4151,66 @@ function DataTab({ node, state }) {
2081
4151
  }
2082
4152
  state.updateProperty(node.path, dataValuesToPatch(next));
2083
4153
  };
2084
- return /* @__PURE__ */ jsx7(
2085
- JsonSchemaFormRenderer4,
2086
- {
2087
- schema,
2088
- data: values,
2089
- onChange: handleChange,
2090
- showActions: false,
2091
- showExtraProperties: false,
2092
- readOnly: state.readOnly
2093
- },
2094
- node.path.join(".") + ":" + type
2095
- );
4154
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
4155
+ !isBranch && /* @__PURE__ */ jsx10(ReferencePicker, { node, state, disabled: state.readOnly }),
4156
+ /* @__PURE__ */ jsx10(
4157
+ JsonSchemaFormRenderer4,
4158
+ {
4159
+ schema,
4160
+ data: values,
4161
+ onChange: handleChange,
4162
+ deferStateUpdates: true,
4163
+ showActions: false,
4164
+ showExtraProperties: false,
4165
+ readOnly: state.readOnly
4166
+ },
4167
+ pathKey(node.path) + ":" + type
4168
+ )
4169
+ ] });
4170
+ }
4171
+ function availableDefinitions(state, node) {
4172
+ const defs = state.schema.$defs ?? state.schema.definitions;
4173
+ if (!defs) return [];
4174
+ const selfName = node.path.length === 1 && node.path[0].kind === "def" ? node.path[0].name : void 0;
4175
+ return Object.keys(defs).filter((name) => name !== selfName);
4176
+ }
4177
+ function ReferencePicker({
4178
+ node,
4179
+ state,
4180
+ disabled
4181
+ }) {
4182
+ const names = availableDefinitions(state, node);
4183
+ if (names.length === 0) return null;
4184
+ const isArray = node.property.type === "array";
4185
+ const label = isArray ? "Reference a definition (array items)" : "Reference a definition";
4186
+ const placeholder = isArray ? "Make items reuse a shared definition..." : "Reuse a shared definition...";
4187
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
4188
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
4189
+ /* @__PURE__ */ jsxs8(
4190
+ Select,
4191
+ {
4192
+ value: "",
4193
+ onValueChange: (name) => state.setReference(node.path, name),
4194
+ disabled,
4195
+ children: [
4196
+ /* @__PURE__ */ jsx10(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx10(SelectValue, { placeholder }) }),
4197
+ /* @__PURE__ */ jsx10(SelectContent, { children: names.map((name) => /* @__PURE__ */ jsx10(SelectItem, { value: name, children: name }, name)) })
4198
+ ]
4199
+ }
4200
+ )
4201
+ ] });
2096
4202
  }
2097
4203
 
2098
4204
  // src/inspector/UITab.tsx
2099
- import { useMemo as useMemo6 } from "react";
4205
+ import { useMemo as useMemo9 } from "react";
2100
4206
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer5 } from "@petrarca/sonnet-forms";
2101
- import { JsonEditor as JsonEditor3 } from "@petrarca/sonnet-ui/json-editor";
4207
+ import { JsonEditor as JsonEditor2 } from "@petrarca/sonnet-ui/json-editor";
2102
4208
  import {
2103
4209
  WIDGET_OPTION_DEFS,
2104
4210
  optionDefsToSchema
2105
4211
  } from "@petrarca/sonnet-forms/widget-options";
2106
- import { FormSelect as FormSelect3 } from "@petrarca/sonnet-forms";
2107
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4212
+ import { FormSelect as FormSelect4 } from "@petrarca/sonnet-forms";
4213
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2108
4214
  var PRESENTATION_META_SCHEMA = {
2109
4215
  title: "",
2110
4216
  type: "object",
@@ -2167,14 +4273,15 @@ function PresentationEditor({
2167
4273
  }
2168
4274
  onChange(next);
2169
4275
  };
2170
- return /* @__PURE__ */ jsxs6("div", { children: [
2171
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Presentation (form overrides)" }),
2172
- /* @__PURE__ */ jsx8(
4276
+ return /* @__PURE__ */ jsxs9("div", { children: [
4277
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Presentation (form overrides)" }),
4278
+ /* @__PURE__ */ jsx11(
2173
4279
  JsonSchemaFormRenderer5,
2174
4280
  {
2175
4281
  schema: PRESENTATION_META_SCHEMA,
2176
4282
  data: values,
2177
4283
  onChange: handleChange,
4284
+ deferStateUpdates: true,
2178
4285
  showActions: false,
2179
4286
  showExtraProperties: false,
2180
4287
  readOnly
@@ -2191,14 +4298,15 @@ function OptionsEditor({
2191
4298
  onChange
2192
4299
  }) {
2193
4300
  if (optionsSchema) {
2194
- return /* @__PURE__ */ jsxs6("div", { children: [
2195
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options" }),
2196
- /* @__PURE__ */ jsx8(
4301
+ return /* @__PURE__ */ jsxs9("div", { children: [
4302
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Options" }),
4303
+ /* @__PURE__ */ jsx11(
2197
4304
  JsonSchemaFormRenderer5,
2198
4305
  {
2199
4306
  schema: optionsSchema,
2200
4307
  data: currentOptions,
2201
4308
  onChange,
4309
+ deferStateUpdates: true,
2202
4310
  showActions: false,
2203
4311
  showExtraProperties: false,
2204
4312
  readOnly
@@ -2207,10 +4315,10 @@ function OptionsEditor({
2207
4315
  )
2208
4316
  ] });
2209
4317
  }
2210
- return /* @__PURE__ */ jsxs6("div", { children: [
2211
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options (raw JSON)" }),
2212
- /* @__PURE__ */ jsx8(
2213
- JsonEditor3,
4318
+ return /* @__PURE__ */ jsxs9("div", { children: [
4319
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Options (raw JSON)" }),
4320
+ /* @__PURE__ */ jsx11(
4321
+ JsonEditor2,
2214
4322
  {
2215
4323
  value: JSON.stringify(currentOptions ?? {}, null, 2),
2216
4324
  editablePaths: readOnly ? [] : ["/"],
@@ -2231,17 +4339,17 @@ function UITab({
2231
4339
  state,
2232
4340
  widgetOptions
2233
4341
  }) {
2234
- const pathKey2 = node.path.join(".");
4342
+ const idBase = pathKey(node.path);
2235
4343
  const uiEntry = getUiEntryAtPath(state.uiSchema, node.path) ?? {};
2236
4344
  const currentWidget = typeof uiEntry["x-ui-widget"] === "string" ? uiEntry["x-ui-widget"] : null;
2237
4345
  const currentOptions = uiEntry["x-ui-options"] ?? {};
2238
4346
  const rootLayout = state.uiSchema["x-ui-layout"];
2239
4347
  const inheritedLabelPlacement = rootLayout?.labelPlacement ?? "top";
2240
- const widgetChoices = useMemo6(
4348
+ const widgetChoices = useMemo9(
2241
4349
  () => compatibleWidgets(node.property),
2242
4350
  [node.property]
2243
4351
  );
2244
- const optionsSchema = useMemo6(() => {
4352
+ const optionsSchema = useMemo9(() => {
2245
4353
  if (!currentWidget) return null;
2246
4354
  const merged = { ...WIDGET_OPTION_DEFS, ...widgetOptions ?? {} };
2247
4355
  const descriptors = merged[currentWidget];
@@ -2274,11 +4382,11 @@ function UITab({
2274
4382
  else delete next["x-ui-options"];
2275
4383
  state.setUiEntry(node.path, next);
2276
4384
  };
2277
- return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
2278
- /* @__PURE__ */ jsx8(
2279
- FormSelect3,
4385
+ return /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
4386
+ /* @__PURE__ */ jsx11(
4387
+ FormSelect4,
2280
4388
  {
2281
- id: `${pathKey2}-widget`,
4389
+ id: `${idBase}-widget`,
2282
4390
  name: "widget",
2283
4391
  label: "Widget",
2284
4392
  value: currentWidget ?? "__auto__",
@@ -2291,29 +4399,29 @@ function UITab({
2291
4399
  clearable: false
2292
4400
  }
2293
4401
  ),
2294
- currentWidget && /* @__PURE__ */ jsx8(
4402
+ currentWidget && /* @__PURE__ */ jsx11(
2295
4403
  OptionsEditor,
2296
4404
  {
2297
- widget: `${pathKey2}:${currentWidget}`,
4405
+ widget: `${idBase}:${currentWidget}`,
2298
4406
  optionsSchema,
2299
4407
  currentOptions,
2300
4408
  readOnly: state.readOnly,
2301
4409
  onChange: setOptions
2302
4410
  }
2303
4411
  ),
2304
- /* @__PURE__ */ jsx8(
4412
+ /* @__PURE__ */ jsx11(
2305
4413
  PresentationEditor,
2306
4414
  {
2307
- fieldName: pathKey2,
4415
+ fieldName: idBase,
2308
4416
  uiEntry,
2309
4417
  readOnly: state.readOnly,
2310
4418
  onChange: (entry) => state.setUiEntry(node.path, entry)
2311
4419
  }
2312
4420
  ),
2313
- /* @__PURE__ */ jsx8(
2314
- FormSelect3,
4421
+ /* @__PURE__ */ jsx11(
4422
+ FormSelect4,
2315
4423
  {
2316
- id: `${pathKey2}-labelPlacement`,
4424
+ id: `${idBase}-labelPlacement`,
2317
4425
  name: "labelPlacement",
2318
4426
  label: "Label placement",
2319
4427
  value: currentOptions.labelPlacement ?? "__inherit__",
@@ -2334,25 +4442,25 @@ function UITab({
2334
4442
  }
2335
4443
 
2336
4444
  // src/inspector/EnumEditor.tsx
2337
- import { useEffect as useEffect3, useState as useState6 } from "react";
2338
- import { Input as Input3, IconButton as IconButton3, Button as Button4 } from "@petrarca/sonnet-ui";
4445
+ import { useEffect as useEffect4, useState as useState7 } from "react";
4446
+ import { Input as Input4, IconButton as IconButton3, Button as Button3 } from "@petrarca/sonnet-ui";
2339
4447
  import { Plus as Plus3, Trash2 as Trash23 } from "lucide-react";
2340
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
4448
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2341
4449
  function EnumEditor({
2342
4450
  node,
2343
4451
  state
2344
4452
  }) {
2345
- const [rows, setRows] = useState6(
4453
+ const [rows, setRows] = useState7(
2346
4454
  () => readEnumChoices(node.property, getUiEntryAtPath(state.uiSchema, node.path))
2347
4455
  );
2348
- useEffect3(() => {
4456
+ useEffect4(() => {
2349
4457
  setRows(
2350
4458
  readEnumChoices(
2351
4459
  node.property,
2352
4460
  getUiEntryAtPath(state.uiSchema, node.path)
2353
4461
  )
2354
4462
  );
2355
- }, [node.path.join(".")]);
4463
+ }, [pathKey(node.path)]);
2356
4464
  const canEditLabels = true;
2357
4465
  const commit = (next) => {
2358
4466
  setRows(next);
@@ -2363,20 +4471,20 @@ function EnumEditor({
2363
4471
  };
2364
4472
  const removeRow = (i) => commit(rows.filter((_, idx) => idx !== i));
2365
4473
  const addRow = () => setRows([...rows, { value: "", label: "" }]);
2366
- return /* @__PURE__ */ jsxs7("div", { className: "space-y-3", children: [
2367
- /* @__PURE__ */ jsxs7("div", { children: [
2368
- /* @__PURE__ */ jsx9("p", { className: "text-sm font-medium", children: "Allowed values" }),
2369
- /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: "Define a fixed set of choices. Leave empty for free input." })
4474
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-3", children: [
4475
+ /* @__PURE__ */ jsxs10("div", { children: [
4476
+ /* @__PURE__ */ jsx12("p", { className: "text-sm font-medium", children: "Allowed values" }),
4477
+ /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", children: "Define a fixed set of choices. Leave empty for free input." })
2370
4478
  ] }),
2371
- rows.length === 0 ? /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: "No choices yet." }) : /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
2372
- canEditLabels && /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-1", children: [
2373
- /* @__PURE__ */ jsx9("span", { className: "heading-meta flex-1", children: "Value" }),
2374
- /* @__PURE__ */ jsx9("span", { className: "heading-meta flex-1", children: "Label (optional)" }),
2375
- /* @__PURE__ */ jsx9("span", { className: "w-7" })
4479
+ rows.length === 0 ? /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", children: "No choices yet." }) : /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
4480
+ canEditLabels && /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 px-1", children: [
4481
+ /* @__PURE__ */ jsx12("span", { className: "heading-meta flex-1", children: "Value" }),
4482
+ /* @__PURE__ */ jsx12("span", { className: "heading-meta flex-1", children: "Label (optional)" }),
4483
+ /* @__PURE__ */ jsx12("span", { className: "w-7" })
2376
4484
  ] }),
2377
- rows.map((row, i) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
2378
- /* @__PURE__ */ jsx9(
2379
- Input3,
4485
+ rows.map((row, i) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
4486
+ /* @__PURE__ */ jsx12(
4487
+ Input4,
2380
4488
  {
2381
4489
  value: row.value,
2382
4490
  onChange: (e) => updateRow(i, { value: e.target.value }),
@@ -2385,8 +4493,8 @@ function EnumEditor({
2385
4493
  className: "h-8 text-sm flex-1"
2386
4494
  }
2387
4495
  ),
2388
- canEditLabels && /* @__PURE__ */ jsx9(
2389
- Input3,
4496
+ canEditLabels && /* @__PURE__ */ jsx12(
4497
+ Input4,
2390
4498
  {
2391
4499
  value: row.label ?? "",
2392
4500
  onChange: (e) => updateRow(i, { label: e.target.value }),
@@ -2395,26 +4503,26 @@ function EnumEditor({
2395
4503
  className: "h-8 text-sm flex-1"
2396
4504
  }
2397
4505
  ),
2398
- !state.readOnly && /* @__PURE__ */ jsx9(
4506
+ !state.readOnly && /* @__PURE__ */ jsx12(
2399
4507
  IconButton3,
2400
4508
  {
2401
4509
  label: "Remove value",
2402
4510
  tone: "destructive",
2403
4511
  onClick: () => removeRow(i),
2404
- children: /* @__PURE__ */ jsx9(Trash23, {})
4512
+ children: /* @__PURE__ */ jsx12(Trash23, {})
2405
4513
  }
2406
4514
  )
2407
4515
  ] }, i))
2408
4516
  ] }),
2409
- !state.readOnly && /* @__PURE__ */ jsxs7(
2410
- Button4,
4517
+ !state.readOnly && /* @__PURE__ */ jsxs10(
4518
+ Button3,
2411
4519
  {
2412
4520
  variant: "outline",
2413
4521
  size: "sm",
2414
4522
  className: "gap-1.5",
2415
4523
  onClick: addRow,
2416
4524
  children: [
2417
- /* @__PURE__ */ jsx9(Plus3, { className: "h-4 w-4" }),
4525
+ /* @__PURE__ */ jsx12(Plus3, { className: "h-4 w-4" }),
2418
4526
  "Add value"
2419
4527
  ]
2420
4528
  }
@@ -2423,11 +4531,11 @@ function EnumEditor({
2423
4531
  }
2424
4532
 
2425
4533
  // src/inspector/TableHintsTab.tsx
2426
- import { useMemo as useMemo7 } from "react";
4534
+ import { useMemo as useMemo10 } from "react";
2427
4535
  import { CELL_RENDERERS } from "@petrarca/sonnet-ui";
2428
- import { JsonEditor as JsonEditor4 } from "@petrarca/sonnet-ui/json-editor";
2429
- import { FormSelect as FormSelect4, JsonSchemaFormRenderer as JsonSchemaFormRenderer6 } from "@petrarca/sonnet-forms";
2430
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
4536
+ import { JsonEditor as JsonEditor3 } from "@petrarca/sonnet-ui/json-editor";
4537
+ import { FormSelect as FormSelect5, JsonSchemaFormRenderer as JsonSchemaFormRenderer6 } from "@petrarca/sonnet-forms";
4538
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2431
4539
  var CELL_NAMES = Object.keys(CELL_RENDERERS);
2432
4540
  var COLUMN_META_SCHEMA = {
2433
4541
  title: "",
@@ -2444,7 +4552,7 @@ function TableHintsTab({
2444
4552
  state
2445
4553
  }) {
2446
4554
  const hints = readTableHints(node.property);
2447
- const cellOptions = useMemo7(
4555
+ const cellOptions = useMemo10(
2448
4556
  () => JSON.stringify(hints.cellOptions ?? {}, null, 2),
2449
4557
  [hints.cellOptions]
2450
4558
  );
@@ -2467,9 +4575,9 @@ function TableHintsTab({
2467
4575
  column.minWidth = v.minWidth;
2468
4576
  commit({ column });
2469
4577
  };
2470
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-4", children: [
2471
- /* @__PURE__ */ jsx10(
2472
- FormSelect4,
4578
+ return /* @__PURE__ */ jsxs11("div", { className: "space-y-4", children: [
4579
+ /* @__PURE__ */ jsx13(
4580
+ FormSelect5,
2473
4581
  {
2474
4582
  id: `${node.key}-cell`,
2475
4583
  name: "cell",
@@ -2484,25 +4592,26 @@ function TableHintsTab({
2484
4592
  clearable: false
2485
4593
  }
2486
4594
  ),
2487
- /* @__PURE__ */ jsxs8("div", { children: [
2488
- /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Column" }),
2489
- /* @__PURE__ */ jsx10(
4595
+ /* @__PURE__ */ jsxs11("div", { children: [
4596
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: "Column" }),
4597
+ /* @__PURE__ */ jsx13(
2490
4598
  JsonSchemaFormRenderer6,
2491
4599
  {
2492
4600
  schema: COLUMN_META_SCHEMA,
2493
4601
  data: columnValues,
2494
4602
  onChange: onColumnChange,
4603
+ deferStateUpdates: true,
2495
4604
  showActions: false,
2496
4605
  showExtraProperties: false,
2497
4606
  readOnly: state.readOnly
2498
4607
  },
2499
- node.path.join(".")
4608
+ pathKey(node.path)
2500
4609
  )
2501
4610
  ] }),
2502
- /* @__PURE__ */ jsxs8("div", { children: [
2503
- /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Cell options (JSON)" }),
2504
- /* @__PURE__ */ jsx10(
2505
- JsonEditor4,
4611
+ /* @__PURE__ */ jsxs11("div", { children: [
4612
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: "Cell options (JSON)" }),
4613
+ /* @__PURE__ */ jsx13(
4614
+ JsonEditor3,
2506
4615
  {
2507
4616
  value: cellOptions,
2508
4617
  editablePaths: state.readOnly ? [] : ["/"],
@@ -2522,22 +4631,236 @@ function TableHintsTab({
2522
4631
  ] });
2523
4632
  }
2524
4633
 
4634
+ // src/inspector/ConditionEditor.tsx
4635
+ import { useMemo as useMemo11 } from "react";
4636
+ import { FormSelect as FormSelect6 } from "@petrarca/sonnet-forms";
4637
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
4638
+ function siblingFields(state, node) {
4639
+ const objectPath = node.path.slice(0, -1);
4640
+ const owner = objectPath.length === 0 ? { properties: state.schema.properties } : getProperty(state.schema, objectPath);
4641
+ return owner?.properties ?? {};
4642
+ }
4643
+ function ConditionEditor({
4644
+ node,
4645
+ state
4646
+ }) {
4647
+ const condition2 = useMemo11(
4648
+ () => ifToCondition(node.property.if),
4649
+ [node.property.if]
4650
+ );
4651
+ const fieldProps = useMemo11(() => siblingFields(state, node), [state, node]);
4652
+ const fields = useMemo11(() => Object.keys(fieldProps), [fieldProps]);
4653
+ const disabled = state.readOnly;
4654
+ const lastSeg = node.path[node.path.length - 1];
4655
+ const combinator = lastSeg?.kind === "condition" ? lastSeg.combinator : "allOf";
4656
+ const objectPath = node.path.slice(0, -1);
4657
+ const commit = (next) => {
4658
+ const ifSchema = conditionToIf(next, fieldProps[next.field]?.type);
4659
+ if (ifSchema) state.setCondition(node.path, ifSchema);
4660
+ };
4661
+ const fieldOptions = fields.map((f) => ({ value: f, label: f }));
4662
+ const needsValue = condition2.operator !== "exists";
4663
+ return /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
4664
+ /* @__PURE__ */ jsxs12("p", { className: "text-xs text-muted-foreground", children: [
4665
+ "Show the ",
4666
+ /* @__PURE__ */ jsx14("span", { className: "font-medium", children: "then" }),
4667
+ " fields when this condition holds (otherwise the ",
4668
+ /* @__PURE__ */ jsx14("span", { className: "font-medium", children: "else" }),
4669
+ " ",
4670
+ "fields, if any)."
4671
+ ] }),
4672
+ /* @__PURE__ */ jsx14(
4673
+ FormSelect6,
4674
+ {
4675
+ id: "condition-match",
4676
+ name: "match",
4677
+ label: "Match (all conditions on this object)",
4678
+ options: [
4679
+ { value: "allOf", label: "All (AND)" },
4680
+ { value: "anyOf", label: "Any (OR)" }
4681
+ ],
4682
+ value: combinator,
4683
+ onChange: (v) => state.setConditionalBehavior(
4684
+ objectPath,
4685
+ v === "anyOf" ? "anyOf" : "allOf"
4686
+ ),
4687
+ clearable: false,
4688
+ disabled
4689
+ }
4690
+ ),
4691
+ /* @__PURE__ */ jsx14(
4692
+ FormSelect6,
4693
+ {
4694
+ id: "condition-field",
4695
+ name: "field",
4696
+ label: "When property",
4697
+ options: fieldOptions,
4698
+ value: condition2.field || null,
4699
+ onChange: (v) => commit({ ...condition2, field: v ?? "" }),
4700
+ placeholder: "Select a property...",
4701
+ clearable: false,
4702
+ disabled
4703
+ }
4704
+ ),
4705
+ /* @__PURE__ */ jsx14(
4706
+ FormSelect6,
4707
+ {
4708
+ id: "condition-operator",
4709
+ name: "operator",
4710
+ label: "Operator",
4711
+ options: OPERATOR_OPTIONS,
4712
+ value: condition2.operator,
4713
+ onChange: (v) => commit({ ...condition2, operator: v ?? "=" }),
4714
+ clearable: false,
4715
+ disabled
4716
+ }
4717
+ ),
4718
+ needsValue && /* @__PURE__ */ jsx14(
4719
+ OperatorValueInput,
4720
+ {
4721
+ id: "condition-value",
4722
+ cacheKey: `${node.path.length}:${condition2.field}:${condition2.operator}`,
4723
+ value: condition2.value,
4724
+ onCommit: (v) => commit({ ...condition2, value: v }),
4725
+ disabled: disabled || !condition2.field
4726
+ }
4727
+ )
4728
+ ] });
4729
+ }
4730
+
4731
+ // src/inspector/RuleEditor.tsx
4732
+ import { useMemo as useMemo12 } from "react";
4733
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4734
+ function siblingPointers(state, node) {
4735
+ const parentProps = node.path.length <= 1 ? state.schema.properties : findParentProperties(state, node);
4736
+ const self = node.key;
4737
+ return Object.entries(parentProps ?? {}).filter(([k]) => k !== self).map(([k, prop2]) => ({ pointer: `/${k}`, type: prop2.type }));
4738
+ }
4739
+ function findParentProperties(state, node) {
4740
+ let props = state.schema.properties;
4741
+ for (let i = 0; i < node.path.length - 1; i++) {
4742
+ const seg = node.path[i];
4743
+ if (seg.kind !== "prop" || !props) return void 0;
4744
+ const next = props[seg.key];
4745
+ props = next?.type === "array" ? next.items?.properties : next?.properties;
4746
+ if (!props) return void 0;
4747
+ }
4748
+ return props;
4749
+ }
4750
+ function RuleEditor({
4751
+ node,
4752
+ state
4753
+ }) {
4754
+ const scopes = useMemo12(() => siblingPointers(state, node), [state, node]);
4755
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
4756
+ /* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "Toggle this field's visibility (or enabled state) based on another field's value. The field keeps its position in the form." }),
4757
+ /* @__PURE__ */ jsx15(
4758
+ RuleFields,
4759
+ {
4760
+ rule: node.property["x-ui-rule"],
4761
+ scopes,
4762
+ effects: FIELD_EFFECT_OPTIONS,
4763
+ idPrefix: "rule",
4764
+ disabled: state.readOnly,
4765
+ onChange: (rule) => state.updateProperty(node.path, { "x-ui-rule": rule })
4766
+ }
4767
+ )
4768
+ ] });
4769
+ }
4770
+
4771
+ // src/inspector/ReferenceTab.tsx
4772
+ import { useMemo as useMemo13, useRef as useRef5 } from "react";
4773
+ import { JsonSchemaFormRenderer as JsonSchemaFormRenderer7 } from "@petrarca/sonnet-forms";
4774
+ import { Button as Button4 } from "@petrarca/sonnet-ui";
4775
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4776
+ function toOverrides(prop2, required) {
4777
+ return {
4778
+ title: prop2.title,
4779
+ description: prop2.description,
4780
+ // Coerce to string for the text field; non-string defaults (number, boolean)
4781
+ // are serialised and written back as strings -- v1 limitation.
4782
+ default: prop2.default == null ? void 0 : String(prop2.default),
4783
+ required
4784
+ };
4785
+ }
4786
+ function toPatch(values) {
4787
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4788
+ return {
4789
+ title: str(values.title),
4790
+ description: str(values.description),
4791
+ default: str(values.default)
4792
+ };
4793
+ }
4794
+ function ReferenceTab({
4795
+ node,
4796
+ state
4797
+ }) {
4798
+ const schema = useMemo13(() => buildReferenceMetaSchema(), []);
4799
+ const values = useMemo13(
4800
+ () => toOverrides(node.property, node.required),
4801
+ [node.property, node.required]
4802
+ );
4803
+ const prevRef = useRef5(values);
4804
+ prevRef.current = values;
4805
+ const handleChange = (data) => {
4806
+ const next = data;
4807
+ const prev = prevRef.current;
4808
+ if (next.required !== prev.required) {
4809
+ state.setRequired(node.path, !!next.required);
4810
+ return;
4811
+ }
4812
+ state.updateProperty(node.path, toPatch(next));
4813
+ };
4814
+ return /* @__PURE__ */ jsxs14("div", { className: "space-y-4", children: [
4815
+ /* @__PURE__ */ jsxs14("p", { className: "text-sm text-muted-foreground", children: [
4816
+ "References ",
4817
+ /* @__PURE__ */ jsx16("span", { className: "font-medium", children: node.reference }),
4818
+ ". Its fields come from a shared definition (read-only here -- edit the definition to change all usages). The values below override the label, help text, and default for this usage only."
4819
+ ] }),
4820
+ /* @__PURE__ */ jsx16(
4821
+ JsonSchemaFormRenderer7,
4822
+ {
4823
+ schema,
4824
+ data: values,
4825
+ onChange: handleChange,
4826
+ deferStateUpdates: true,
4827
+ showActions: false,
4828
+ showExtraProperties: false,
4829
+ readOnly: state.readOnly
4830
+ },
4831
+ pathKey(node.path)
4832
+ ),
4833
+ !state.readOnly && state.canEditProperty(node.path) && /* @__PURE__ */ jsxs14("div", { className: "border-t pt-3", children: [
4834
+ /* @__PURE__ */ jsx16(
4835
+ Button4,
4836
+ {
4837
+ variant: "outline",
4838
+ size: "sm",
4839
+ onClick: () => state.changePropertyType(node.path, "string"),
4840
+ children: "Stop referencing"
4841
+ }
4842
+ ),
4843
+ /* @__PURE__ */ jsx16("p", { className: "mt-1 text-xs text-muted-foreground", children: "Removes the reference and turns this into an editable property." })
4844
+ ] })
4845
+ ] });
4846
+ }
4847
+
2525
4848
  // src/inspector/Inspector.tsx
2526
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
4849
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2527
4850
  var ENUM_CAPABLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer"]);
2528
4851
  function KeyField({
2529
4852
  fieldKey,
2530
4853
  onRename,
2531
4854
  readOnly
2532
4855
  }) {
2533
- const [editing, setEditing] = useState7(false);
2534
- const [value, setValue] = useState7(fieldKey);
2535
- const ref = useRef4(null);
2536
- useEffect4(() => {
4856
+ const [editing, setEditing] = useState8(false);
4857
+ const [value, setValue] = useState8(fieldKey);
4858
+ const ref = useRef6(null);
4859
+ useEffect5(() => {
2537
4860
  setValue(fieldKey);
2538
4861
  setEditing(false);
2539
4862
  }, [fieldKey]);
2540
- useEffect4(() => {
4863
+ useEffect5(() => {
2541
4864
  if (editing) {
2542
4865
  ref.current?.focus();
2543
4866
  ref.current?.select();
@@ -2553,14 +4876,14 @@ function KeyField({
2553
4876
  setEditing(false);
2554
4877
  };
2555
4878
  if (!editing) {
2556
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1", children: [
2557
- /* @__PURE__ */ jsx11("span", { className: "text-sm font-semibold truncate", children: fieldKey }),
2558
- !readOnly && /* @__PURE__ */ jsx11(IconButton4, { label: "Rename property", onClick: () => setEditing(true), children: /* @__PURE__ */ jsx11(Pencil, {}) })
4879
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
4880
+ /* @__PURE__ */ jsx17("span", { className: "text-sm font-semibold truncate", children: fieldKey }),
4881
+ !readOnly && /* @__PURE__ */ jsx17(IconButton4, { label: "Rename property", onClick: () => setEditing(true), children: /* @__PURE__ */ jsx17(Pencil, {}) })
2559
4882
  ] });
2560
4883
  }
2561
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1", children: [
2562
- /* @__PURE__ */ jsx11(
2563
- Input4,
4884
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
4885
+ /* @__PURE__ */ jsx17(
4886
+ Input5,
2564
4887
  {
2565
4888
  ref,
2566
4889
  value,
@@ -2577,8 +4900,8 @@ function KeyField({
2577
4900
  className: "h-7 text-sm font-medium"
2578
4901
  }
2579
4902
  ),
2580
- /* @__PURE__ */ jsx11(IconButton4, { label: "Confirm rename", onClick: commit, children: /* @__PURE__ */ jsx11(Check2, {}) }),
2581
- /* @__PURE__ */ jsx11(IconButton4, { label: "Cancel rename", onClick: cancel, children: /* @__PURE__ */ jsx11(X, {}) })
4903
+ /* @__PURE__ */ jsx17(IconButton4, { label: "Confirm rename", onClick: commit, children: /* @__PURE__ */ jsx17(Check2, {}) }),
4904
+ /* @__PURE__ */ jsx17(IconButton4, { label: "Cancel rename", onClick: cancel, children: /* @__PURE__ */ jsx17(X, {}) })
2582
4905
  ] });
2583
4906
  }
2584
4907
  function findNode(nodes, path) {
@@ -2589,49 +4912,109 @@ function findNode(nodes, path) {
2589
4912
  }
2590
4913
  return void 0;
2591
4914
  }
4915
+ function InspectorHeader({
4916
+ node,
4917
+ state
4918
+ }) {
4919
+ const isDefinition = node.kind === "definition";
4920
+ const noRename = isDefinition ? state.capabilities.locked : state.readOnly || node.slotKind === "branch" || node.slotKind === "conditional";
4921
+ const onRename = (next) => isDefinition ? state.renameDefinition(node.key, next) : state.renameProperty(node.path, next);
4922
+ return /* @__PURE__ */ jsxs15("div", { className: "mb-3 space-y-1", children: [
4923
+ /* @__PURE__ */ jsx17(
4924
+ KeyField,
4925
+ {
4926
+ fieldKey: node.key,
4927
+ readOnly: noRename,
4928
+ onRename
4929
+ },
4930
+ pathKey(node.path)
4931
+ ),
4932
+ /* @__PURE__ */ jsx17("p", { className: "font-mono text-xs text-muted-foreground", children: isDefinition ? "definition" : node.typeLabel })
4933
+ ] });
4934
+ }
4935
+ function PropertyTabs({
4936
+ node,
4937
+ state,
4938
+ widgetOptions
4939
+ }) {
4940
+ const enumCapable = ENUM_CAPABLE_TYPES.has(node.property.type ?? "");
4941
+ const ruleUnsupported = node.path.some(
4942
+ (s) => s.kind === "branch" || s.kind === "condition" || s.kind === "slot"
4943
+ );
4944
+ const uiUnsupported = node.path.some((s) => s.kind !== "prop");
4945
+ return /* @__PURE__ */ jsxs15(Tabs2, { defaultValue: "data", children: [
4946
+ /* @__PURE__ */ jsxs15(TabsList2, { className: "mb-3", children: [
4947
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "data", children: "Data" }),
4948
+ enumCapable && /* @__PURE__ */ jsx17(TabsTrigger2, { value: "enum", children: "Enum" }),
4949
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "ui", children: "UI" }),
4950
+ !ruleUnsupported && /* @__PURE__ */ jsx17(TabsTrigger2, { value: "rule", children: "Rule" }),
4951
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "table", children: "Table" })
4952
+ ] }),
4953
+ /* @__PURE__ */ jsx17(TabsContent2, { value: "data", children: /* @__PURE__ */ jsx17(DataTab, { node, state }) }),
4954
+ enumCapable && /* @__PURE__ */ jsx17(TabsContent2, { value: "enum", children: /* @__PURE__ */ jsx17(EnumEditor, { node, state }) }),
4955
+ !ruleUnsupported && /* @__PURE__ */ jsx17(TabsContent2, { value: "rule", children: /* @__PURE__ */ jsx17(RuleEditor, { node, state }) }),
4956
+ /* @__PURE__ */ jsx17(TabsContent2, { value: "ui", children: uiUnsupported ? /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground p-2", children: "Widget hints for properties inside a variant branch or definition are not yet editable here. Author them in the JSON Schema or UI Schema tab." }) : /* @__PURE__ */ jsx17(UITab, { node, state, widgetOptions }) }),
4957
+ /* @__PURE__ */ jsx17(TabsContent2, { value: "table", children: /* @__PURE__ */ jsx17(TableHintsTab, { node, state }) })
4958
+ ] }, `${pathKey(node.path)}:${enumCapable}`);
4959
+ }
4960
+ function InspectorBody({
4961
+ node,
4962
+ state,
4963
+ widgetOptions
4964
+ }) {
4965
+ const lastSeg = node.path[node.path.length - 1];
4966
+ if (node.kind === "reference") {
4967
+ return /* @__PURE__ */ jsx17(ReferenceTab, { node, state });
4968
+ }
4969
+ if (node.readOnly) {
4970
+ return /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground p-2", children: "This field comes from a shared definition and is read-only here. Edit the definition in the JSON Schema tab to change it." });
4971
+ }
4972
+ if (node.kind === "definition") {
4973
+ return /* @__PURE__ */ jsxs15("p", { className: "text-sm text-muted-foreground p-2", children: [
4974
+ "Reusable definition ",
4975
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: node.key }),
4976
+ ". Add or edit its fields in the tree; reference it from a property to reuse its shape. Renaming it here updates every reference."
4977
+ ] });
4978
+ }
4979
+ if (node.kind === "conditional") {
4980
+ return /* @__PURE__ */ jsx17(ConditionEditor, { node, state });
4981
+ }
4982
+ if (node.slotKind === "conditional" && lastSeg?.kind === "slot") {
4983
+ return /* @__PURE__ */ jsxs15("p", { className: "text-sm text-muted-foreground p-2", children: [
4984
+ "Add fields to this ",
4985
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: node.key }),
4986
+ " ",
4987
+ "branch from the tree. They apply when the condition",
4988
+ " ",
4989
+ node.key === "then" ? "holds" : "does not hold",
4990
+ "."
4991
+ ] });
4992
+ }
4993
+ return /* @__PURE__ */ jsx17(PropertyTabs, { node, state, widgetOptions });
4994
+ }
2592
4995
  function Inspector({
2593
4996
  state,
2594
4997
  widgetOptions
2595
4998
  }) {
2596
- const tree = useMemo8(() => buildPropertyTree(state.schema), [state.schema]);
4999
+ const tree = useMemo14(
5000
+ () => buildPropertyTree(state.schema, state.refResolver),
5001
+ [state.schema, state.refResolver]
5002
+ );
2597
5003
  const node = state.selection ? findNode(tree, state.selection) : void 0;
2598
5004
  if (!node) {
2599
- return /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground", children: "Select a property to edit." });
5005
+ return /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground", children: "Select a property to edit." });
2600
5006
  }
2601
- const enumCapable = ENUM_CAPABLE_TYPES.has(node.property.type ?? "");
2602
- return /* @__PURE__ */ jsxs9("div", { children: [
2603
- /* @__PURE__ */ jsxs9("div", { className: "mb-3 space-y-1", children: [
2604
- /* @__PURE__ */ jsx11(
2605
- KeyField,
2606
- {
2607
- fieldKey: node.key,
2608
- readOnly: state.readOnly,
2609
- onRename: (next) => state.renameProperty(node.path, next)
2610
- },
2611
- node.path.join(".")
2612
- ),
2613
- /* @__PURE__ */ jsx11("p", { className: "font-mono text-xs text-muted-foreground", children: node.typeLabel })
2614
- ] }),
2615
- /* @__PURE__ */ jsxs9(Tabs2, { defaultValue: "data", children: [
2616
- /* @__PURE__ */ jsxs9(TabsList2, { className: "mb-3", children: [
2617
- /* @__PURE__ */ jsx11(TabsTrigger2, { value: "data", children: "Data" }),
2618
- enumCapable && /* @__PURE__ */ jsx11(TabsTrigger2, { value: "enum", children: "Enum" }),
2619
- /* @__PURE__ */ jsx11(TabsTrigger2, { value: "ui", children: "UI" }),
2620
- /* @__PURE__ */ jsx11(TabsTrigger2, { value: "table", children: "Table" })
2621
- ] }),
2622
- /* @__PURE__ */ jsx11(TabsContent2, { value: "data", children: /* @__PURE__ */ jsx11(DataTab, { node, state }) }),
2623
- enumCapable && /* @__PURE__ */ jsx11(TabsContent2, { value: "enum", children: /* @__PURE__ */ jsx11(EnumEditor, { node, state }) }),
2624
- /* @__PURE__ */ jsx11(TabsContent2, { value: "ui", children: /* @__PURE__ */ jsx11(UITab, { node, state, widgetOptions }) }),
2625
- /* @__PURE__ */ jsx11(TabsContent2, { value: "table", children: /* @__PURE__ */ jsx11(TableHintsTab, { node, state }) })
2626
- ] }, `${node.path.join(".")}:${enumCapable}`)
5007
+ return /* @__PURE__ */ jsxs15("div", { children: [
5008
+ /* @__PURE__ */ jsx17(InspectorHeader, { node, state }),
5009
+ /* @__PURE__ */ jsx17(InspectorBody, { node, state, widgetOptions })
2627
5010
  ] });
2628
5011
  }
2629
5012
 
2630
5013
  // src/SchemaDesigner.tsx
2631
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
5014
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2632
5015
  function SchemaDesigner(props) {
2633
- const [capabilities, setCapabilities] = useState8(props.capabilities);
2634
- useEffect5(() => {
5016
+ const [capabilities, setCapabilities] = useState9(props.capabilities);
5017
+ useEffect6(() => {
2635
5018
  setCapabilities(props.capabilities);
2636
5019
  }, [props.capabilities]);
2637
5020
  const state = useDesignerState({ ...props, capabilities });
@@ -2657,17 +5040,17 @@ function SchemaDesigner(props) {
2657
5040
  maxWidth: 520,
2658
5041
  direction: "right-edge"
2659
5042
  });
2660
- const [inspectorCollapsed, setInspectorCollapsed] = useState8(false);
2661
- const [treeCollapsed, setTreeCollapsed] = useState8(false);
2662
- const [settingsOpen, setSettingsOpen] = useState8(false);
2663
- return /* @__PURE__ */ jsxs10("div", { className: "flex w-full h-full overflow-hidden border rounded-lg bg-background", children: [
2664
- !treeCollapsed && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2665
- /* @__PURE__ */ jsx12(
5043
+ const [inspectorCollapsed, setInspectorCollapsed] = useState9(false);
5044
+ const [treeCollapsed, setTreeCollapsed] = useState9(false);
5045
+ const [settingsOpen, setSettingsOpen] = useState9(false);
5046
+ return /* @__PURE__ */ jsxs16("div", { className: "flex w-full h-full overflow-hidden border rounded-lg bg-background", children: [
5047
+ !treeCollapsed && /* @__PURE__ */ jsxs16(Fragment5, { children: [
5048
+ /* @__PURE__ */ jsx18(
2666
5049
  "div",
2667
5050
  {
2668
5051
  style: state.capabilities.showPreview ? { width: treeWidth } : void 0,
2669
- className: state.capabilities.showPreview ? "shrink-0 overflow-auto p-3" : "flex-1 min-w-0 overflow-auto p-3",
2670
- children: /* @__PURE__ */ jsx12(
5052
+ className: state.capabilities.showPreview ? "shrink-0 overflow-hidden p-3 flex flex-col min-h-0" : "flex-1 min-w-0 overflow-hidden p-3 flex flex-col min-h-0",
5053
+ children: /* @__PURE__ */ jsx18(
2671
5054
  PropertyTree,
2672
5055
  {
2673
5056
  state,
@@ -2676,7 +5059,7 @@ function SchemaDesigner(props) {
2676
5059
  )
2677
5060
  }
2678
5061
  ),
2679
- state.capabilities.showPreview && /* @__PURE__ */ jsx12(
5062
+ state.capabilities.showPreview && /* @__PURE__ */ jsx18(
2680
5063
  ResizeHandle,
2681
5064
  {
2682
5065
  orientation: "vertical",
@@ -2686,15 +5069,15 @@ function SchemaDesigner(props) {
2686
5069
  }
2687
5070
  )
2688
5071
  ] }),
2689
- treeCollapsed && /* @__PURE__ */ jsx12("div", { className: "shrink-0 border-r flex flex-col items-center gap-1 p-1", children: /* @__PURE__ */ jsx12(
5072
+ treeCollapsed && /* @__PURE__ */ jsx18("div", { className: "shrink-0 border-r flex flex-col items-center gap-1 p-1", children: /* @__PURE__ */ jsx18(
2690
5073
  IconButton5,
2691
5074
  {
2692
5075
  label: "Expand properties",
2693
5076
  onClick: () => setTreeCollapsed(false),
2694
- children: /* @__PURE__ */ jsx12(PanelLeftOpen, {})
5077
+ children: /* @__PURE__ */ jsx18(PanelLeftOpen, {})
2695
5078
  }
2696
5079
  ) }),
2697
- state.capabilities.showPreview && /* @__PURE__ */ jsx12("div", { className: "flex-1 min-w-0 overflow-hidden p-4 flex flex-col min-h-0", children: /* @__PURE__ */ jsx12(
5080
+ state.capabilities.showPreview && /* @__PURE__ */ jsx18("div", { className: "flex-1 min-w-0 overflow-hidden p-4 flex flex-col min-h-0", children: /* @__PURE__ */ jsx18(
2698
5081
  PreviewPane,
2699
5082
  {
2700
5083
  state,
@@ -2702,8 +5085,8 @@ function SchemaDesigner(props) {
2702
5085
  onCapabilitiesChange: setCapabilities
2703
5086
  }
2704
5087
  ) }),
2705
- !inspectorCollapsed && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2706
- /* @__PURE__ */ jsx12(
5088
+ !inspectorCollapsed && /* @__PURE__ */ jsxs16(Fragment5, { children: [
5089
+ /* @__PURE__ */ jsx18(
2707
5090
  ResizeHandle,
2708
5091
  {
2709
5092
  orientation: "vertical",
@@ -2712,57 +5095,57 @@ function SchemaDesigner(props) {
2712
5095
  onDoubleClick: resetInspectorWidth
2713
5096
  }
2714
5097
  ),
2715
- /* @__PURE__ */ jsxs10(
5098
+ /* @__PURE__ */ jsxs16(
2716
5099
  "div",
2717
5100
  {
2718
5101
  style: { width: inspectorWidth },
2719
5102
  className: "shrink-0 border-l overflow-auto p-4 bg-card",
2720
5103
  children: [
2721
- /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1 mb-2", children: [
2722
- /* @__PURE__ */ jsx12("p", { className: "heading-meta", children: "Inspector" }),
2723
- /* @__PURE__ */ jsxs10("span", { className: "ml-auto flex gap-0.5", children: [
2724
- /* @__PURE__ */ jsx12(
5104
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-1 mb-2", children: [
5105
+ /* @__PURE__ */ jsx18("p", { className: "heading-meta", children: "Inspector" }),
5106
+ /* @__PURE__ */ jsxs16("span", { className: "ml-auto flex gap-0.5", children: [
5107
+ /* @__PURE__ */ jsx18(
2725
5108
  IconButton5,
2726
5109
  {
2727
5110
  label: "Form settings",
2728
5111
  onClick: () => setSettingsOpen(true),
2729
- children: /* @__PURE__ */ jsx12(Settings, {})
5112
+ children: /* @__PURE__ */ jsx18(Settings, {})
2730
5113
  }
2731
5114
  ),
2732
- /* @__PURE__ */ jsx12(
5115
+ /* @__PURE__ */ jsx18(
2733
5116
  IconButton5,
2734
5117
  {
2735
5118
  label: "Collapse inspector",
2736
5119
  onClick: () => setInspectorCollapsed(true),
2737
- children: /* @__PURE__ */ jsx12(PanelRightClose, {})
5120
+ children: /* @__PURE__ */ jsx18(PanelRightClose, {})
2738
5121
  }
2739
5122
  )
2740
5123
  ] })
2741
5124
  ] }),
2742
- /* @__PURE__ */ jsx12(Inspector, { state, widgetOptions: props.widgetOptions })
5125
+ /* @__PURE__ */ jsx18(Inspector, { state, widgetOptions: props.widgetOptions })
2743
5126
  ]
2744
5127
  }
2745
5128
  )
2746
5129
  ] }),
2747
- inspectorCollapsed && /* @__PURE__ */ jsxs10("div", { className: "shrink-0 border-l flex flex-col items-center gap-1 p-1", children: [
2748
- /* @__PURE__ */ jsx12(
5130
+ inspectorCollapsed && /* @__PURE__ */ jsxs16("div", { className: "shrink-0 border-l flex flex-col items-center gap-1 p-1", children: [
5131
+ /* @__PURE__ */ jsx18(
2749
5132
  IconButton5,
2750
5133
  {
2751
5134
  label: "Expand inspector",
2752
5135
  onClick: () => setInspectorCollapsed(false),
2753
- children: /* @__PURE__ */ jsx12(PanelRightOpen, {})
5136
+ children: /* @__PURE__ */ jsx18(PanelRightOpen, {})
2754
5137
  }
2755
5138
  ),
2756
- /* @__PURE__ */ jsx12(
5139
+ /* @__PURE__ */ jsx18(
2757
5140
  IconButton5,
2758
5141
  {
2759
5142
  label: "Form settings",
2760
5143
  onClick: () => setSettingsOpen(true),
2761
- children: /* @__PURE__ */ jsx12(Settings, {})
5144
+ children: /* @__PURE__ */ jsx18(Settings, {})
2762
5145
  }
2763
5146
  )
2764
5147
  ] }),
2765
- /* @__PURE__ */ jsx12(
5148
+ /* @__PURE__ */ jsx18(
2766
5149
  FormSettingsSheet,
2767
5150
  {
2768
5151
  open: settingsOpen,
@@ -2784,41 +5167,77 @@ export {
2784
5167
  TableHintsTab,
2785
5168
  TablePreview,
2786
5169
  UITab,
5170
+ addBranch,
5171
+ addConditional,
5172
+ addDefinition,
5173
+ addElse,
5174
+ addLayoutGroup,
2787
5175
  addProperty,
5176
+ branch,
2788
5177
  buildPropertyTree,
2789
5178
  capabilitiesSchema,
2790
5179
  changePropertyType,
2791
5180
  childContainer,
2792
- childrenOf,
5181
+ childSlots,
2793
5182
  compatibleWidgets,
5183
+ condition,
5184
+ def,
5185
+ definitionUsageCount,
5186
+ definitionsOf,
2794
5187
  deleteProperty,
2795
5188
  embedUiSchema,
2796
5189
  getProperty,
2797
5190
  getScopedLayout,
2798
5191
  getUiEntryAtPath,
2799
5192
  isContainer,
5193
+ isReservedKey,
2800
5194
  layoutScopes,
5195
+ moveBranch,
5196
+ moveLayoutGroup,
2801
5197
  moveProperty,
5198
+ newBranch,
5199
+ newCondition,
2802
5200
  newProperty,
2803
5201
  orderedKeys,
5202
+ parsePathKey,
2804
5203
  pathKey,
5204
+ placeFieldInGroup,
5205
+ prop,
2805
5206
  propertyKind,
2806
5207
  readEnumChoices,
2807
5208
  readTableHints,
5209
+ removeBranch,
5210
+ removeConditional,
5211
+ removeDefinition,
5212
+ removeElse,
5213
+ removeLayoutGroup,
5214
+ renameDefinition,
5215
+ renameLayoutGroup,
2808
5216
  renameProperty,
2809
5217
  resolveCapabilities,
2810
5218
  samePath,
5219
+ sameSegment,
2811
5220
  sampleRows,
5221
+ schemaAsRootProp,
5222
+ setCondition,
5223
+ setConditionalBehavior,
2812
5224
  setEnum,
2813
5225
  setGlobalOptions,
5226
+ setLayoutGroupRule,
2814
5227
  setPropertyLayout,
5228
+ setReference,
2815
5229
  setRequired,
2816
5230
  setRootLayout,
2817
5231
  setRootLayoutRows,
5232
+ setScopedGroupRows,
5233
+ setScopedLayoutGroups,
5234
+ setScopedLayoutMode,
2818
5235
  setScopedLayoutOptions,
2819
5236
  setScopedLayoutRows,
5237
+ setScopedLayoutVariant,
2820
5238
  setTableHints,
2821
5239
  setUiEntry,
5240
+ slot,
2822
5241
  updateProperty,
2823
5242
  useDesignerState,
2824
5243
  validateCapabilities