@petrarca/sonnet-schema-designer 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }
@@ -535,11 +1261,12 @@ import { useCallback, useMemo, useRef, useState } from "react";
535
1261
  // src/capabilities.ts
536
1262
  import { z } from "zod";
537
1263
  import { warnLog } from "@petrarca/sonnet-core";
1264
+ var previewKindSchema = z.enum(["form", "table", "json", "capabilities"]);
538
1265
  var capabilitiesSchema = z.object({
539
1266
  mode: z.enum(["data", "full"]).default("full").meta({
540
1267
  description: "What the designer authors. 'full' (default): JSON Schema + uiSchema (form presentation hints). 'data': JSON Schema only -- the UI Schema tab, Combined tab, and the inspector's per-field UI tab are hidden (pure data schemas: audit events, terminology)."
541
1268
  }),
542
- previews: z.array(z.enum(["form", "table", "json", "capabilities"])).optional().meta({
1269
+ previews: z.array(previewKindSchema).optional().meta({
543
1270
  description: "Which tabs the center pane offers, in order. 'form'/'table'/'json' preview the schema; 'capabilities' is a JSON editor for this profile (opt-in, never in a default). When omitted, defaults to ['form','table','json'] in 'full' mode and ['json'] in 'data' mode. An empty list hides the center pane entirely."
544
1271
  }),
545
1272
  editablePaths: z.array(z.string()).optional().meta({
@@ -586,7 +1313,7 @@ function validateCapabilities(input) {
586
1313
  }
587
1314
 
588
1315
  // src/model/scoping.ts
589
- function escapeToken(token) {
1316
+ function escapeToken2(token) {
590
1317
  return token.replace(/~/g, "~0").replace(/\//g, "~1");
591
1318
  }
592
1319
  function parentContainerPointer(schema, path) {
@@ -595,20 +1322,27 @@ function parentContainerPointer(schema, path) {
595
1322
  let pointer = "/properties";
596
1323
  for (let i = 0; i < path.length; i++) {
597
1324
  if (!container) return void 0;
598
- const seg = path[i];
599
- const prop = container[seg];
600
- if (!prop) return void 0;
1325
+ const segment = path[i];
1326
+ if (segment.kind !== "prop") return void 0;
1327
+ const key = segment.key;
1328
+ const prop2 = container[key];
1329
+ if (!prop2) return void 0;
601
1330
  if (i === path.length - 1) return pointer;
602
- const next = childContainer(prop);
1331
+ const next = childContainer(prop2);
603
1332
  if (!next) return void 0;
604
- pointer = prop.type === "array" ? `${pointer}/${escapeToken(seg)}/items/properties` : `${pointer}/${escapeToken(seg)}/properties`;
1333
+ pointer = prop2.type === "array" ? `${pointer}/${escapeToken2(key)}/items/properties` : `${pointer}/${escapeToken2(key)}/properties`;
605
1334
  container = next;
606
1335
  }
607
1336
  return pointer;
608
1337
  }
609
1338
  function isPropertyEditable(schema, path, editablePaths) {
610
1339
  if (editablePaths.length === 0) return true;
611
- const parent = parentContainerPointer(schema, path);
1340
+ const allPropPrefix = path.slice(
1341
+ 0,
1342
+ path.findIndex((s) => s.kind !== "prop") + 1 || path.length
1343
+ );
1344
+ const effectivePath = allPropPrefix.length < path.length ? allPropPrefix : path;
1345
+ const parent = parentContainerPointer(schema, effectivePath);
612
1346
  if (parent === void 0) return false;
613
1347
  return editablePaths.includes(parent);
614
1348
  }
@@ -623,18 +1357,89 @@ function ownChildContainerPointer(schema, path) {
623
1357
  if (parent === void 0) return void 0;
624
1358
  let container = schema.properties;
625
1359
  for (let i = 0; i < path.length; i++) {
626
- const prop = container?.[path[i]];
627
- if (!prop) return void 0;
1360
+ const segment = path[i];
1361
+ if (segment.kind !== "prop") return void 0;
1362
+ const prop2 = container?.[segment.key];
1363
+ if (!prop2) return void 0;
628
1364
  if (i === path.length - 1) {
629
- const seg = escapeToken(path[i]);
630
- return prop.type === "array" ? `${parent}/${seg}/items/properties` : `${parent}/${seg}/properties`;
1365
+ const seg = escapeToken2(segment.key);
1366
+ return prop2.type === "array" ? `${parent}/${seg}/items/properties` : `${parent}/${seg}/properties`;
631
1367
  }
632
- container = childContainer(prop);
1368
+ container = childContainer(prop2);
633
1369
  }
634
1370
  return void 0;
635
1371
  }
636
1372
 
637
1373
  // src/hooks/useDesignerState.ts
1374
+ function pathStartsWith(p, prefix) {
1375
+ if (p.length < prefix.length) return false;
1376
+ return prefix.every((seg, i) => sameSegment(seg, p[i]));
1377
+ }
1378
+ function sharesParentPrefix(p, removed) {
1379
+ if (p.length < removed.length) return false;
1380
+ const parentLen = removed.length - 1;
1381
+ for (let i = 0; i < parentLen; i++) {
1382
+ if (!sameSegment(p[i], removed[i])) return false;
1383
+ }
1384
+ return true;
1385
+ }
1386
+ function sameIndexedArray(a, b) {
1387
+ if (a.kind !== b.kind) return false;
1388
+ if (a.kind === "branch" && b.kind === "branch") return true;
1389
+ if (a.kind === "condition" && b.kind === "condition")
1390
+ return a.combinator === b.combinator;
1391
+ return false;
1392
+ }
1393
+ function affectedSibling(prev, removed, kind) {
1394
+ const last = removed[removed.length - 1];
1395
+ if (!last || last.kind !== kind) return null;
1396
+ if (!sharesParentPrefix(prev, removed)) return null;
1397
+ const at = removed.length - 1;
1398
+ const seg = prev[at];
1399
+ if (!seg || !sameIndexedArray(seg, last) || !("index" in seg)) return null;
1400
+ return { at, seg };
1401
+ }
1402
+ function adjustIndexedSelection(prev, removed, kind) {
1403
+ if (!prev) return null;
1404
+ const hit = affectedSibling(prev, removed, kind);
1405
+ if (!hit) return prev;
1406
+ const removedIndex = removed[removed.length - 1].index;
1407
+ if (hit.seg.index === removedIndex) return null;
1408
+ if (hit.seg.index < removedIndex) return prev;
1409
+ const shifted = { ...hit.seg, index: hit.seg.index - 1 };
1410
+ return [...prev.slice(0, hit.at), shifted, ...prev.slice(hit.at + 1)];
1411
+ }
1412
+ function adjustBranchSelection(prev, removed) {
1413
+ return adjustIndexedSelection(prev, removed, "branch");
1414
+ }
1415
+ function countConditionals(arr) {
1416
+ return Array.isArray(arr) ? arr.filter((m) => !!m.if).length : 0;
1417
+ }
1418
+ function selectedConditionInCombinator(prev, objectPath, from) {
1419
+ if (!prev) return null;
1420
+ const at = objectPath.length;
1421
+ const seg = prev[at];
1422
+ if (!seg || seg.kind !== "condition" || seg.combinator !== from) return null;
1423
+ if (!samePath(prev.slice(0, at), objectPath)) return null;
1424
+ return { at, index: seg.index };
1425
+ }
1426
+ function remapConditionSelection(before, prev, objectPath, next) {
1427
+ const from = next === "allOf" ? "anyOf" : "allOf";
1428
+ const hit = selectedConditionInCombinator(prev, objectPath, from);
1429
+ if (!hit || !prev) return prev;
1430
+ const owner = objectPath.length === 0 ? { allOf: before.schema.allOf, anyOf: before.schema.anyOf } : getProperty(before.schema, objectPath);
1431
+ const sourceArr = owner?.[from];
1432
+ if (!Array.isArray(sourceArr)) return prev;
1433
+ const posAmongSource = countConditionals(sourceArr.slice(0, hit.index));
1434
+ const targetOffset = countConditionals(
1435
+ owner?.[next]
1436
+ );
1437
+ return [
1438
+ ...prev.slice(0, hit.at),
1439
+ condition(targetOffset + posAmongSource, next),
1440
+ ...prev.slice(hit.at + 1)
1441
+ ];
1442
+ }
638
1443
  var EMPTY_SCHEMA = {
639
1444
  title: "",
640
1445
  type: "object",
@@ -695,6 +1500,56 @@ function useDesignerState(args) {
695
1500
  commit(res.model);
696
1501
  setSelection(res.path);
697
1502
  },
1503
+ addBranch(path) {
1504
+ if (!canEditProperty(path)) return;
1505
+ const res = addBranch(modelRef.current, path);
1506
+ commit(res.model);
1507
+ setSelection(res.path);
1508
+ },
1509
+ removeBranch(path) {
1510
+ if (!canEditProperty(path)) return;
1511
+ commit(removeBranch(modelRef.current, path));
1512
+ setSelection((prev) => adjustBranchSelection(prev, path));
1513
+ },
1514
+ moveBranch(path, direction) {
1515
+ if (!canEditProperty(path)) return;
1516
+ commit(moveBranch(modelRef.current, path, direction));
1517
+ },
1518
+ addConditional(objectPath, combinator = "allOf") {
1519
+ if (!canAddChild(objectPath)) return;
1520
+ const res = addConditional(modelRef.current, objectPath, combinator);
1521
+ commit(res.model);
1522
+ setSelection(res.path);
1523
+ },
1524
+ setConditionalBehavior(objectPath, combinator) {
1525
+ if (!canAddChild(objectPath)) return;
1526
+ const before = modelRef.current;
1527
+ commit(setConditionalBehavior(before, objectPath, combinator));
1528
+ setSelection(
1529
+ (prev) => remapConditionSelection(before, prev, objectPath, combinator)
1530
+ );
1531
+ },
1532
+ removeConditional(path) {
1533
+ if (!canAddChild(path.slice(0, -1))) return;
1534
+ commit(removeConditional(modelRef.current, path));
1535
+ setSelection((prev) => adjustIndexedSelection(prev, path, "condition"));
1536
+ },
1537
+ setCondition(path, ifSchema) {
1538
+ if (!canAddChild(path.slice(0, -1))) return;
1539
+ commit(setCondition(modelRef.current, path, ifSchema));
1540
+ },
1541
+ addElse(path) {
1542
+ if (!canAddChild(path.slice(0, -1))) return;
1543
+ commit(addElse(modelRef.current, path));
1544
+ },
1545
+ removeElse(path) {
1546
+ if (!canAddChild(path.slice(0, -1))) return;
1547
+ commit(removeElse(modelRef.current, path));
1548
+ const elsePath = [...path, slot("else")];
1549
+ setSelection(
1550
+ (prev) => prev && pathStartsWith(prev, elsePath) ? null : prev
1551
+ );
1552
+ },
698
1553
  deleteProperty(path) {
699
1554
  if (!canEditProperty(path)) return;
700
1555
  commit(deleteProperty(modelRef.current, path));
@@ -718,9 +1573,48 @@ function useDesignerState(args) {
718
1573
  if (!canEditProperty(path)) return;
719
1574
  commit(updateProperty(modelRef.current, path, patch));
720
1575
  },
721
- changePropertyType(path, nextType) {
1576
+ changePropertyType(path, next) {
722
1577
  if (!canEditProperty(path)) return;
723
- commit(changePropertyType(modelRef.current, path, nextType));
1578
+ commit(changePropertyType(modelRef.current, path, next));
1579
+ },
1580
+ setReference(path, name) {
1581
+ if (!canEditProperty(path)) return;
1582
+ commit(setReference(modelRef.current, path, name));
1583
+ },
1584
+ // Definitions are document-level, not path-scoped: gate on lock only.
1585
+ addDefinition(name) {
1586
+ if (capabilities.locked) return;
1587
+ const res = addDefinition(modelRef.current, name);
1588
+ commit(res.model);
1589
+ setSelection(res.path);
1590
+ },
1591
+ renameDefinition(oldName, newName) {
1592
+ if (capabilities.locked) return;
1593
+ const res = renameDefinition(modelRef.current, oldName, newName);
1594
+ commit(res.model);
1595
+ setSelection(
1596
+ (prev) => prev && samePath(prev, [def(oldName)]) ? res.path : prev
1597
+ );
1598
+ },
1599
+ removeDefinition(name) {
1600
+ if (capabilities.locked) return;
1601
+ commit(removeDefinition(modelRef.current, name));
1602
+ setSelection(
1603
+ (prev) => prev && pathStartsWith(prev, [def(name)]) ? null : prev
1604
+ );
1605
+ },
1606
+ // Replace the whole JSON Schema (from the editable JSON Schema tab).
1607
+ // The uiSchema is preserved. Gated by capabilities.locked only -- a
1608
+ // wholesale replace is not subject to per-path scoping.
1609
+ replaceSchema(schema) {
1610
+ if (capabilities.locked) return;
1611
+ commit({ schema, uiSchema: modelRef.current.uiSchema });
1612
+ setSelection(null);
1613
+ },
1614
+ // Replace the whole UI Schema map (from the editable UI Schema tab).
1615
+ replaceUiSchema(uiSchema) {
1616
+ if (readOnly) return;
1617
+ commit({ schema: modelRef.current.schema, uiSchema });
724
1618
  },
725
1619
  setEnum(path, choices) {
726
1620
  if (!canEditProperty(path)) return;
@@ -757,18 +1651,67 @@ function useDesignerState(args) {
757
1651
  if (readOnly) return;
758
1652
  commit(setScopedLayoutOptions(modelRef.current, scopePath, options));
759
1653
  },
1654
+ setScopedLayoutGroups(scopePath, groups) {
1655
+ if (readOnly) return;
1656
+ commit(setScopedLayoutGroups(modelRef.current, scopePath, groups));
1657
+ },
1658
+ setScopedGroupRows(scopePath, groupIndex, rows) {
1659
+ if (readOnly) return;
1660
+ commit(
1661
+ setScopedGroupRows(modelRef.current, scopePath, groupIndex, rows)
1662
+ );
1663
+ },
1664
+ setScopedLayoutMode(scopePath, mode) {
1665
+ if (readOnly) return;
1666
+ commit(setScopedLayoutMode(modelRef.current, scopePath, mode));
1667
+ },
1668
+ addLayoutGroup(scopePath, label) {
1669
+ if (readOnly) return;
1670
+ commit(addLayoutGroup(modelRef.current, scopePath, label));
1671
+ },
1672
+ renameLayoutGroup(scopePath, groupIndex, label) {
1673
+ if (readOnly) return;
1674
+ commit(
1675
+ renameLayoutGroup(modelRef.current, scopePath, groupIndex, label)
1676
+ );
1677
+ },
1678
+ removeLayoutGroup(scopePath, groupIndex) {
1679
+ if (readOnly) return;
1680
+ commit(removeLayoutGroup(modelRef.current, scopePath, groupIndex));
1681
+ },
1682
+ moveLayoutGroup(scopePath, groupIndex, direction) {
1683
+ if (readOnly) return;
1684
+ commit(
1685
+ moveLayoutGroup(modelRef.current, scopePath, groupIndex, direction)
1686
+ );
1687
+ },
1688
+ setLayoutGroupRule(scopePath, groupIndex, rule) {
1689
+ if (readOnly) return;
1690
+ commit(
1691
+ setLayoutGroupRule(modelRef.current, scopePath, groupIndex, rule)
1692
+ );
1693
+ },
1694
+ setScopedLayoutVariant(scopePath, variant) {
1695
+ if (readOnly) return;
1696
+ commit(setScopedLayoutVariant(modelRef.current, scopePath, variant));
1697
+ },
1698
+ placeFieldInGroup(scopePath, key, target) {
1699
+ if (readOnly) return;
1700
+ commit(placeFieldInGroup(modelRef.current, scopePath, key, target));
1701
+ },
760
1702
  setGlobalOptions(options) {
761
1703
  if (readOnly) return;
762
1704
  commit(setGlobalOptions(modelRef.current, options));
763
1705
  }
764
1706
  }),
765
- [commit, readOnly, canEditProperty, canAddChild]
1707
+ [commit, readOnly, capabilities.locked, canEditProperty, canAddChild]
766
1708
  );
767
1709
  return {
768
1710
  schema: model.schema,
769
1711
  uiSchema: model.uiSchema,
770
1712
  selection,
771
1713
  readOnly,
1714
+ refResolver: args.refResolver,
772
1715
  capabilities,
773
1716
  canEditProperty,
774
1717
  canAddChild,
@@ -778,7 +1721,7 @@ function useDesignerState(args) {
778
1721
  }
779
1722
 
780
1723
  // src/SchemaDesigner.tsx
781
- import { useEffect as useEffect5, useState as useState8 } from "react";
1724
+ import { useEffect as useEffect6, useState as useState9 } from "react";
782
1725
  import { ResizeHandle, IconButton as IconButton5 } from "@petrarca/sonnet-ui";
783
1726
  import { useResizablePanel } from "@petrarca/sonnet-core/hooks";
784
1727
  import {
@@ -951,7 +1894,7 @@ function FormSettingsSheet({
951
1894
  }
952
1895
 
953
1896
  // src/tree/PropertyTree.tsx
954
- import { useEffect, useRef as useRef2, useState as useState2 } from "react";
1897
+ import { useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
955
1898
  import { cn } from "@petrarca/sonnet-core";
956
1899
  import { IconButton, Input } from "@petrarca/sonnet-ui";
957
1900
  import {
@@ -960,30 +1903,90 @@ import {
960
1903
  ChevronUp,
961
1904
  ChevronDown,
962
1905
  PanelLeftClose,
963
- Lock
1906
+ Lock,
1907
+ GitBranch,
1908
+ SplitSquareHorizontal,
1909
+ Eye,
1910
+ Braces,
1911
+ Brackets,
1912
+ GitFork,
1913
+ CornerDownRight,
1914
+ Type,
1915
+ List,
1916
+ ToggleLeft,
1917
+ Hash,
1918
+ Link2,
1919
+ Puzzle,
1920
+ ChevronRight
964
1921
  } from "lucide-react";
965
1922
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1923
+ function buildInUseDefs(schema, names) {
1924
+ return new Set(names.filter((n) => definitionUsageCount(schema, n) > 0));
1925
+ }
966
1926
  function PropertyTree({
967
1927
  state,
968
1928
  onCollapse
969
1929
  }) {
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" }),
1930
+ const nodes = useMemo2(
1931
+ () => buildPropertyTree(state.schema, state.refResolver),
1932
+ [state.schema, state.refResolver]
1933
+ );
1934
+ const propertyNodes = useMemo2(
1935
+ () => nodes.filter((n) => n.kind !== "definition"),
1936
+ [nodes]
1937
+ );
1938
+ const definitionNodes = useMemo2(
1939
+ () => nodes.filter((n) => n.kind === "definition"),
1940
+ [nodes]
1941
+ );
1942
+ const canEditDefs = !state.capabilities.locked;
1943
+ const [propsOpen, setPropsOpen] = useState2(true);
1944
+ return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col min-h-0", children: [
1945
+ /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center gap-2 shrink-0", children: [
1946
+ /* @__PURE__ */ jsxs2(
1947
+ "button",
1948
+ {
1949
+ type: "button",
1950
+ className: "flex items-center gap-1 heading-meta",
1951
+ onClick: () => setPropsOpen((v) => !v),
1952
+ children: [
1953
+ propsOpen ? /* @__PURE__ */ jsx2(ChevronDown, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx2(ChevronRight, { className: "h-3 w-3" }),
1954
+ "Properties",
1955
+ propertyNodes.length > 0 && /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground/60", children: [
1956
+ "(",
1957
+ propertyNodes.length,
1958
+ ")"
1959
+ ] })
1960
+ ]
1961
+ }
1962
+ ),
974
1963
  /* @__PURE__ */ jsxs2("span", { className: "ml-auto flex gap-1", children: [
975
1964
  state.canAddChild([]) && /* @__PURE__ */ jsx2(
976
1965
  IconButton,
977
1966
  {
978
1967
  label: "Add property",
979
- onClick: () => state.addProperty([], "scalar"),
1968
+ onClick: () => {
1969
+ setPropsOpen(true);
1970
+ state.addProperty([], "scalar");
1971
+ },
980
1972
  children: /* @__PURE__ */ jsx2(Plus, {})
981
1973
  }
982
1974
  ),
1975
+ state.canAddChild([]) && /* @__PURE__ */ jsx2(
1976
+ IconButton,
1977
+ {
1978
+ label: "Add condition",
1979
+ onClick: () => {
1980
+ setPropsOpen(true);
1981
+ state.addConditional([]);
1982
+ },
1983
+ children: /* @__PURE__ */ jsx2(GitBranch, {})
1984
+ }
1985
+ ),
983
1986
  onCollapse && /* @__PURE__ */ jsx2(IconButton, { label: "Collapse properties", onClick: onCollapse, children: /* @__PURE__ */ jsx2(PanelLeftClose, {}) })
984
1987
  ] })
985
1988
  ] }),
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(
1989
+ 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
1990
  PropertyRow,
988
1991
  {
989
1992
  node,
@@ -991,7 +1994,72 @@ function PropertyTree({
991
1994
  depth: 0
992
1995
  },
993
1996
  pathKey(node.path)
994
- )) })
1997
+ )) })),
1998
+ /* @__PURE__ */ jsx2(
1999
+ DefinitionsRegion,
2000
+ {
2001
+ nodes: definitionNodes,
2002
+ state,
2003
+ canEdit: canEditDefs
2004
+ }
2005
+ )
2006
+ ] });
2007
+ }
2008
+ function DefinitionsRegion({
2009
+ nodes,
2010
+ state,
2011
+ canEdit
2012
+ }) {
2013
+ const [open, setOpen] = useState2(false);
2014
+ const inUseDefs = useMemo2(
2015
+ () => buildInUseDefs(
2016
+ state.schema,
2017
+ nodes.map((n) => n.key)
2018
+ ),
2019
+ [state.schema, nodes]
2020
+ );
2021
+ if (nodes.length === 0 && !canEdit) return null;
2022
+ return /* @__PURE__ */ jsxs2("div", { className: "mt-3 shrink-0", children: [
2023
+ /* @__PURE__ */ jsxs2("div", { className: "mb-2 flex items-center gap-2", children: [
2024
+ /* @__PURE__ */ jsxs2(
2025
+ "button",
2026
+ {
2027
+ type: "button",
2028
+ className: "flex items-center gap-1 heading-meta",
2029
+ onClick: () => setOpen((v) => !v),
2030
+ children: [
2031
+ open ? /* @__PURE__ */ jsx2(ChevronDown, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx2(ChevronRight, { className: "h-3 w-3" }),
2032
+ "Definitions",
2033
+ nodes.length > 0 && /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground/60", children: [
2034
+ "(",
2035
+ nodes.length,
2036
+ ")"
2037
+ ] })
2038
+ ]
2039
+ }
2040
+ ),
2041
+ canEdit && /* @__PURE__ */ jsx2("span", { className: "ml-auto flex gap-1", children: /* @__PURE__ */ jsx2(
2042
+ IconButton,
2043
+ {
2044
+ label: "Add definition",
2045
+ onClick: () => {
2046
+ setOpen(true);
2047
+ state.addDefinition();
2048
+ },
2049
+ children: /* @__PURE__ */ jsx2(Plus, {})
2050
+ }
2051
+ ) })
2052
+ ] }),
2053
+ 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(
2054
+ PropertyRow,
2055
+ {
2056
+ node,
2057
+ state,
2058
+ depth: 0,
2059
+ inUseDefs
2060
+ },
2061
+ pathKey(node.path)
2062
+ )) }))
995
2063
  ] });
996
2064
  }
997
2065
  function NameEditor({
@@ -1031,23 +2099,82 @@ function NameEditor({
1031
2099
  }
1032
2100
  );
1033
2101
  }
2102
+ var KIND_ICONS = {
2103
+ reference: { Icon: Link2, label: "reference" },
2104
+ definition: { Icon: Puzzle, label: "definition" },
2105
+ conditional: { Icon: GitBranch, label: "condition" },
2106
+ object: { Icon: Braces, label: "object" },
2107
+ array: { Icon: Brackets, label: "array" },
2108
+ oneOf: { Icon: GitFork, label: "one of" },
2109
+ enum: { Icon: List, label: "choice" }
2110
+ };
2111
+ function kindIconFor(node) {
2112
+ const last = node.path[node.path.length - 1];
2113
+ if (last?.kind === "slot") return { Icon: CornerDownRight, label: "branch" };
2114
+ if (node.slotKind === "branch") return { Icon: GitFork, label: "variant" };
2115
+ return KIND_ICONS[node.kind] ?? scalarIcon(node.property.type);
2116
+ }
2117
+ function scalarIcon(type) {
2118
+ if (type === "number" || type === "integer")
2119
+ return { Icon: Hash, label: "number" };
2120
+ if (type === "boolean") return { Icon: ToggleLeft, label: "boolean" };
2121
+ return { Icon: Type, label: "text" };
2122
+ }
2123
+ function KindIcon({ node }) {
2124
+ const { Icon, label } = kindIconFor(node);
2125
+ return /* @__PURE__ */ jsx2(
2126
+ Icon,
2127
+ {
2128
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/60",
2129
+ "aria-label": label
2130
+ }
2131
+ );
2132
+ }
2133
+ function Markers({ node }) {
2134
+ const rule = node.property["x-ui-rule"];
2135
+ if (!node.required && !rule) return null;
2136
+ const scopeField = rule?.condition.scope.replace(/^\//, "");
2137
+ const ruleTip = rule ? `${rule.effect} when ${scopeField || rule.condition.scope}` : "";
2138
+ return /* @__PURE__ */ jsxs2("span", { className: "shrink-0 flex items-center gap-1 group-hover/row:hidden", children: [
2139
+ node.required && /* @__PURE__ */ jsx2(
2140
+ "span",
2141
+ {
2142
+ className: "text-destructive text-xs leading-none",
2143
+ title: "required",
2144
+ "aria-label": "required",
2145
+ children: "*"
2146
+ }
2147
+ ),
2148
+ rule && /* @__PURE__ */ jsx2("span", { title: ruleTip, "aria-label": ruleTip, className: "flex", children: /* @__PURE__ */ jsx2(Eye, { className: "h-3 w-3 text-primary" }) })
2149
+ ] });
2150
+ }
2151
+ function canAddChildHere(node, state, isConditional) {
2152
+ if (!acceptsChildren(node)) return false;
2153
+ return node.kind === "oneOf" || isConditional ? state.canEditProperty(node.path) : state.canAddChild(node.path);
2154
+ }
2155
+ function isRuleStyled(node) {
2156
+ const last = node.path[node.path.length - 1];
2157
+ return node.kind === "conditional" || last?.kind === "slot";
2158
+ }
1034
2159
  function RowLabel({
1035
2160
  node,
1036
2161
  editable,
1037
2162
  onStartRename
1038
2163
  }) {
1039
2164
  const title = editable ? `${node.key}${node.required ? " (required)" : ""} -- double-click to rename` : `${node.key} -- fixed (not editable)`;
2165
+ const ruleStyled = isRuleStyled(node);
1040
2166
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
1041
- /* @__PURE__ */ jsxs2(
2167
+ /* @__PURE__ */ jsx2(KindIcon, { node }),
2168
+ /* @__PURE__ */ jsx2(
1042
2169
  "span",
1043
2170
  {
1044
- className: "flex-1 min-w-0 truncate text-sm font-medium",
2171
+ className: cn(
2172
+ "flex-1 min-w-0 truncate text-sm",
2173
+ ruleStyled ? "font-mono text-xs uppercase tracking-wide text-muted-foreground" : "font-medium"
2174
+ ),
1045
2175
  onDoubleClick: onStartRename,
1046
2176
  title,
1047
- children: [
1048
- node.key,
1049
- node.required && /* @__PURE__ */ jsx2("span", { className: "ml-0.5 text-destructive", "aria-label": "required", children: "*" })
1050
- ]
2177
+ children: node.key
1051
2178
  }
1052
2179
  ),
1053
2180
  !editable && /* @__PURE__ */ jsx2(
@@ -1057,89 +2184,192 @@ function RowLabel({
1057
2184
  "aria-label": "fixed"
1058
2185
  }
1059
2186
  ),
2187
+ /* @__PURE__ */ jsx2(Markers, { node }),
1060
2188
  /* @__PURE__ */ jsx2(
1061
2189
  "span",
1062
2190
  {
1063
- className: "shrink-0 max-w-[45%] truncate font-mono text-[11px] text-muted-foreground/70 group-hover/row:hidden",
2191
+ className: "shrink-0 max-w-[40%] truncate font-mono text-[11px] text-muted-foreground/70 group-hover/row:hidden",
1064
2192
  title: node.typeLabel,
1065
2193
  children: node.typeLabel
1066
2194
  }
1067
2195
  )
1068
2196
  ] });
1069
2197
  }
1070
- function RowActions({
2198
+ function isConditionalMember2(node) {
2199
+ return node.kind === "conditional";
2200
+ }
2201
+ function isConditionalSlot(node) {
2202
+ const last = node.path[node.path.length - 1];
2203
+ return node.slotKind === "conditional" && last?.kind === "slot";
2204
+ }
2205
+ function acceptsChildren(node) {
2206
+ return node.kind === "object" || node.kind === "array" || node.kind === "oneOf" || node.kind === "definition" || node.slotKind === "branch" || isConditionalSlot(node);
2207
+ }
2208
+ function acceptsConditions(node) {
2209
+ return node.kind === "object" || isConditionalSlot(node);
2210
+ }
2211
+ function ActionButton({
2212
+ label,
2213
+ tone,
2214
+ disabled,
2215
+ onAct,
2216
+ children
2217
+ }) {
2218
+ return /* @__PURE__ */ jsx2(
2219
+ IconButton,
2220
+ {
2221
+ label,
2222
+ tone,
2223
+ disabled,
2224
+ revealOnHover: true,
2225
+ onClick: (e) => {
2226
+ e.stopPropagation();
2227
+ onAct();
2228
+ },
2229
+ children
2230
+ }
2231
+ );
2232
+ }
2233
+ function MoveActions({
2234
+ node,
2235
+ state
2236
+ }) {
2237
+ const isBranch = node.slotKind === "branch";
2238
+ const move = (dir) => isBranch ? state.moveBranch(node.path, dir) : state.moveProperty(node.path, dir);
2239
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2240
+ /* @__PURE__ */ jsx2(ActionButton, { label: "Move up", onAct: () => move(-1), children: /* @__PURE__ */ jsx2(ChevronUp, {}) }),
2241
+ /* @__PURE__ */ jsx2(ActionButton, { label: "Move down", onAct: () => move(1), children: /* @__PURE__ */ jsx2(ChevronDown, {}) })
2242
+ ] });
2243
+ }
2244
+ function AddActions({
1071
2245
  node,
1072
2246
  state,
1073
2247
  editable,
1074
2248
  canAddHere
1075
2249
  }) {
2250
+ const isOneOf3 = node.kind === "oneOf";
2251
+ const addChild = () => isOneOf3 ? state.addBranch(node.path) : state.addProperty(node.path, "scalar");
2252
+ const condMember = isConditionalMember2(node) && editable;
2253
+ const hasElse = !!node.property.else;
1076
2254
  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
2255
  canAddHere && /* @__PURE__ */ jsx2(
1104
- IconButton,
2256
+ ActionButton,
1105
2257
  {
1106
- label: "Add child property",
1107
- revealOnHover: true,
1108
- onClick: (e) => {
1109
- e.stopPropagation();
1110
- state.addProperty(node.path, "scalar");
1111
- },
2258
+ label: isOneOf3 ? "Add branch" : "Add child property",
2259
+ onAct: addChild,
1112
2260
  children: /* @__PURE__ */ jsx2(Plus, {})
1113
2261
  }
1114
2262
  ),
1115
- editable && /* @__PURE__ */ jsx2(
1116
- IconButton,
2263
+ editable && acceptsConditions(node) && /* @__PURE__ */ jsx2(
2264
+ ActionButton,
1117
2265
  {
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, {})
2266
+ label: "Add condition",
2267
+ onAct: () => state.addConditional(node.path),
2268
+ children: /* @__PURE__ */ jsx2(GitBranch, {})
1126
2269
  }
1127
- )
2270
+ ),
2271
+ condMember && !hasElse && /* @__PURE__ */ jsx2(ActionButton, { label: "Add else", onAct: () => state.addElse(node.path), children: /* @__PURE__ */ jsx2(SplitSquareHorizontal, {}) })
2272
+ ] });
2273
+ }
2274
+ function lastSegment(node) {
2275
+ return node.path[node.path.length - 1];
2276
+ }
2277
+ function removeDescriptor(node, state, inUseDefs) {
2278
+ const last = lastSegment(node);
2279
+ const slotName = last?.kind === "slot" ? last.slot : void 0;
2280
+ if (slotName === "then") return null;
2281
+ if (node.slotKind === "branch") {
2282
+ return simpleRemove("Remove branch", () => state.removeBranch(node.path));
2283
+ }
2284
+ if (isConditionalMember2(node)) {
2285
+ return simpleRemove(
2286
+ "Remove condition",
2287
+ () => state.removeConditional(node.path)
2288
+ );
2289
+ }
2290
+ if (node.kind === "definition") {
2291
+ const inUse = inUseDefs.has(node.key);
2292
+ return {
2293
+ label: inUse ? "In use -- remove references first" : "Delete definition",
2294
+ disabled: inUse,
2295
+ remove: () => state.removeDefinition(node.key)
2296
+ };
2297
+ }
2298
+ if (slotName === "else") {
2299
+ return simpleRemove(
2300
+ "Remove else",
2301
+ () => state.removeElse(node.path.slice(0, -1))
2302
+ );
2303
+ }
2304
+ return simpleRemove("Delete property", () => state.deleteProperty(node.path));
2305
+ }
2306
+ function simpleRemove(label, remove) {
2307
+ return { label, disabled: false, remove };
2308
+ }
2309
+ function RemoveAction({
2310
+ node,
2311
+ state,
2312
+ inUseDefs
2313
+ }) {
2314
+ const desc = removeDescriptor(node, state, inUseDefs);
2315
+ if (!desc) return null;
2316
+ return /* @__PURE__ */ jsx2(
2317
+ ActionButton,
2318
+ {
2319
+ label: desc.label,
2320
+ tone: "destructive",
2321
+ disabled: desc.disabled,
2322
+ onAct: desc.remove,
2323
+ children: /* @__PURE__ */ jsx2(Trash2, {})
2324
+ }
2325
+ );
2326
+ }
2327
+ var EMPTY_SET = /* @__PURE__ */ new Set();
2328
+ function RowActions({
2329
+ node,
2330
+ state,
2331
+ editable,
2332
+ canAddHere,
2333
+ inUseDefs = EMPTY_SET
2334
+ }) {
2335
+ const reorderable = editable && node.slotKind !== "conditional" && node.slotKind !== "definition";
2336
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2337
+ reorderable && /* @__PURE__ */ jsx2(MoveActions, { node, state }),
2338
+ /* @__PURE__ */ jsx2(
2339
+ AddActions,
2340
+ {
2341
+ node,
2342
+ state,
2343
+ editable,
2344
+ canAddHere
2345
+ }
2346
+ ),
2347
+ editable && /* @__PURE__ */ jsx2(RemoveAction, { node, state, inUseDefs })
1128
2348
  ] });
1129
2349
  }
1130
2350
  function PropertyRow({
1131
2351
  node,
1132
2352
  state,
1133
- depth
2353
+ depth,
2354
+ inUseDefs = EMPTY_SET
1134
2355
  }) {
1135
2356
  const selected = samePath(state.selection, node.path);
1136
2357
  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;
2358
+ const editable = !node.readOnly && state.canEditProperty(node.path);
2359
+ const isBranch = node.slotKind === "branch";
2360
+ const isConditional = node.slotKind === "conditional";
2361
+ const isDefinition = node.kind === "definition";
2362
+ const editableName = isDefinition ? !state.capabilities.locked : editable;
2363
+ const canAddHere = canAddChildHere(node, state, isConditional);
2364
+ const showActions = editableName || canAddHere;
1140
2365
  const startRename = (e) => {
1141
2366
  e.stopPropagation();
1142
- if (editable) setEditing(true);
2367
+ if (editableName && !isBranch && !isConditional) setEditing(true);
2368
+ };
2369
+ const commitRename = (next) => {
2370
+ setEditing(false);
2371
+ if (isDefinition) state.renameDefinition(node.key, next);
2372
+ else state.renameProperty(node.path, next);
1143
2373
  };
1144
2374
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
1145
2375
  /* @__PURE__ */ jsxs2(
@@ -1156,17 +2386,14 @@ function PropertyRow({
1156
2386
  NameEditor,
1157
2387
  {
1158
2388
  initial: node.key,
1159
- onCommit: (next) => {
1160
- setEditing(false);
1161
- state.renameProperty(node.path, next);
1162
- },
2389
+ onCommit: commitRename,
1163
2390
  onCancel: () => setEditing(false)
1164
2391
  }
1165
2392
  ) : /* @__PURE__ */ jsx2(
1166
2393
  RowLabel,
1167
2394
  {
1168
2395
  node,
1169
- editable,
2396
+ editable: editableName,
1170
2397
  onStartRename: startRename
1171
2398
  }
1172
2399
  ),
@@ -1187,8 +2414,9 @@ function PropertyRow({
1187
2414
  {
1188
2415
  node,
1189
2416
  state,
1190
- editable,
1191
- canAddHere
2417
+ editable: editableName,
2418
+ canAddHere,
2419
+ inUseDefs
1192
2420
  }
1193
2421
  )
1194
2422
  }
@@ -1201,7 +2429,8 @@ function PropertyRow({
1201
2429
  {
1202
2430
  node: child,
1203
2431
  state,
1204
- depth: depth + 1
2432
+ depth: depth + 1,
2433
+ inUseDefs
1205
2434
  },
1206
2435
  pathKey(child.path)
1207
2436
  ))
@@ -1209,29 +2438,27 @@ function PropertyRow({
1209
2438
  }
1210
2439
 
1211
2440
  // src/preview/PreviewPane.tsx
1212
- import { useMemo as useMemo4, useState as useState5 } from "react";
1213
- import { Check, Copy } from "lucide-react";
2441
+ import { useMemo as useMemo7, useState as useState6 } from "react";
1214
2442
  import {
1215
- Button as Button3,
1216
2443
  Tabs,
1217
2444
  TabsList,
1218
2445
  TabsTrigger,
1219
2446
  TabsContent,
1220
2447
  TabsSeparator
1221
2448
  } from "@petrarca/sonnet-ui";
1222
- import { JsonEditor as JsonEditor2 } from "@petrarca/sonnet-ui/json-editor";
1223
2449
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer3 } from "@petrarca/sonnet-forms";
1224
- import { extractSchemaDefaults } from "@petrarca/sonnet-core/schema";
1225
- import { copyTextToClipboard } from "@petrarca/sonnet-core";
2450
+ import {
2451
+ extractSchemaDefaults
2452
+ } from "@petrarca/sonnet-core/schema";
1226
2453
 
1227
2454
  // src/preview/TablePreview.tsx
1228
- import { useMemo as useMemo2 } from "react";
2455
+ import { useMemo as useMemo3 } from "react";
1229
2456
  import { EntityTable } from "@petrarca/sonnet-ui";
1230
2457
  import { toPageResult } from "@petrarca/sonnet-core";
1231
2458
 
1232
2459
  // src/preview/sampleRows.ts
1233
- function sampleString(prop, key, i) {
1234
- switch (prop.format) {
2460
+ function sampleString(prop2, key, i) {
2461
+ switch (prop2.format) {
1235
2462
  case "email":
1236
2463
  return `user${i + 1}@example.com`;
1237
2464
  case "date":
@@ -1251,13 +2478,13 @@ function titleCase(key) {
1251
2478
  const s = key.replace(/[_-]+/g, " ").trim();
1252
2479
  return s.charAt(0).toUpperCase() + s.slice(1);
1253
2480
  }
1254
- function sampleValue(prop, key, i) {
1255
- if (Array.isArray(prop.enum) && prop.enum.length > 0) {
1256
- return prop.enum[i % prop.enum.length];
2481
+ function sampleValue(prop2, key, i) {
2482
+ if (Array.isArray(prop2.enum) && prop2.enum.length > 0) {
2483
+ return prop2.enum[i % prop2.enum.length];
1257
2484
  }
1258
- switch (prop.type) {
2485
+ switch (prop2.type) {
1259
2486
  case "string":
1260
- return sampleString(prop, key, i);
2487
+ return sampleString(prop2, key, i);
1261
2488
  case "number":
1262
2489
  return Math.round((i + 1) * 1.5 * 100) / 100;
1263
2490
  case "integer":
@@ -1276,8 +2503,8 @@ function sampleRows(schema, count = 3) {
1276
2503
  const entries = Object.entries(schema.properties);
1277
2504
  return Array.from({ length: count }, (_, i) => {
1278
2505
  const row = {};
1279
- for (const [key, prop] of entries) {
1280
- row[key] = sampleValue(prop, key, i);
2506
+ for (const [key, prop2] of entries) {
2507
+ row[key] = sampleValue(prop2, key, i);
1281
2508
  }
1282
2509
  return row;
1283
2510
  });
@@ -1289,7 +2516,7 @@ function TablePreview({
1289
2516
  schema,
1290
2517
  uiSchema
1291
2518
  }) {
1292
- const data = useMemo2(() => toPageResult(sampleRows(schema)), [schema]);
2519
+ const data = useMemo3(() => toPageResult(sampleRows(schema)), [schema]);
1293
2520
  return /* @__PURE__ */ jsx3(
1294
2521
  EntityTable,
1295
2522
  {
@@ -1304,17 +2531,282 @@ function TablePreview({
1304
2531
  }
1305
2532
 
1306
2533
  // 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";
2534
+ import { useMemo as useMemo5, useState as useState4 } from "react";
2535
+ import { cn as cn2 } from "@petrarca/sonnet-core";
2536
+ import { Input as Input3, IconButton as IconButton2, Button } from "@petrarca/sonnet-ui";
2537
+ import { FormSelect as FormSelect3, JsonSchemaFormRenderer as JsonSchemaFormRenderer2 } from "@petrarca/sonnet-forms";
1310
2538
  import {
1311
2539
  Plus as Plus2,
1312
2540
  Trash2 as Trash22,
1313
2541
  ChevronUp as ChevronUp2,
1314
2542
  ChevronDown as ChevronDown2,
2543
+ ChevronLeft,
2544
+ ChevronRight as ChevronRight2,
1315
2545
  GripVertical
1316
2546
  } from "lucide-react";
2547
+
2548
+ // src/inspector/RuleFields.tsx
2549
+ import { useEffect as useEffect2, useMemo as useMemo4, useState as useState3 } from "react";
2550
+ import { FormSelect as FormSelect2 } from "@petrarca/sonnet-forms";
2551
+
2552
+ // src/model/conditions.ts
2553
+ var CONDITION_OPERATORS = [
2554
+ "=",
2555
+ "!=",
2556
+ ">",
2557
+ ">=",
2558
+ "<",
2559
+ "<=",
2560
+ "exists"
2561
+ ];
2562
+ var CONDITION_OPERATOR_LABELS = {
2563
+ "=": "equals",
2564
+ "!=": "not equals",
2565
+ ">": "greater than",
2566
+ ">=": "greater or equal",
2567
+ "<": "less than",
2568
+ "<=": "less or equal",
2569
+ exists: "exists"
2570
+ };
2571
+ var OPERATOR_OPTIONS = CONDITION_OPERATORS.map((op) => ({
2572
+ value: op,
2573
+ label: CONDITION_OPERATOR_LABELS[op]
2574
+ }));
2575
+ var NUMERIC_OPS = /* @__PURE__ */ new Set([
2576
+ ">",
2577
+ ">=",
2578
+ "<",
2579
+ "<="
2580
+ ]);
2581
+ function coerceValue(value) {
2582
+ if (value.trim() === "") return value;
2583
+ const n = Number(value);
2584
+ return Number.isFinite(n) ? n : value;
2585
+ }
2586
+ function coerceForScope(value, operator, type) {
2587
+ if (NUMERIC_OPS.has(operator)) return coerceValue(value);
2588
+ if (type === "boolean") return value === "true";
2589
+ if (type === "number" || type === "integer") return coerceValue(value);
2590
+ return value;
2591
+ }
2592
+ function operatorToConstraint(operator, value, type) {
2593
+ const v = coerceForScope(value, operator, type);
2594
+ switch (operator) {
2595
+ case "=":
2596
+ return { const: v };
2597
+ case "!=":
2598
+ return { not: { const: v } };
2599
+ case ">":
2600
+ return { exclusiveMinimum: v };
2601
+ case ">=":
2602
+ return { minimum: v };
2603
+ case "<":
2604
+ return { exclusiveMaximum: v };
2605
+ case "<=":
2606
+ return { maximum: v };
2607
+ case "exists":
2608
+ return {};
2609
+ }
2610
+ }
2611
+ function conditionToIf(condition2, type) {
2612
+ const { field, operator } = condition2;
2613
+ if (!field) return void 0;
2614
+ if (operator === "exists") {
2615
+ return { required: [field] };
2616
+ }
2617
+ return {
2618
+ properties: {
2619
+ [field]: operatorToConstraint(operator, condition2.value, type)
2620
+ },
2621
+ required: [field]
2622
+ };
2623
+ }
2624
+ function constraintToOperator(c) {
2625
+ if (c.not && "const" in c.not)
2626
+ return { operator: "!=", value: String(c.not.const) };
2627
+ if (c.exclusiveMinimum !== void 0)
2628
+ return { operator: ">", value: String(c.exclusiveMinimum) };
2629
+ if (c.minimum !== void 0)
2630
+ return { operator: ">=", value: String(c.minimum) };
2631
+ if (c.exclusiveMaximum !== void 0)
2632
+ return { operator: "<", value: String(c.exclusiveMaximum) };
2633
+ if (c.maximum !== void 0)
2634
+ return { operator: "<=", value: String(c.maximum) };
2635
+ if ("const" in c) return { operator: "=", value: String(c.const) };
2636
+ return { operator: "exists", value: "" };
2637
+ }
2638
+ function ifToCondition(ifSchema) {
2639
+ const empty = { field: "", operator: "=", value: "" };
2640
+ if (!ifSchema) return empty;
2641
+ const props = ifSchema.properties ?? {};
2642
+ const field = Object.keys(props)[0] ?? ifSchema.required?.[0] ?? "";
2643
+ if (!field) return empty;
2644
+ const constraint = props[field];
2645
+ if (!constraint) return { field, operator: "exists", value: "" };
2646
+ const { operator, value } = constraintToOperator(constraint);
2647
+ return { field, operator, value };
2648
+ }
2649
+
2650
+ // src/inspector/OperatorValueInput.tsx
2651
+ import { Input as Input2 } from "@petrarca/sonnet-ui";
1317
2652
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2653
+ function OperatorValueInput({
2654
+ id,
2655
+ cacheKey,
2656
+ value,
2657
+ onCommit,
2658
+ disabled
2659
+ }) {
2660
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
2661
+ /* @__PURE__ */ jsx4("label", { htmlFor: id, className: "text-sm font-medium", children: "Value" }),
2662
+ /* @__PURE__ */ jsx4(
2663
+ Input2,
2664
+ {
2665
+ id,
2666
+ defaultValue: value,
2667
+ onBlur: (e) => onCommit(e.target.value),
2668
+ disabled,
2669
+ placeholder: "Value to compare"
2670
+ },
2671
+ cacheKey
2672
+ )
2673
+ ] });
2674
+ }
2675
+
2676
+ // src/inspector/RuleFields.tsx
2677
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2678
+ function ruleToForm(rule) {
2679
+ if (!rule) return { effect: "", scope: "", operator: "=", value: "" };
2680
+ const { operator, value } = constraintToOperator(rule.condition.schema);
2681
+ return { effect: rule.effect, scope: rule.condition.scope, operator, value };
2682
+ }
2683
+ function formToRule(form, scopeType) {
2684
+ if (!form.effect || !form.scope) return void 0;
2685
+ return {
2686
+ effect: form.effect,
2687
+ condition: {
2688
+ scope: form.scope,
2689
+ schema: operatorToConstraint(form.operator, form.value, scopeType)
2690
+ }
2691
+ };
2692
+ }
2693
+ var BOOLEAN_VALUE_OPTIONS = [
2694
+ { value: "true", label: "true" },
2695
+ { value: "false", label: "false" }
2696
+ ];
2697
+ function RuleFields({
2698
+ rule,
2699
+ scopes,
2700
+ effects,
2701
+ idPrefix,
2702
+ disabled = false,
2703
+ onChange
2704
+ }) {
2705
+ const [form, setForm] = useState3(() => ruleToForm(rule));
2706
+ const seed = JSON.stringify(rule ?? null);
2707
+ useEffect2(() => {
2708
+ setForm(ruleToForm(JSON.parse(seed) ?? void 0));
2709
+ }, [seed]);
2710
+ const scopeType = useMemo4(
2711
+ () => scopes.find((s) => s.pointer === form.scope)?.type,
2712
+ [scopes, form.scope]
2713
+ );
2714
+ const commit = (next) => {
2715
+ setForm(next);
2716
+ const nextType = scopes.find((s) => s.pointer === next.scope)?.type;
2717
+ const built = formToRule(next, nextType);
2718
+ if (built || next.effect === "") onChange(built);
2719
+ };
2720
+ const scopeOptions = useMemo4(
2721
+ () => scopes.map((s) => ({ value: s.pointer, label: s.pointer.slice(1) })),
2722
+ [scopes]
2723
+ );
2724
+ const needsValue = form.operator !== "exists";
2725
+ const isBoolean = scopeType === "boolean";
2726
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-3", children: [
2727
+ /* @__PURE__ */ jsx5(
2728
+ FormSelect2,
2729
+ {
2730
+ id: `${idPrefix}-effect`,
2731
+ name: "effect",
2732
+ label: "Effect",
2733
+ options: effects,
2734
+ value: form.effect,
2735
+ onChange: (v) => commit({ ...form, effect: v ?? "" }),
2736
+ clearable: false,
2737
+ disabled
2738
+ }
2739
+ ),
2740
+ form.effect !== "" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
2741
+ /* @__PURE__ */ jsx5(
2742
+ FormSelect2,
2743
+ {
2744
+ id: `${idPrefix}-scope`,
2745
+ name: "scope",
2746
+ label: "When field",
2747
+ options: scopeOptions,
2748
+ value: form.scope || null,
2749
+ onChange: (v) => commit({ ...form, scope: v ?? "" }),
2750
+ placeholder: "Select a field...",
2751
+ clearable: false,
2752
+ disabled
2753
+ }
2754
+ ),
2755
+ /* @__PURE__ */ jsx5(
2756
+ FormSelect2,
2757
+ {
2758
+ id: `${idPrefix}-operator`,
2759
+ name: "operator",
2760
+ label: "Operator",
2761
+ options: OPERATOR_OPTIONS,
2762
+ value: form.operator,
2763
+ onChange: (v) => commit({ ...form, operator: v ?? "=" }),
2764
+ clearable: false,
2765
+ disabled
2766
+ }
2767
+ ),
2768
+ needsValue && (isBoolean ? /* @__PURE__ */ jsx5(
2769
+ FormSelect2,
2770
+ {
2771
+ id: `${idPrefix}-value`,
2772
+ name: "value",
2773
+ label: "Value",
2774
+ options: BOOLEAN_VALUE_OPTIONS,
2775
+ value: form.value === "true" ? "true" : "false",
2776
+ onChange: (v) => commit({ ...form, value: v ?? "false" }),
2777
+ clearable: false,
2778
+ disabled: disabled || !form.scope
2779
+ }
2780
+ ) : /* @__PURE__ */ jsx5(
2781
+ OperatorValueInput,
2782
+ {
2783
+ id: `${idPrefix}-value`,
2784
+ cacheKey: `${form.scope}:${form.operator}`,
2785
+ value: form.value,
2786
+ onCommit: (v) => commit({ ...form, value: v }),
2787
+ disabled: disabled || !form.scope
2788
+ }
2789
+ ))
2790
+ ] })
2791
+ ] });
2792
+ }
2793
+
2794
+ // src/inspector/ruleEffects.ts
2795
+ var VISIBILITY_EFFECT_OPTIONS = [
2796
+ { value: "", label: "None (always shown)" },
2797
+ { value: "show", label: "Show when" },
2798
+ { value: "hide", label: "Hide when" }
2799
+ ];
2800
+ var FIELD_EFFECT_OPTIONS = [
2801
+ { value: "", label: "None (always visible)" },
2802
+ { value: "show", label: "Show when" },
2803
+ { value: "hide", label: "Hide when" },
2804
+ { value: "enable", label: "Enable when" },
2805
+ { value: "disable", label: "Disable when" }
2806
+ ];
2807
+
2808
+ // src/designers/FormLayoutDesigner.tsx
2809
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1318
2810
  var DRAG_MIME = "application/x-sonnet-property";
1319
2811
  function orderedKeysOf(properties, order) {
1320
2812
  const keys = Object.keys(properties);
@@ -1323,21 +2815,43 @@ function orderedKeysOf(properties, order) {
1323
2815
  return [...inOrder, ...keys.filter((k) => !inOrder.includes(k))];
1324
2816
  }
1325
2817
  function scopeTarget(schema, scopePath) {
1326
- const prop = getProperty(schema, scopePath);
1327
- if (!prop) return void 0;
1328
- return prop.type === "array" ? prop.items : prop;
2818
+ const prop2 = getProperty(schema, scopePath);
2819
+ if (!prop2) return void 0;
2820
+ return prop2.type === "array" ? prop2.items : prop2;
2821
+ }
2822
+ function scopeContainer(schema, uiSchema, scopePath) {
2823
+ if (scopePath.length === 0) {
2824
+ return {
2825
+ container: schema,
2826
+ layout: uiSchema["x-ui-layout"]
2827
+ };
2828
+ }
2829
+ const container = scopeTarget(schema, scopePath);
2830
+ return {
2831
+ container,
2832
+ layout: container?.["x-ui-layout"]
2833
+ };
1329
2834
  }
1330
2835
  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);
2836
+ const { container, layout } = scopeContainer(schema, uiSchema, scopePath);
2837
+ const props = container?.properties ?? {};
1334
2838
  return {
1335
- keys: orderedKeysOf(container?.properties ?? {}, container?.["x-ui-order"]),
2839
+ keys: orderedKeysOf(props, container?.["x-ui-order"]),
2840
+ props,
1336
2841
  rows: layout?.rows ?? [],
2842
+ groups: layout?.groups ?? [],
1337
2843
  layout,
1338
- isArray
2844
+ isArray: scopeIsArray(schema, scopePath)
1339
2845
  };
1340
2846
  }
2847
+ function ownsField(rows, key) {
2848
+ return rows.some((r) => r.fields.includes(key));
2849
+ }
2850
+ function rowTarget(grouped, groups, groupIndex, flatRows) {
2851
+ const rows = grouped ? groups[groupIndex]?.rows ?? [] : flatRows;
2852
+ const all = grouped ? groups.flatMap((t) => t.rows) : flatRows;
2853
+ return { rows, assigned: new Set(all.flatMap((r) => r.fields)) };
2854
+ }
1341
2855
  function scopeIsArray(schema, scopePath) {
1342
2856
  if (scopePath.length === 0) return false;
1343
2857
  return getProperty(schema, scopePath)?.type === "array";
@@ -1377,9 +2891,9 @@ function ArrayItemLayoutBar({
1377
2891
  readOnly,
1378
2892
  onChange
1379
2893
  }) {
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(
2894
+ return /* @__PURE__ */ jsxs5("div", { className: "shrink-0 rounded-lg border p-3", children: [
2895
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta mb-2", children: "Array item layout" }),
2896
+ /* @__PURE__ */ jsx6(
1383
2897
  JsonSchemaFormRenderer2,
1384
2898
  {
1385
2899
  schema: SCALAR_META_SCHEMA,
@@ -1396,23 +2910,37 @@ function ArrayItemLayoutBar({
1396
2910
  function FormLayoutDesigner({
1397
2911
  state
1398
2912
  }) {
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];
2913
+ const scopes = useMemo5(() => layoutScopes(state.schema), [state.schema]);
2914
+ const [scopePath, setScopePath] = useState4([]);
2915
+ const scopeKey = pathKey(scopePath);
2916
+ const activeScope = scopes.find((s) => pathKey(s.path) === scopeKey) ?? scopes[0];
1403
2917
  const effectivePath = activeScope.path;
1404
2918
  const {
1405
2919
  keys: allKeys,
1406
- rows,
2920
+ props: scopeProps,
2921
+ rows: flatRows,
2922
+ groups,
1407
2923
  layout,
1408
2924
  isArray
1409
2925
  } = resolveScope(state.schema, state.uiSchema, effectivePath);
1410
- const assigned = new Set(rows.flatMap((r) => r.fields));
2926
+ const grouped = groups.length > 0;
2927
+ const [activeGroup, setActiveGroup] = useState4(0);
2928
+ const groupIndex = grouped ? Math.min(activeGroup, groups.length - 1) : 0;
2929
+ const { rows, assigned } = rowTarget(grouped, groups, groupIndex, flatRows);
1411
2930
  const available = allKeys.filter((k) => !assigned.has(k));
1412
- const [dragKey, setDragKey] = useState3(null);
1413
- const commit = (next) => state.setScopedLayoutRows(effectivePath, next);
2931
+ const inOtherGroups = grouped ? allKeys.filter((k) => assigned.has(k) && !ownsField(rows, k)) : [];
2932
+ const [dragKey, setDragKey] = useState4(null);
2933
+ const commit = makeCommitRows(state, effectivePath, grouped, groupIndex);
1414
2934
  const place = (key, rowIndex) => {
1415
2935
  if (!key) return;
2936
+ if (grouped) {
2937
+ state.placeFieldInGroup(
2938
+ effectivePath,
2939
+ key,
2940
+ rowIndex === null ? null : { groupIndex, rowIndex }
2941
+ );
2942
+ return;
2943
+ }
1416
2944
  const next = rows.map((r) => ({
1417
2945
  ...r,
1418
2946
  fields: r.fields.filter((f) => f !== key)
@@ -1466,26 +2994,57 @@ function FormLayoutDesigner({
1466
2994
  });
1467
2995
  commit(next);
1468
2996
  };
2997
+ const scopeOptions = allKeys.map((k) => ({
2998
+ pointer: `/${k}`,
2999
+ type: scopeProps[k]?.type
3000
+ }));
3001
+ const groupOps = makeGroupOps(
3002
+ state,
3003
+ effectivePath,
3004
+ groups.length,
3005
+ groupIndex,
3006
+ setActiveGroup
3007
+ );
1469
3008
  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,
3009
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-3 h-full min-h-0", children: [
3010
+ /* @__PURE__ */ jsx6(
3011
+ LayoutToolbar,
3012
+ {
3013
+ scopes,
3014
+ scopePath: effectivePath,
3015
+ onScope: setScopePath,
3016
+ grouped,
3017
+ variant: layout?.variant,
3018
+ readOnly,
3019
+ onMode: groupOps.setMode,
3020
+ onVariant: (v) => state.setScopedLayoutVariant(effectivePath, v)
3021
+ }
3022
+ ),
3023
+ grouped && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3024
+ /* @__PURE__ */ jsx6(
3025
+ GroupBar,
1475
3026
  {
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
3027
+ groups,
3028
+ activeIndex: groupIndex,
3029
+ readOnly,
3030
+ onSelect: setActiveGroup,
3031
+ onAdd: groupOps.add,
3032
+ onRename: groupOps.rename,
3033
+ onRemove: groupOps.remove,
3034
+ onMove: groupOps.move
1485
3035
  }
1486
- ) })
3036
+ ),
3037
+ /* @__PURE__ */ jsx6(
3038
+ GroupSettings,
3039
+ {
3040
+ group: groups[groupIndex],
3041
+ scopes: scopeOptions,
3042
+ readOnly,
3043
+ onRule: (rule) => state.setLayoutGroupRule(effectivePath, groupIndex, rule)
3044
+ }
3045
+ )
1487
3046
  ] }),
1488
- isArray && /* @__PURE__ */ jsx4(
3047
+ isArray && /* @__PURE__ */ jsx6(
1489
3048
  ArrayItemLayoutBar,
1490
3049
  {
1491
3050
  layout,
@@ -1493,38 +3052,24 @@ function FormLayoutDesigner({
1493
3052
  onChange: (opts) => state.setScopedLayoutOptions(effectivePath, opts)
1494
3053
  }
1495
3054
  ),
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(
3055
+ /* @__PURE__ */ jsxs5("div", { className: "flex gap-4 flex-1 min-h-0", children: [
3056
+ /* @__PURE__ */ jsx6(
3057
+ AvailablePool,
3058
+ {
3059
+ available,
3060
+ inOtherGroups,
3061
+ grouped,
3062
+ readOnly,
3063
+ dragActive: !!dragKey,
3064
+ onDropToPool: (key) => place(key, null),
3065
+ onDragStartKey: setDragKey,
3066
+ onDragEndKey: () => setDragKey(null)
3067
+ }
3068
+ ),
3069
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [
3070
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3071
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta", children: rowsHeading(grouped, groups, groupIndex) }),
3072
+ !readOnly && /* @__PURE__ */ jsx6("span", { className: "ml-auto", children: /* @__PURE__ */ jsxs5(
1528
3073
  Button,
1529
3074
  {
1530
3075
  size: "sm",
@@ -1532,13 +3077,13 @@ function FormLayoutDesigner({
1532
3077
  className: "gap-1.5",
1533
3078
  onClick: addRow,
1534
3079
  children: [
1535
- /* @__PURE__ */ jsx4(Plus2, { className: "h-4 w-4" }),
3080
+ /* @__PURE__ */ jsx6(Plus2, { className: "h-4 w-4" }),
1536
3081
  "Add row"
1537
3082
  ]
1538
3083
  }
1539
3084
  ) })
1540
3085
  ] }),
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(
3086
+ /* @__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
3087
  RowCard,
1543
3088
  {
1544
3089
  row,
@@ -1564,6 +3109,352 @@ function FormLayoutDesigner({
1564
3109
  ] })
1565
3110
  ] });
1566
3111
  }
3112
+ function makeCommitRows(state, scopePath, grouped, groupIndex) {
3113
+ if (grouped) {
3114
+ return (rows) => state.setScopedGroupRows(scopePath, groupIndex, rows);
3115
+ }
3116
+ return (rows) => state.setScopedLayoutRows(scopePath, rows);
3117
+ }
3118
+ function rowsHeading(grouped, groups, groupIndex) {
3119
+ if (!grouped) return "Rows";
3120
+ return `Rows in ${groups[groupIndex]?.label ?? ""}`;
3121
+ }
3122
+ var VARIANT_OPTIONS = [
3123
+ { value: "tabs", label: "Tabs" },
3124
+ { value: "stepper", label: "Stepper" },
3125
+ { value: "sections", label: "Sections" }
3126
+ ];
3127
+ function makeGroupOps(state, scopePath, groupCount, activeIndex, setActiveGroup) {
3128
+ return {
3129
+ add: () => {
3130
+ state.addLayoutGroup(scopePath);
3131
+ setActiveGroup(groupCount);
3132
+ },
3133
+ rename: (index, label) => state.renameLayoutGroup(scopePath, index, label),
3134
+ move: (index, direction) => {
3135
+ state.moveLayoutGroup(scopePath, index, direction);
3136
+ if (index === activeIndex) setActiveGroup(activeIndex + direction);
3137
+ },
3138
+ remove: (index) => {
3139
+ state.removeLayoutGroup(scopePath, index);
3140
+ if (index <= activeIndex && activeIndex > 0)
3141
+ setActiveGroup(activeIndex - 1);
3142
+ },
3143
+ setMode: (mode) => {
3144
+ state.setScopedLayoutMode(scopePath, mode);
3145
+ setActiveGroup(0);
3146
+ }
3147
+ };
3148
+ }
3149
+ function AvailablePool({
3150
+ available,
3151
+ inOtherGroups,
3152
+ grouped,
3153
+ readOnly,
3154
+ dragActive,
3155
+ onDropToPool,
3156
+ onDragStartKey,
3157
+ onDragEndKey
3158
+ }) {
3159
+ const empty = available.length === 0 && inOtherGroups.length === 0;
3160
+ return /* @__PURE__ */ jsxs5("div", { className: "w-56 shrink-0 flex flex-col min-h-0", children: [
3161
+ /* @__PURE__ */ jsx6("p", { className: "heading-meta mb-2", children: "Available" }),
3162
+ /* @__PURE__ */ jsxs5(
3163
+ "div",
3164
+ {
3165
+ className: "flex-1 min-h-0 overflow-auto rounded-lg border p-2 space-y-1 bg-muted/20",
3166
+ onDragOver: (e) => {
3167
+ if (dragActive) e.preventDefault();
3168
+ },
3169
+ onDrop: (e) => {
3170
+ const key = e.dataTransfer.getData(DRAG_MIME);
3171
+ if (key) onDropToPool(key);
3172
+ },
3173
+ children: [
3174
+ 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(
3175
+ Chip,
3176
+ {
3177
+ label: key,
3178
+ readOnly,
3179
+ onDragStart: () => onDragStartKey(key),
3180
+ onDragEnd: onDragEndKey
3181
+ },
3182
+ key
3183
+ )),
3184
+ inOtherGroups.length > 0 && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3185
+ /* @__PURE__ */ jsx6("p", { className: "pt-2 pb-1 text-[11px] uppercase tracking-wide text-muted-foreground/70", children: "In other groups" }),
3186
+ inOtherGroups.map((key) => /* @__PURE__ */ jsx6(
3187
+ Chip,
3188
+ {
3189
+ label: key,
3190
+ readOnly,
3191
+ muted: true,
3192
+ onDragStart: () => onDragStartKey(key),
3193
+ onDragEnd: onDragEndKey
3194
+ },
3195
+ key
3196
+ ))
3197
+ ] })
3198
+ ]
3199
+ }
3200
+ ),
3201
+ /* @__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." })
3202
+ ] });
3203
+ }
3204
+ function LayoutToolbar({
3205
+ scopes,
3206
+ scopePath,
3207
+ onScope,
3208
+ grouped,
3209
+ variant,
3210
+ readOnly,
3211
+ onMode,
3212
+ onVariant
3213
+ }) {
3214
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 shrink-0", children: [
3215
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta", children: "Scope" }),
3216
+ /* @__PURE__ */ jsx6("div", { className: "w-64", children: /* @__PURE__ */ jsx6(
3217
+ FormSelect3,
3218
+ {
3219
+ id: "layout-scope",
3220
+ name: "scope",
3221
+ value: pathKey(scopePath),
3222
+ options: scopes.map((s) => ({
3223
+ value: pathKey(s.path),
3224
+ label: s.label
3225
+ })),
3226
+ onChange: (v) => onScope(v ? parsePathKey(v) : []),
3227
+ clearable: false
3228
+ }
3229
+ ) }),
3230
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta ml-4", children: "Group by" }),
3231
+ /* @__PURE__ */ jsx6(
3232
+ ModeToggle,
3233
+ {
3234
+ mode: grouped ? "groups" : "rows",
3235
+ readOnly,
3236
+ onChange: onMode
3237
+ }
3238
+ ),
3239
+ grouped && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3240
+ /* @__PURE__ */ jsx6("label", { className: "heading-meta ml-4", children: "Show as" }),
3241
+ /* @__PURE__ */ jsx6("div", { className: "w-36", children: /* @__PURE__ */ jsx6(
3242
+ FormSelect3,
3243
+ {
3244
+ id: "layout-variant",
3245
+ name: "variant",
3246
+ value: variant ?? "tabs",
3247
+ options: VARIANT_OPTIONS,
3248
+ onChange: (v) => onVariant(v ?? "tabs"),
3249
+ clearable: false,
3250
+ disabled: readOnly
3251
+ }
3252
+ ) })
3253
+ ] })
3254
+ ] });
3255
+ }
3256
+ function ModeToggle({
3257
+ mode,
3258
+ readOnly,
3259
+ onChange
3260
+ }) {
3261
+ return /* @__PURE__ */ jsx6("div", { className: "inline-flex rounded-md border p-0.5", "aria-label": "Group by", children: ["rows", "groups"].map((m) => /* @__PURE__ */ jsx6(
3262
+ "button",
3263
+ {
3264
+ type: "button",
3265
+ disabled: readOnly,
3266
+ "aria-pressed": mode === m,
3267
+ onClick: () => onChange(m),
3268
+ className: cn2(
3269
+ "rounded px-2 py-0.5 text-xs capitalize disabled:opacity-50",
3270
+ mode === m ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:text-foreground"
3271
+ ),
3272
+ children: m
3273
+ },
3274
+ m
3275
+ )) });
3276
+ }
3277
+ function GroupSettings({
3278
+ group,
3279
+ scopes,
3280
+ readOnly,
3281
+ onRule
3282
+ }) {
3283
+ const [open, setOpen] = useState4(false);
3284
+ if (!group) return null;
3285
+ const hasRule = !!group.rule;
3286
+ return /* @__PURE__ */ jsxs5("div", { className: "shrink-0 rounded-lg border px-3 py-2", children: [
3287
+ /* @__PURE__ */ jsxs5(
3288
+ "button",
3289
+ {
3290
+ type: "button",
3291
+ className: "flex w-full items-center gap-1 heading-meta",
3292
+ onClick: () => setOpen((v) => !v),
3293
+ "aria-expanded": open,
3294
+ children: [
3295
+ open ? /* @__PURE__ */ jsx6(ChevronDown2, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx6(ChevronRight2, { className: "h-3 w-3" }),
3296
+ "Visibility of ",
3297
+ group.label,
3298
+ /* @__PURE__ */ jsx6("span", { className: "ml-1 normal-case text-muted-foreground/70", children: hasRule ? "conditional" : "always shown" })
3299
+ ]
3300
+ }
3301
+ ),
3302
+ open && /* @__PURE__ */ jsx6("div", { className: "mt-3 max-w-sm", children: /* @__PURE__ */ jsx6(
3303
+ RuleFields,
3304
+ {
3305
+ rule: group.rule,
3306
+ scopes,
3307
+ effects: VISIBILITY_EFFECT_OPTIONS,
3308
+ idPrefix: "group-rule",
3309
+ disabled: readOnly,
3310
+ onChange: (rule) => onRule(rule)
3311
+ }
3312
+ ) })
3313
+ ] });
3314
+ }
3315
+ function GroupBar({
3316
+ groups,
3317
+ activeIndex,
3318
+ readOnly,
3319
+ onSelect,
3320
+ onAdd,
3321
+ onRename,
3322
+ onRemove,
3323
+ onMove
3324
+ }) {
3325
+ return /* @__PURE__ */ jsxs5(
3326
+ "div",
3327
+ {
3328
+ role: "tablist",
3329
+ "aria-label": "Layout groups",
3330
+ className: "shrink-0 flex items-center gap-1 rounded-lg border p-1.5 overflow-x-auto",
3331
+ children: [
3332
+ groups.map((group, i) => /* @__PURE__ */ jsx6(
3333
+ GroupChip,
3334
+ {
3335
+ group,
3336
+ active: i === activeIndex,
3337
+ readOnly,
3338
+ onSelect: () => onSelect(i),
3339
+ onRename: (label) => onRename(i, label),
3340
+ onRemove: () => onRemove(i),
3341
+ onMoveLeft: i > 0 ? () => onMove(i, -1) : void 0,
3342
+ onMoveRight: i < groups.length - 1 ? () => onMove(i, 1) : void 0
3343
+ },
3344
+ `${i}:${group.label}`
3345
+ )),
3346
+ !readOnly && /* @__PURE__ */ jsxs5(
3347
+ Button,
3348
+ {
3349
+ size: "sm",
3350
+ variant: "outline",
3351
+ className: "gap-1.5 shrink-0",
3352
+ onClick: onAdd,
3353
+ children: [
3354
+ /* @__PURE__ */ jsx6(Plus2, { className: "h-4 w-4" }),
3355
+ "Add group"
3356
+ ]
3357
+ }
3358
+ )
3359
+ ]
3360
+ }
3361
+ );
3362
+ }
3363
+ function GroupChip({
3364
+ group,
3365
+ active,
3366
+ readOnly,
3367
+ onSelect,
3368
+ onRename,
3369
+ onRemove,
3370
+ onMoveLeft,
3371
+ onMoveRight
3372
+ }) {
3373
+ const [editing, setEditing] = useState4(false);
3374
+ if (editing) {
3375
+ return /* @__PURE__ */ jsx6(
3376
+ Input3,
3377
+ {
3378
+ autoFocus: true,
3379
+ defaultValue: group.label,
3380
+ className: "h-7 w-32 text-sm",
3381
+ onBlur: (e) => {
3382
+ const next = e.target.value.trim();
3383
+ if (next && next !== group.label) onRename(next);
3384
+ setEditing(false);
3385
+ },
3386
+ onKeyDown: (e) => {
3387
+ if (e.key === "Enter") e.currentTarget.blur();
3388
+ if (e.key === "Escape") setEditing(false);
3389
+ }
3390
+ }
3391
+ );
3392
+ }
3393
+ const fieldCount = group.rows.reduce((n, r) => n + r.fields.length, 0);
3394
+ return /* @__PURE__ */ jsxs5(
3395
+ "span",
3396
+ {
3397
+ className: cn2(
3398
+ "group/group inline-flex shrink-0 items-center gap-1 rounded-md border pl-2 pr-1 py-0.5 text-sm",
3399
+ active ? "bg-accent border-accent-foreground/20" : "hover:bg-muted"
3400
+ ),
3401
+ children: [
3402
+ /* @__PURE__ */ jsxs5(
3403
+ "button",
3404
+ {
3405
+ type: "button",
3406
+ role: "group",
3407
+ "aria-selected": active,
3408
+ className: "inline-flex items-center gap-1 py-0.5",
3409
+ onClick: onSelect,
3410
+ onDoubleClick: () => !readOnly && setEditing(true),
3411
+ onKeyDown: (e) => {
3412
+ if (e.key === "F2" && !readOnly) {
3413
+ e.preventDefault();
3414
+ setEditing(true);
3415
+ }
3416
+ },
3417
+ title: readOnly ? group.label : `${group.label} -- F2 to rename`,
3418
+ children: [
3419
+ group.label,
3420
+ /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: fieldCount })
3421
+ ]
3422
+ }
3423
+ ),
3424
+ !readOnly && /* @__PURE__ */ jsxs5("span", { className: "flex items-center opacity-0 group-hover/group:opacity-100 focus-within:opacity-100", children: [
3425
+ onMoveLeft && /* @__PURE__ */ jsx6(
3426
+ IconButton2,
3427
+ {
3428
+ label: `Move ${group.label} left`,
3429
+ revealOnHover: true,
3430
+ onClick: onMoveLeft,
3431
+ children: /* @__PURE__ */ jsx6(ChevronLeft, {})
3432
+ }
3433
+ ),
3434
+ onMoveRight && /* @__PURE__ */ jsx6(
3435
+ IconButton2,
3436
+ {
3437
+ label: `Move ${group.label} right`,
3438
+ revealOnHover: true,
3439
+ onClick: onMoveRight,
3440
+ children: /* @__PURE__ */ jsx6(ChevronRight2, {})
3441
+ }
3442
+ ),
3443
+ /* @__PURE__ */ jsx6(
3444
+ IconButton2,
3445
+ {
3446
+ label: `Remove ${group.label}`,
3447
+ tone: "destructive",
3448
+ revealOnHover: true,
3449
+ onClick: onRemove,
3450
+ children: /* @__PURE__ */ jsx6(Trash22, {})
3451
+ }
3452
+ )
3453
+ ] })
3454
+ ]
3455
+ }
3456
+ );
3457
+ }
1567
3458
  var WIDTH_LABELS = {
1568
3459
  full: "Full",
1569
3460
  sm: "Small",
@@ -1584,9 +3475,10 @@ function Chip({
1584
3475
  onDragEnd,
1585
3476
  width,
1586
3477
  onWidth,
1587
- widthDisabled
3478
+ widthDisabled,
3479
+ muted = false
1588
3480
  }) {
1589
- return /* @__PURE__ */ jsxs3(
3481
+ return /* @__PURE__ */ jsxs5(
1590
3482
  "div",
1591
3483
  {
1592
3484
  draggable: !readOnly,
@@ -1596,11 +3488,14 @@ function Chip({
1596
3488
  onDragStart();
1597
3489
  },
1598
3490
  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",
3491
+ className: cn2(
3492
+ "group/chip flex items-center gap-1 rounded-md border bg-background px-2 py-1 text-sm cursor-grab active:cursor-grabbing",
3493
+ muted && "border-dashed text-muted-foreground"
3494
+ ),
1600
3495
  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(
3496
+ /* @__PURE__ */ jsx6(GripVertical, { className: "h-3.5 w-3.5 text-muted-foreground/60 shrink-0" }),
3497
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: label }),
3498
+ onWidth && /* @__PURE__ */ jsx6(
1604
3499
  "select",
1605
3500
  {
1606
3501
  value: width ?? "full",
@@ -1608,10 +3503,10 @@ function Chip({
1608
3503
  disabled: readOnly || widthDisabled,
1609
3504
  title: widthDisabled ? "Overridden by raw CSS columns" : "Column width",
1610
3505
  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))
3506
+ children: ["full", "sm", "md", "lg"].map((w) => /* @__PURE__ */ jsx6("option", { value: w, title: WIDTH_LABELS[w], children: WIDTH_SHORT[w] }, w))
1612
3507
  }
1613
3508
  ),
1614
- onRemove && !readOnly && /* @__PURE__ */ jsx4(IconButton2, { label: "Remove from row", onClick: onRemove, children: /* @__PURE__ */ jsx4(Trash22, {}) })
3509
+ onRemove && !readOnly && /* @__PURE__ */ jsx6(IconButton2, { label: "Remove from row", onClick: onRemove, children: /* @__PURE__ */ jsx6(Trash22, {}) })
1615
3510
  ]
1616
3511
  }
1617
3512
  );
@@ -1634,48 +3529,48 @@ function RowCard({
1634
3529
  onWidth,
1635
3530
  onArrange
1636
3531
  }) {
1637
- const [over, setOver] = useState3(false);
3532
+ const [over, setOver] = useState4(false);
1638
3533
  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: [
3534
+ return /* @__PURE__ */ jsxs5("div", { className: "rounded-lg border p-3", children: [
3535
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 mb-2", children: [
3536
+ /* @__PURE__ */ jsxs5("span", { className: "heading-meta", children: [
1642
3537
  "Row ",
1643
3538
  index + 1
1644
3539
  ] }),
1645
- !readOnly && /* @__PURE__ */ jsxs3("span", { className: "ml-auto flex gap-0.5", children: [
1646
- /* @__PURE__ */ jsx4(
3540
+ !readOnly && /* @__PURE__ */ jsxs5("span", { className: "ml-auto flex gap-0.5", children: [
3541
+ /* @__PURE__ */ jsx6(
1647
3542
  IconButton2,
1648
3543
  {
1649
3544
  label: "Move row up",
1650
3545
  disabled: index === 0,
1651
3546
  onClick: onMoveUp,
1652
- children: /* @__PURE__ */ jsx4(ChevronUp2, {})
3547
+ children: /* @__PURE__ */ jsx6(ChevronUp2, {})
1653
3548
  }
1654
3549
  ),
1655
- /* @__PURE__ */ jsx4(
3550
+ /* @__PURE__ */ jsx6(
1656
3551
  IconButton2,
1657
3552
  {
1658
3553
  label: "Move row down",
1659
3554
  disabled: index === total - 1,
1660
3555
  onClick: onMoveDown,
1661
- children: /* @__PURE__ */ jsx4(ChevronDown2, {})
3556
+ children: /* @__PURE__ */ jsx6(ChevronDown2, {})
1662
3557
  }
1663
3558
  ),
1664
- /* @__PURE__ */ jsx4(
3559
+ /* @__PURE__ */ jsx6(
1665
3560
  IconButton2,
1666
3561
  {
1667
3562
  label: "Delete row",
1668
3563
  tone: "destructive",
1669
3564
  onClick: onDelete,
1670
- children: /* @__PURE__ */ jsx4(Trash22, {})
3565
+ children: /* @__PURE__ */ jsx6(Trash22, {})
1671
3566
  }
1672
3567
  )
1673
3568
  ] })
1674
3569
  ] }),
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,
3570
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3571
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Group" }),
3572
+ /* @__PURE__ */ jsx6(
3573
+ Input3,
1679
3574
  {
1680
3575
  value: row.group ?? "",
1681
3576
  onChange: (e) => onGroup(e.target.value),
@@ -1685,9 +3580,9 @@ function RowCard({
1685
3580
  }
1686
3581
  )
1687
3582
  ] }),
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(
3583
+ /* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2", children: [
3584
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Arrange" }),
3585
+ /* @__PURE__ */ jsxs5(
1691
3586
  "select",
1692
3587
  {
1693
3588
  value: isFlow ? "flow" : "grid",
@@ -1696,13 +3591,13 @@ function RowCard({
1696
3591
  title: "Grid: sized columns. Inline: fields at natural width, wrapping.",
1697
3592
  className: "h-7 rounded-md border bg-background px-2 text-sm text-foreground disabled:opacity-50",
1698
3593
  children: [
1699
- /* @__PURE__ */ jsx4("option", { value: "grid", children: "Grid (columns)" }),
1700
- /* @__PURE__ */ jsx4("option", { value: "flow", children: "Inline (natural width)" })
3594
+ /* @__PURE__ */ jsx6("option", { value: "grid", children: "Grid (columns)" }),
3595
+ /* @__PURE__ */ jsx6("option", { value: "flow", children: "Inline (natural width)" })
1701
3596
  ]
1702
3597
  }
1703
3598
  )
1704
3599
  ] }),
1705
- /* @__PURE__ */ jsx4(
3600
+ /* @__PURE__ */ jsx6(
1706
3601
  "div",
1707
3602
  {
1708
3603
  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 +3612,7 @@ function RowCard({
1717
3612
  setOver(false);
1718
3613
  onDropKey(e.dataTransfer.getData(DRAG_MIME) || null);
1719
3614
  },
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(
3615
+ 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
3616
  Chip,
1722
3617
  {
1723
3618
  label: key,
@@ -1733,12 +3628,12 @@ function RowCard({
1733
3628
  ))
1734
3629
  }
1735
3630
  ),
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,
3631
+ !isFlow && /* @__PURE__ */ jsxs5("details", { className: "mt-2", children: [
3632
+ /* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-xs text-muted-foreground", children: "Advanced: raw CSS columns" }),
3633
+ /* @__PURE__ */ jsxs5("div", { className: "mt-2 flex items-center gap-2", children: [
3634
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-muted-foreground w-16", children: "Columns" }),
3635
+ /* @__PURE__ */ jsx6(
3636
+ Input3,
1742
3637
  {
1743
3638
  value: row.columns ?? "",
1744
3639
  onChange: (e) => onColumns(e.target.value),
@@ -1748,9 +3643,9 @@ function RowCard({
1748
3643
  }
1749
3644
  )
1750
3645
  ] }),
1751
- /* @__PURE__ */ jsxs3("p", { className: "mt-1 pl-[4.5rem] text-xs text-muted-foreground", children: [
3646
+ /* @__PURE__ */ jsxs5("p", { className: "mt-1 pl-[4.5rem] text-xs text-muted-foreground", children: [
1752
3647
  "Raw ",
1753
- /* @__PURE__ */ jsx4("span", { className: "font-mono", children: "grid-template-columns" }),
3648
+ /* @__PURE__ */ jsx6("span", { className: "font-mono", children: "grid-template-columns" }),
1754
3649
  ". When set, it overrides the per-property widths. Blank = use widths."
1755
3650
  ] })
1756
3651
  ] })
@@ -1758,21 +3653,44 @@ function RowCard({
1758
3653
  }
1759
3654
 
1760
3655
  // src/preview/CapabilitiesPanel.tsx
1761
- import { useEffect as useEffect2, useState as useState4 } from "react";
3656
+ import { useMemo as useMemo6 } from "react";
3657
+
3658
+ // src/preview/SchemaSourcePanel.tsx
3659
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState5 } from "react";
3660
+ import { Check, Copy } from "lucide-react";
1762
3661
  import { Button as Button2 } from "@petrarca/sonnet-ui";
1763
3662
  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
3663
+ import { copyTextToClipboard } from "@petrarca/sonnet-core";
3664
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3665
+ function SchemaSourcePanel({
3666
+ value,
3667
+ editable,
3668
+ onApply,
3669
+ validate
1768
3670
  }) {
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]);
3671
+ const [text, setText] = useState5(value);
3672
+ const [error, setError] = useState5(null);
3673
+ const [copied, setCopied] = useState5(false);
3674
+ const [dirty, setDirty] = useState5(false);
3675
+ const lastValueRef = useRef3(value);
3676
+ useEffect3(() => {
3677
+ if (value === lastValueRef.current) return;
3678
+ if (!dirty) {
3679
+ lastValueRef.current = value;
3680
+ setText(value);
3681
+ setError(null);
3682
+ }
3683
+ }, [value, dirty]);
3684
+ const handleEditorChange = (next) => {
3685
+ setText(next);
3686
+ setDirty(next !== value);
3687
+ };
3688
+ const handleCopy = () => {
3689
+ void copyTextToClipboard(editable ? text : value).then(() => {
3690
+ setCopied(true);
3691
+ setTimeout(() => setCopied(false), 1500);
3692
+ });
3693
+ };
1776
3694
  const handleApply = () => {
1777
3695
  let parsed;
1778
3696
  try {
@@ -1781,81 +3699,121 @@ function CapabilitiesPanel({
1781
3699
  setError("Invalid JSON.");
1782
3700
  return;
1783
3701
  }
1784
- const result = validateCapabilities(parsed);
1785
- if (!result.ok) {
1786
- setError(result.errors.join("; "));
3702
+ const validationError = validate?.(parsed);
3703
+ if (validationError) {
3704
+ setError(validationError);
1787
3705
  return;
1788
3706
  }
1789
3707
  setError(null);
1790
- onApply?.(result.value);
3708
+ setDirty(false);
3709
+ onApply(parsed);
1791
3710
  };
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(
3711
+ return /* @__PURE__ */ jsxs6("div", { className: "absolute inset-0 rounded-md border overflow-hidden", children: [
3712
+ /* @__PURE__ */ jsxs6("div", { className: "absolute right-2 top-2 z-10 flex items-center gap-2", children: [
3713
+ error && /* @__PURE__ */ jsx7("span", { className: "rounded bg-destructive px-2 py-1 text-xs font-medium text-destructive-foreground", children: error }),
3714
+ editable && /* @__PURE__ */ jsx7(
1796
3715
  Button2,
1797
3716
  {
1798
3717
  type: "button",
1799
3718
  size: "sm",
1800
3719
  onClick: handleApply,
1801
- className: "ml-auto",
3720
+ disabled: !dirty,
1802
3721
  children: "Apply"
1803
3722
  }
3723
+ ),
3724
+ /* @__PURE__ */ jsxs6(
3725
+ Button2,
3726
+ {
3727
+ type: "button",
3728
+ variant: "outline",
3729
+ size: "sm",
3730
+ onClick: handleCopy,
3731
+ className: "gap-1.5",
3732
+ children: [
3733
+ copied ? /* @__PURE__ */ jsx7(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx7(Copy, { className: "h-3.5 w-3.5" }),
3734
+ copied ? "Copied" : "Copy"
3735
+ ]
3736
+ }
1804
3737
  )
1805
3738
  ] }),
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(
3739
+ editable ? /* @__PURE__ */ jsx7(
1808
3740
  JsonEditor,
1809
3741
  {
1810
3742
  value: text,
1811
3743
  editablePaths: ["/"],
1812
3744
  fillContainer: true,
1813
- onChange: setText
3745
+ onChange: handleEditorChange
3746
+ }
3747
+ ) : /* @__PURE__ */ jsx7(
3748
+ JsonEditor,
3749
+ {
3750
+ value,
3751
+ editablePaths: [],
3752
+ fillContainer: true
3753
+ },
3754
+ value
3755
+ )
3756
+ ] });
3757
+ }
3758
+
3759
+ // src/preview/CapabilitiesPanel.tsx
3760
+ import { jsx as jsx8 } from "react/jsx-runtime";
3761
+ function CapabilitiesPanel({
3762
+ capabilities,
3763
+ onApply
3764
+ }) {
3765
+ const value = useMemo6(
3766
+ () => JSON.stringify(capabilities ?? {}, null, 2),
3767
+ [capabilities]
3768
+ );
3769
+ return /* @__PURE__ */ jsx8(
3770
+ SchemaSourcePanel,
3771
+ {
3772
+ value,
3773
+ editable: true,
3774
+ validate: (parsed) => {
3775
+ const result = validateCapabilities(parsed);
3776
+ return result.ok ? null : result.errors.join("; ");
3777
+ },
3778
+ onApply: (parsed) => {
3779
+ const result = validateCapabilities(parsed);
3780
+ if (result.ok) onApply?.(result.value);
1814
3781
  }
1815
- ) })
1816
- ] });
3782
+ }
3783
+ );
1817
3784
  }
1818
3785
 
1819
3786
  // 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
- ]
1842
- }
1843
- ),
1844
- /* @__PURE__ */ jsx6(JsonEditor2, { value, editablePaths: [], fillContainer: true }, value)
1845
- ] });
3787
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3788
+ function validateRootSchema(parsed) {
3789
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3790
+ return "Schema must be a JSON object.";
3791
+ }
3792
+ const s = parsed;
3793
+ if (s.type !== "object") return 'Root schema must have type: "object".';
3794
+ if (typeof s.properties !== "object" || s.properties === null) {
3795
+ return "Root schema must have a properties object.";
3796
+ }
3797
+ return null;
3798
+ }
3799
+ function validateUiSchema(parsed) {
3800
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3801
+ return "UI Schema must be a JSON object.";
3802
+ }
3803
+ return null;
1846
3804
  }
1847
3805
  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" })
3806
+ return /* @__PURE__ */ jsxs7(TabsList, { className: "mb-3", children: [
3807
+ flags.showForm && /* @__PURE__ */ jsx9(TabsTrigger, { value: "form", children: "Form" }),
3808
+ flags.isFull && flags.showForm && /* @__PURE__ */ jsx9(TabsTrigger, { value: "layout", children: "Form Layout" }),
3809
+ flags.showTable && /* @__PURE__ */ jsx9(TabsTrigger, { value: "table", children: "Table" }),
3810
+ /* @__PURE__ */ jsx9(TabsSeparator, {}),
3811
+ flags.showJson && /* @__PURE__ */ jsx9(TabsTrigger, { value: "schema", children: "JSON Schema" }),
3812
+ flags.isFull && /* @__PURE__ */ jsx9(TabsTrigger, { value: "ui", children: "UI Schema" }),
3813
+ flags.isFull && /* @__PURE__ */ jsx9(TabsTrigger, { value: "combined", children: "Combined" }),
3814
+ flags.showCapabilities && /* @__PURE__ */ jsxs7(Fragment4, { children: [
3815
+ /* @__PURE__ */ jsx9(TabsSeparator, {}),
3816
+ /* @__PURE__ */ jsx9(TabsTrigger, { value: "capabilities", children: "Capabilities" })
1859
3817
  ] })
1860
3818
  ] });
1861
3819
  }
@@ -1873,22 +3831,22 @@ function PreviewPane({
1873
3831
  showJson: caps.previews.includes("json"),
1874
3832
  showCapabilities: caps.showCapabilities
1875
3833
  };
1876
- const [data, setData] = useState5(
3834
+ const [data, setData] = useState6(
1877
3835
  () => extractSchemaDefaults(schema)
1878
3836
  );
1879
- const schemaText = useMemo4(() => JSON.stringify(schema, null, 2), [schema]);
1880
- const uiSchemaText = useMemo4(
3837
+ const schemaText = useMemo7(() => JSON.stringify(schema, null, 2), [schema]);
3838
+ const uiSchemaText = useMemo7(
1881
3839
  () => JSON.stringify(uiSchema ?? {}, null, 2),
1882
3840
  [uiSchema]
1883
3841
  );
1884
- const embeddedText = useMemo4(
3842
+ const embeddedText = useMemo7(
1885
3843
  () => JSON.stringify(embedUiSchema(schema, uiSchema ?? {}), null, 2),
1886
3844
  [schema, uiSchema]
1887
3845
  );
1888
3846
  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(
3847
+ return /* @__PURE__ */ jsxs7(Tabs, { defaultValue: defaultTab, className: "flex flex-col h-full min-h-0", children: [
3848
+ /* @__PURE__ */ jsx9(PreviewTabList, { flags }),
3849
+ flags.showForm && /* @__PURE__ */ jsx9(TabsContent, { value: "form", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx9(
1892
3850
  JsonSchemaFormRenderer3,
1893
3851
  {
1894
3852
  schema,
@@ -1899,11 +3857,12 @@ function PreviewPane({
1899
3857
  showExtraProperties: false
1900
3858
  }
1901
3859
  ) }),
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(
3860
+ flags.isFull && flags.showForm && /* @__PURE__ */ jsx9(TabsContent, { value: "layout", className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx9(FormLayoutDesigner, { state }) }),
3861
+ flags.showTable && /* @__PURE__ */ jsx9(TabsContent, { value: "table", className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsx9(TablePreview, { schema }) }),
3862
+ /* @__PURE__ */ jsx9(
1905
3863
  SourceTabPanels,
1906
3864
  {
3865
+ state,
1907
3866
  flags,
1908
3867
  schemaText,
1909
3868
  uiSchemaText,
@@ -1915,6 +3874,7 @@ function PreviewPane({
1915
3874
  ] });
1916
3875
  }
1917
3876
  function SourceTabPanels({
3877
+ state,
1918
3878
  flags,
1919
3879
  schemaText,
1920
3880
  uiSchemaText,
@@ -1922,11 +3882,36 @@ function SourceTabPanels({
1922
3882
  capabilities,
1923
3883
  onCapabilitiesChange
1924
3884
  }) {
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(
3885
+ const canEditSource = !state.capabilities.locked && !state.capabilities.scoped;
3886
+ return /* @__PURE__ */ jsxs7(Fragment4, { children: [
3887
+ flags.showJson && /* @__PURE__ */ jsx9(TabsContent, { value: "schema", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3888
+ SchemaSourcePanel,
3889
+ {
3890
+ value: schemaText,
3891
+ editable: canEditSource,
3892
+ validate: validateRootSchema,
3893
+ onApply: (parsed) => state.replaceSchema(parsed)
3894
+ }
3895
+ ) }),
3896
+ flags.isFull && /* @__PURE__ */ jsx9(TabsContent, { value: "ui", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3897
+ SchemaSourcePanel,
3898
+ {
3899
+ value: uiSchemaText,
3900
+ editable: canEditSource,
3901
+ validate: validateUiSchema,
3902
+ onApply: (parsed) => state.replaceUiSchema(parsed)
3903
+ }
3904
+ ) }),
3905
+ flags.isFull && /* @__PURE__ */ jsx9(TabsContent, { value: "combined", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
3906
+ SchemaSourcePanel,
3907
+ {
3908
+ value: embeddedText,
3909
+ editable: false,
3910
+ onApply: () => {
3911
+ }
3912
+ }
3913
+ ) }),
3914
+ flags.showCapabilities && /* @__PURE__ */ jsx9(TabsContent, { value: "capabilities", className: "flex-1 min-h-0 relative", children: /* @__PURE__ */ jsx9(
1930
3915
  CapabilitiesPanel,
1931
3916
  {
1932
3917
  capabilities,
@@ -1937,20 +3922,27 @@ function SourceTabPanels({
1937
3922
  }
1938
3923
 
1939
3924
  // src/inspector/Inspector.tsx
1940
- import { useEffect as useEffect4, useMemo as useMemo8, useRef as useRef4, useState as useState7 } from "react";
3925
+ import { useEffect as useEffect5, useMemo as useMemo14, useRef as useRef6, useState as useState8 } from "react";
1941
3926
  import {
1942
3927
  Tabs as Tabs2,
1943
3928
  TabsList as TabsList2,
1944
3929
  TabsTrigger as TabsTrigger2,
1945
3930
  TabsContent as TabsContent2,
1946
- Input as Input4,
3931
+ Input as Input5,
1947
3932
  IconButton as IconButton4
1948
3933
  } from "@petrarca/sonnet-ui";
1949
3934
  import { Pencil, Check as Check2, X } from "lucide-react";
1950
3935
 
1951
3936
  // src/inspector/DataTab.tsx
1952
- import { useMemo as useMemo5, useRef as useRef3 } from "react";
3937
+ import { useMemo as useMemo8, useRef as useRef4 } from "react";
1953
3938
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer4 } from "@petrarca/sonnet-forms";
3939
+ import {
3940
+ Select,
3941
+ SelectTrigger,
3942
+ SelectValue,
3943
+ SelectContent,
3944
+ SelectItem
3945
+ } from "@petrarca/sonnet-ui";
1954
3946
 
1955
3947
  // src/inspector/metaSchemas.ts
1956
3948
  var PROPERTY_TYPES = [
@@ -1959,8 +3951,20 @@ var PROPERTY_TYPES = [
1959
3951
  "integer",
1960
3952
  "boolean",
1961
3953
  "object",
1962
- "array"
3954
+ "array",
3955
+ "null"
1963
3956
  ];
3957
+ var PROPERTY_TYPE_CHOICES = [...PROPERTY_TYPES, "oneOf"];
3958
+ var PROPERTY_TYPE_NAMES = {
3959
+ string: "String",
3960
+ number: "Number",
3961
+ integer: "Integer",
3962
+ boolean: "Boolean",
3963
+ object: "Object",
3964
+ array: "Array",
3965
+ null: "Display (text only)",
3966
+ oneOf: "One of (variant)"
3967
+ };
1964
3968
  var STRING_FORMATS = [
1965
3969
  "",
1966
3970
  "email",
@@ -1995,37 +3999,100 @@ function validationProps(type) {
1995
3999
  return {};
1996
4000
  }
1997
4001
  }
1998
- function buildDataMetaSchema(type) {
4002
+ var BRANCH_META_TYPE = "branch";
4003
+ function typeTraits(type) {
4004
+ const isDisplay = type === "null";
4005
+ const isOneOf3 = type === "oneOf";
1999
4006
  const isContainer2 = type === "object" || type === "array";
4007
+ return {
4008
+ isDisplay,
4009
+ isOneOf: isOneOf3,
4010
+ // Containers, display (null), and oneOf have no meaningful default value.
4011
+ noDefault: isContainer2 || isDisplay || isOneOf3,
4012
+ // Display notes and oneOf unions are never "required" on their own.
4013
+ noRequired: isDisplay || isOneOf3,
4014
+ // A fixed value (const) is only meaningful on scalar properties.
4015
+ isScalar: type === "string" || type === "number" || type === "integer" || type === "boolean"
4016
+ };
4017
+ }
4018
+ function buildReferenceMetaSchema() {
4019
+ return {
4020
+ title: "",
4021
+ type: "object",
4022
+ properties: {
4023
+ title: { type: "string", title: "Label (title)" },
4024
+ description: {
4025
+ type: "string",
4026
+ title: "Description",
4027
+ "x-ui-widget": "textarea"
4028
+ },
4029
+ required: { type: "boolean", title: "Required" },
4030
+ default: { type: "string", title: "Default" }
4031
+ }
4032
+ };
4033
+ }
4034
+ function buildDataMetaSchema(type) {
4035
+ if (type === BRANCH_META_TYPE) {
4036
+ return {
4037
+ title: "",
4038
+ type: "object",
4039
+ properties: {
4040
+ title: { type: "string", title: "Branch label (title)" }
4041
+ }
4042
+ };
4043
+ }
4044
+ const { isDisplay, noDefault, noRequired, isScalar, isOneOf: isOneOf3 } = typeTraits(type);
2000
4045
  const properties = {
2001
- type: { type: "string", title: "Type", enum: [...PROPERTY_TYPES] },
2002
- title: { type: "string", title: "Label (title)" },
4046
+ type: {
4047
+ type: "string",
4048
+ title: "Type",
4049
+ enum: [...PROPERTY_TYPE_CHOICES],
4050
+ "x-ui-options": { enumNames: PROPERTY_TYPE_NAMES }
4051
+ },
4052
+ title: { type: "string", title: isDisplay ? "Text" : "Label (title)" },
2003
4053
  description: {
2004
4054
  type: "string",
2005
4055
  title: "Description",
2006
4056
  "x-ui-widget": "textarea"
2007
4057
  },
2008
- required: { type: "boolean", title: "Required" },
2009
- ...isContainer2 ? {} : { default: { type: "string", title: "Default" } },
4058
+ ...isOneOf3 ? {
4059
+ discriminator: {
4060
+ type: "string",
4061
+ title: "Discriminator property",
4062
+ "x-ui-description": "Shared property whose const value tags each branch. Auto-detected when blank."
4063
+ }
4064
+ } : {},
4065
+ ...noRequired ? {} : { required: { type: "boolean", title: "Required" } },
4066
+ ...noDefault ? {} : { default: { type: "string", title: "Default" } },
4067
+ ...isScalar ? {
4068
+ const: {
4069
+ type: "string",
4070
+ title: "Const (fixed value)",
4071
+ "x-ui-description": "Fixed value this property must equal. Leave blank for none."
4072
+ }
4073
+ } : {},
2010
4074
  ...validationProps(type)
2011
4075
  };
2012
4076
  return { title: "", type: "object", properties };
2013
4077
  }
2014
- function propertyToDataValues(prop, required) {
4078
+ function propertyToDataValues(prop2, required) {
2015
4079
  return {
2016
- type: prop.type ?? "string",
2017
- title: prop.title,
2018
- description: prop.description,
2019
- default: prop.default,
4080
+ type: prop2.oneOf ? "oneOf" : prop2.type ?? "string",
4081
+ title: prop2.title,
4082
+ description: prop2.description,
4083
+ default: prop2.default,
4084
+ // const may be any JSON type; the Data-tab field edits it as a string.
4085
+ const: prop2.const == null ? void 0 : String(prop2.const),
2020
4086
  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
4087
+ minLength: prop2.minLength,
4088
+ maxLength: prop2.maxLength,
4089
+ pattern: prop2.pattern,
4090
+ format: prop2.format,
4091
+ minimum: prop2.minimum,
4092
+ maximum: prop2.maximum,
4093
+ minItems: prop2.minItems,
4094
+ maxItems: prop2.maxItems,
4095
+ discriminator: prop2.discriminator?.propertyName
2029
4096
  };
2030
4097
  }
2031
4098
  function dataValuesToPatch(values) {
@@ -2043,6 +4110,7 @@ function dataValuesToPatch(values) {
2043
4110
  setStr("title", values.title);
2044
4111
  setStr("description", values.description);
2045
4112
  setStr("default", values.default);
4113
+ setStr("const", values.const);
2046
4114
  setStr("format", values.format);
2047
4115
  setStr("pattern", values.pattern);
2048
4116
  setNum("minLength", values.minLength);
@@ -2051,28 +4119,31 @@ function dataValuesToPatch(values) {
2051
4119
  setNum("maximum", values.maximum);
2052
4120
  setNum("minItems", values.minItems);
2053
4121
  setNum("maxItems", values.maxItems);
4122
+ if (typeof values.discriminator === "string" && values.discriminator.length) {
4123
+ patch.discriminator = { propertyName: values.discriminator };
4124
+ } else {
4125
+ patch.discriminator = void 0;
4126
+ }
2054
4127
  return patch;
2055
4128
  }
2056
4129
 
2057
4130
  // src/inspector/DataTab.tsx
2058
- import { jsx as jsx7 } from "react/jsx-runtime";
4131
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2059
4132
  function DataTab({ node, state }) {
2060
- const type = node.property.type ?? "string";
2061
- const schema = useMemo5(() => buildDataMetaSchema(type), [type]);
2062
- const values = useMemo5(
4133
+ const isBranch = node.slotKind === "branch";
4134
+ const type = isBranch ? BRANCH_META_TYPE : node.property.oneOf ? "oneOf" : node.property.type ?? "string";
4135
+ const schema = useMemo8(() => buildDataMetaSchema(type), [type]);
4136
+ const values = useMemo8(
2063
4137
  () => propertyToDataValues(node.property, node.required),
2064
4138
  [node.property, node.required]
2065
4139
  );
2066
- const prevRef = useRef3(values);
4140
+ const prevRef = useRef4(values);
2067
4141
  prevRef.current = values;
2068
4142
  const handleChange = (data) => {
2069
4143
  const next = data;
2070
4144
  const prev = prevRef.current;
2071
- if (next.type !== prev.type) {
2072
- state.changePropertyType(
2073
- node.path,
2074
- next.type
2075
- );
4145
+ if (!isBranch && next.type !== prev.type) {
4146
+ state.changePropertyType(node.path, next.type);
2076
4147
  return;
2077
4148
  }
2078
4149
  if (next.required !== prev.required) {
@@ -2081,30 +4152,66 @@ function DataTab({ node, state }) {
2081
4152
  }
2082
4153
  state.updateProperty(node.path, dataValuesToPatch(next));
2083
4154
  };
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
- );
4155
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-3", children: [
4156
+ !isBranch && /* @__PURE__ */ jsx10(ReferencePicker, { node, state, disabled: state.readOnly }),
4157
+ /* @__PURE__ */ jsx10(
4158
+ JsonSchemaFormRenderer4,
4159
+ {
4160
+ schema,
4161
+ data: values,
4162
+ onChange: handleChange,
4163
+ deferStateUpdates: true,
4164
+ showActions: false,
4165
+ showExtraProperties: false,
4166
+ readOnly: state.readOnly
4167
+ },
4168
+ pathKey(node.path) + ":" + type
4169
+ )
4170
+ ] });
4171
+ }
4172
+ function availableDefinitions(state, node) {
4173
+ const defs = state.schema.$defs ?? state.schema.definitions;
4174
+ if (!defs) return [];
4175
+ const selfName = node.path.length === 1 && node.path[0].kind === "def" ? node.path[0].name : void 0;
4176
+ return Object.keys(defs).filter((name) => name !== selfName);
4177
+ }
4178
+ function ReferencePicker({
4179
+ node,
4180
+ state,
4181
+ disabled
4182
+ }) {
4183
+ const names = availableDefinitions(state, node);
4184
+ if (names.length === 0) return null;
4185
+ const isArray = node.property.type === "array";
4186
+ const label = isArray ? "Reference a definition (array items)" : "Reference a definition";
4187
+ const placeholder = isArray ? "Make items reuse a shared definition..." : "Reuse a shared definition...";
4188
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
4189
+ /* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
4190
+ /* @__PURE__ */ jsxs8(
4191
+ Select,
4192
+ {
4193
+ value: "",
4194
+ onValueChange: (name) => state.setReference(node.path, name),
4195
+ disabled,
4196
+ children: [
4197
+ /* @__PURE__ */ jsx10(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx10(SelectValue, { placeholder }) }),
4198
+ /* @__PURE__ */ jsx10(SelectContent, { children: names.map((name) => /* @__PURE__ */ jsx10(SelectItem, { value: name, children: name }, name)) })
4199
+ ]
4200
+ }
4201
+ )
4202
+ ] });
2096
4203
  }
2097
4204
 
2098
4205
  // src/inspector/UITab.tsx
2099
- import { useMemo as useMemo6 } from "react";
4206
+ import { useMemo as useMemo9 } from "react";
2100
4207
  import { JsonSchemaFormRenderer as JsonSchemaFormRenderer5 } from "@petrarca/sonnet-forms";
2101
- import { JsonEditor as JsonEditor3 } from "@petrarca/sonnet-ui/json-editor";
4208
+ import { JsonEditor as JsonEditor2 } from "@petrarca/sonnet-ui/json-editor";
2102
4209
  import {
2103
4210
  WIDGET_OPTION_DEFS,
2104
4211
  optionDefsToSchema
2105
4212
  } 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";
4213
+ import { FormSelect as FormSelect4 } from "@petrarca/sonnet-forms";
4214
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2108
4215
  var PRESENTATION_META_SCHEMA = {
2109
4216
  title: "",
2110
4217
  type: "object",
@@ -2167,14 +4274,15 @@ function PresentationEditor({
2167
4274
  }
2168
4275
  onChange(next);
2169
4276
  };
2170
- return /* @__PURE__ */ jsxs6("div", { children: [
2171
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Presentation (form overrides)" }),
2172
- /* @__PURE__ */ jsx8(
4277
+ return /* @__PURE__ */ jsxs9("div", { children: [
4278
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Presentation (form overrides)" }),
4279
+ /* @__PURE__ */ jsx11(
2173
4280
  JsonSchemaFormRenderer5,
2174
4281
  {
2175
4282
  schema: PRESENTATION_META_SCHEMA,
2176
4283
  data: values,
2177
4284
  onChange: handleChange,
4285
+ deferStateUpdates: true,
2178
4286
  showActions: false,
2179
4287
  showExtraProperties: false,
2180
4288
  readOnly
@@ -2191,14 +4299,15 @@ function OptionsEditor({
2191
4299
  onChange
2192
4300
  }) {
2193
4301
  if (optionsSchema) {
2194
- return /* @__PURE__ */ jsxs6("div", { children: [
2195
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options" }),
2196
- /* @__PURE__ */ jsx8(
4302
+ return /* @__PURE__ */ jsxs9("div", { children: [
4303
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Options" }),
4304
+ /* @__PURE__ */ jsx11(
2197
4305
  JsonSchemaFormRenderer5,
2198
4306
  {
2199
4307
  schema: optionsSchema,
2200
4308
  data: currentOptions,
2201
4309
  onChange,
4310
+ deferStateUpdates: true,
2202
4311
  showActions: false,
2203
4312
  showExtraProperties: false,
2204
4313
  readOnly
@@ -2207,10 +4316,10 @@ function OptionsEditor({
2207
4316
  )
2208
4317
  ] });
2209
4318
  }
2210
- return /* @__PURE__ */ jsxs6("div", { children: [
2211
- /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options (raw JSON)" }),
2212
- /* @__PURE__ */ jsx8(
2213
- JsonEditor3,
4319
+ return /* @__PURE__ */ jsxs9("div", { children: [
4320
+ /* @__PURE__ */ jsx11("p", { className: "heading-meta mb-2", children: "Options (raw JSON)" }),
4321
+ /* @__PURE__ */ jsx11(
4322
+ JsonEditor2,
2214
4323
  {
2215
4324
  value: JSON.stringify(currentOptions ?? {}, null, 2),
2216
4325
  editablePaths: readOnly ? [] : ["/"],
@@ -2231,17 +4340,17 @@ function UITab({
2231
4340
  state,
2232
4341
  widgetOptions
2233
4342
  }) {
2234
- const pathKey2 = node.path.join(".");
4343
+ const idBase = pathKey(node.path);
2235
4344
  const uiEntry = getUiEntryAtPath(state.uiSchema, node.path) ?? {};
2236
4345
  const currentWidget = typeof uiEntry["x-ui-widget"] === "string" ? uiEntry["x-ui-widget"] : null;
2237
4346
  const currentOptions = uiEntry["x-ui-options"] ?? {};
2238
4347
  const rootLayout = state.uiSchema["x-ui-layout"];
2239
4348
  const inheritedLabelPlacement = rootLayout?.labelPlacement ?? "top";
2240
- const widgetChoices = useMemo6(
4349
+ const widgetChoices = useMemo9(
2241
4350
  () => compatibleWidgets(node.property),
2242
4351
  [node.property]
2243
4352
  );
2244
- const optionsSchema = useMemo6(() => {
4353
+ const optionsSchema = useMemo9(() => {
2245
4354
  if (!currentWidget) return null;
2246
4355
  const merged = { ...WIDGET_OPTION_DEFS, ...widgetOptions ?? {} };
2247
4356
  const descriptors = merged[currentWidget];
@@ -2274,11 +4383,11 @@ function UITab({
2274
4383
  else delete next["x-ui-options"];
2275
4384
  state.setUiEntry(node.path, next);
2276
4385
  };
2277
- return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
2278
- /* @__PURE__ */ jsx8(
2279
- FormSelect3,
4386
+ return /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
4387
+ /* @__PURE__ */ jsx11(
4388
+ FormSelect4,
2280
4389
  {
2281
- id: `${pathKey2}-widget`,
4390
+ id: `${idBase}-widget`,
2282
4391
  name: "widget",
2283
4392
  label: "Widget",
2284
4393
  value: currentWidget ?? "__auto__",
@@ -2291,29 +4400,29 @@ function UITab({
2291
4400
  clearable: false
2292
4401
  }
2293
4402
  ),
2294
- currentWidget && /* @__PURE__ */ jsx8(
4403
+ currentWidget && /* @__PURE__ */ jsx11(
2295
4404
  OptionsEditor,
2296
4405
  {
2297
- widget: `${pathKey2}:${currentWidget}`,
4406
+ widget: `${idBase}:${currentWidget}`,
2298
4407
  optionsSchema,
2299
4408
  currentOptions,
2300
4409
  readOnly: state.readOnly,
2301
4410
  onChange: setOptions
2302
4411
  }
2303
4412
  ),
2304
- /* @__PURE__ */ jsx8(
4413
+ /* @__PURE__ */ jsx11(
2305
4414
  PresentationEditor,
2306
4415
  {
2307
- fieldName: pathKey2,
4416
+ fieldName: idBase,
2308
4417
  uiEntry,
2309
4418
  readOnly: state.readOnly,
2310
4419
  onChange: (entry) => state.setUiEntry(node.path, entry)
2311
4420
  }
2312
4421
  ),
2313
- /* @__PURE__ */ jsx8(
2314
- FormSelect3,
4422
+ /* @__PURE__ */ jsx11(
4423
+ FormSelect4,
2315
4424
  {
2316
- id: `${pathKey2}-labelPlacement`,
4425
+ id: `${idBase}-labelPlacement`,
2317
4426
  name: "labelPlacement",
2318
4427
  label: "Label placement",
2319
4428
  value: currentOptions.labelPlacement ?? "__inherit__",
@@ -2334,25 +4443,25 @@ function UITab({
2334
4443
  }
2335
4444
 
2336
4445
  // 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";
4446
+ import { useEffect as useEffect4, useState as useState7 } from "react";
4447
+ import { Input as Input4, IconButton as IconButton3, Button as Button3 } from "@petrarca/sonnet-ui";
2339
4448
  import { Plus as Plus3, Trash2 as Trash23 } from "lucide-react";
2340
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
4449
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2341
4450
  function EnumEditor({
2342
4451
  node,
2343
4452
  state
2344
4453
  }) {
2345
- const [rows, setRows] = useState6(
4454
+ const [rows, setRows] = useState7(
2346
4455
  () => readEnumChoices(node.property, getUiEntryAtPath(state.uiSchema, node.path))
2347
4456
  );
2348
- useEffect3(() => {
4457
+ useEffect4(() => {
2349
4458
  setRows(
2350
4459
  readEnumChoices(
2351
4460
  node.property,
2352
4461
  getUiEntryAtPath(state.uiSchema, node.path)
2353
4462
  )
2354
4463
  );
2355
- }, [node.path.join(".")]);
4464
+ }, [pathKey(node.path)]);
2356
4465
  const canEditLabels = true;
2357
4466
  const commit = (next) => {
2358
4467
  setRows(next);
@@ -2363,20 +4472,20 @@ function EnumEditor({
2363
4472
  };
2364
4473
  const removeRow = (i) => commit(rows.filter((_, idx) => idx !== i));
2365
4474
  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." })
4475
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-3", children: [
4476
+ /* @__PURE__ */ jsxs10("div", { children: [
4477
+ /* @__PURE__ */ jsx12("p", { className: "text-sm font-medium", children: "Allowed values" }),
4478
+ /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", children: "Define a fixed set of choices. Leave empty for free input." })
2370
4479
  ] }),
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" })
4480
+ rows.length === 0 ? /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", children: "No choices yet." }) : /* @__PURE__ */ jsxs10("div", { className: "space-y-2", children: [
4481
+ canEditLabels && /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 px-1", children: [
4482
+ /* @__PURE__ */ jsx12("span", { className: "heading-meta flex-1", children: "Value" }),
4483
+ /* @__PURE__ */ jsx12("span", { className: "heading-meta flex-1", children: "Label (optional)" }),
4484
+ /* @__PURE__ */ jsx12("span", { className: "w-7" })
2376
4485
  ] }),
2377
- rows.map((row, i) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
2378
- /* @__PURE__ */ jsx9(
2379
- Input3,
4486
+ rows.map((row, i) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
4487
+ /* @__PURE__ */ jsx12(
4488
+ Input4,
2380
4489
  {
2381
4490
  value: row.value,
2382
4491
  onChange: (e) => updateRow(i, { value: e.target.value }),
@@ -2385,8 +4494,8 @@ function EnumEditor({
2385
4494
  className: "h-8 text-sm flex-1"
2386
4495
  }
2387
4496
  ),
2388
- canEditLabels && /* @__PURE__ */ jsx9(
2389
- Input3,
4497
+ canEditLabels && /* @__PURE__ */ jsx12(
4498
+ Input4,
2390
4499
  {
2391
4500
  value: row.label ?? "",
2392
4501
  onChange: (e) => updateRow(i, { label: e.target.value }),
@@ -2395,26 +4504,26 @@ function EnumEditor({
2395
4504
  className: "h-8 text-sm flex-1"
2396
4505
  }
2397
4506
  ),
2398
- !state.readOnly && /* @__PURE__ */ jsx9(
4507
+ !state.readOnly && /* @__PURE__ */ jsx12(
2399
4508
  IconButton3,
2400
4509
  {
2401
4510
  label: "Remove value",
2402
4511
  tone: "destructive",
2403
4512
  onClick: () => removeRow(i),
2404
- children: /* @__PURE__ */ jsx9(Trash23, {})
4513
+ children: /* @__PURE__ */ jsx12(Trash23, {})
2405
4514
  }
2406
4515
  )
2407
4516
  ] }, i))
2408
4517
  ] }),
2409
- !state.readOnly && /* @__PURE__ */ jsxs7(
2410
- Button4,
4518
+ !state.readOnly && /* @__PURE__ */ jsxs10(
4519
+ Button3,
2411
4520
  {
2412
4521
  variant: "outline",
2413
4522
  size: "sm",
2414
4523
  className: "gap-1.5",
2415
4524
  onClick: addRow,
2416
4525
  children: [
2417
- /* @__PURE__ */ jsx9(Plus3, { className: "h-4 w-4" }),
4526
+ /* @__PURE__ */ jsx12(Plus3, { className: "h-4 w-4" }),
2418
4527
  "Add value"
2419
4528
  ]
2420
4529
  }
@@ -2423,11 +4532,11 @@ function EnumEditor({
2423
4532
  }
2424
4533
 
2425
4534
  // src/inspector/TableHintsTab.tsx
2426
- import { useMemo as useMemo7 } from "react";
4535
+ import { useMemo as useMemo10 } from "react";
2427
4536
  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";
4537
+ import { JsonEditor as JsonEditor3 } from "@petrarca/sonnet-ui/json-editor";
4538
+ import { FormSelect as FormSelect5, JsonSchemaFormRenderer as JsonSchemaFormRenderer6 } from "@petrarca/sonnet-forms";
4539
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2431
4540
  var CELL_NAMES = Object.keys(CELL_RENDERERS);
2432
4541
  var COLUMN_META_SCHEMA = {
2433
4542
  title: "",
@@ -2444,7 +4553,7 @@ function TableHintsTab({
2444
4553
  state
2445
4554
  }) {
2446
4555
  const hints = readTableHints(node.property);
2447
- const cellOptions = useMemo7(
4556
+ const cellOptions = useMemo10(
2448
4557
  () => JSON.stringify(hints.cellOptions ?? {}, null, 2),
2449
4558
  [hints.cellOptions]
2450
4559
  );
@@ -2467,9 +4576,9 @@ function TableHintsTab({
2467
4576
  column.minWidth = v.minWidth;
2468
4577
  commit({ column });
2469
4578
  };
2470
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-4", children: [
2471
- /* @__PURE__ */ jsx10(
2472
- FormSelect4,
4579
+ return /* @__PURE__ */ jsxs11("div", { className: "space-y-4", children: [
4580
+ /* @__PURE__ */ jsx13(
4581
+ FormSelect5,
2473
4582
  {
2474
4583
  id: `${node.key}-cell`,
2475
4584
  name: "cell",
@@ -2484,25 +4593,26 @@ function TableHintsTab({
2484
4593
  clearable: false
2485
4594
  }
2486
4595
  ),
2487
- /* @__PURE__ */ jsxs8("div", { children: [
2488
- /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Column" }),
2489
- /* @__PURE__ */ jsx10(
4596
+ /* @__PURE__ */ jsxs11("div", { children: [
4597
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: "Column" }),
4598
+ /* @__PURE__ */ jsx13(
2490
4599
  JsonSchemaFormRenderer6,
2491
4600
  {
2492
4601
  schema: COLUMN_META_SCHEMA,
2493
4602
  data: columnValues,
2494
4603
  onChange: onColumnChange,
4604
+ deferStateUpdates: true,
2495
4605
  showActions: false,
2496
4606
  showExtraProperties: false,
2497
4607
  readOnly: state.readOnly
2498
4608
  },
2499
- node.path.join(".")
4609
+ pathKey(node.path)
2500
4610
  )
2501
4611
  ] }),
2502
- /* @__PURE__ */ jsxs8("div", { children: [
2503
- /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Cell options (JSON)" }),
2504
- /* @__PURE__ */ jsx10(
2505
- JsonEditor4,
4612
+ /* @__PURE__ */ jsxs11("div", { children: [
4613
+ /* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: "Cell options (JSON)" }),
4614
+ /* @__PURE__ */ jsx13(
4615
+ JsonEditor3,
2506
4616
  {
2507
4617
  value: cellOptions,
2508
4618
  editablePaths: state.readOnly ? [] : ["/"],
@@ -2522,22 +4632,264 @@ function TableHintsTab({
2522
4632
  ] });
2523
4633
  }
2524
4634
 
4635
+ // src/inspector/ConditionEditor.tsx
4636
+ import { useMemo as useMemo11 } from "react";
4637
+ import { FormSelect as FormSelect6 } from "@petrarca/sonnet-forms";
4638
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
4639
+ function siblingFields(state, node) {
4640
+ const objectPath = node.path.slice(0, -1);
4641
+ const owner = objectPath.length === 0 ? { properties: state.schema.properties } : getProperty(state.schema, objectPath);
4642
+ return owner?.properties ?? {};
4643
+ }
4644
+ function ConditionEditor({
4645
+ node,
4646
+ state
4647
+ }) {
4648
+ const condition2 = useMemo11(
4649
+ () => ifToCondition(node.property.if),
4650
+ [node.property.if]
4651
+ );
4652
+ const fieldProps = useMemo11(() => siblingFields(state, node), [state, node]);
4653
+ const fields = useMemo11(() => Object.keys(fieldProps), [fieldProps]);
4654
+ const disabled = state.readOnly;
4655
+ const lastSeg = node.path[node.path.length - 1];
4656
+ const combinator = lastSeg?.kind === "condition" ? lastSeg.combinator : "allOf";
4657
+ const objectPath = node.path.slice(0, -1);
4658
+ const commit = (next) => {
4659
+ const ifSchema = conditionToIf(next, fieldProps[next.field]?.type);
4660
+ if (ifSchema) state.setCondition(node.path, ifSchema);
4661
+ };
4662
+ const fieldOptions = fields.map((f) => ({ value: f, label: f }));
4663
+ const needsValue = condition2.operator !== "exists";
4664
+ return /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
4665
+ /* @__PURE__ */ jsxs12("p", { className: "text-xs text-muted-foreground", children: [
4666
+ "Show the ",
4667
+ /* @__PURE__ */ jsx14("span", { className: "font-medium", children: "then" }),
4668
+ " fields when this condition holds (otherwise the ",
4669
+ /* @__PURE__ */ jsx14("span", { className: "font-medium", children: "else" }),
4670
+ " ",
4671
+ "fields, if any)."
4672
+ ] }),
4673
+ /* @__PURE__ */ jsx14(
4674
+ FormSelect6,
4675
+ {
4676
+ id: "condition-match",
4677
+ name: "match",
4678
+ label: "Match (all conditions on this object)",
4679
+ options: [
4680
+ { value: "allOf", label: "All (AND)" },
4681
+ { value: "anyOf", label: "Any (OR)" }
4682
+ ],
4683
+ value: combinator,
4684
+ onChange: (v) => state.setConditionalBehavior(
4685
+ objectPath,
4686
+ v === "anyOf" ? "anyOf" : "allOf"
4687
+ ),
4688
+ clearable: false,
4689
+ disabled
4690
+ }
4691
+ ),
4692
+ /* @__PURE__ */ jsx14(
4693
+ FormSelect6,
4694
+ {
4695
+ id: "condition-field",
4696
+ name: "field",
4697
+ label: "When property",
4698
+ options: fieldOptions,
4699
+ value: condition2.field || null,
4700
+ onChange: (v) => commit({ ...condition2, field: v ?? "" }),
4701
+ placeholder: "Select a property...",
4702
+ clearable: false,
4703
+ disabled
4704
+ }
4705
+ ),
4706
+ /* @__PURE__ */ jsx14(
4707
+ FormSelect6,
4708
+ {
4709
+ id: "condition-operator",
4710
+ name: "operator",
4711
+ label: "Operator",
4712
+ options: OPERATOR_OPTIONS,
4713
+ value: condition2.operator,
4714
+ onChange: (v) => commit({ ...condition2, operator: v ?? "=" }),
4715
+ clearable: false,
4716
+ disabled
4717
+ }
4718
+ ),
4719
+ needsValue && /* @__PURE__ */ jsx14(
4720
+ OperatorValueInput,
4721
+ {
4722
+ id: "condition-value",
4723
+ cacheKey: `${node.path.length}:${condition2.field}:${condition2.operator}`,
4724
+ value: condition2.value,
4725
+ onCommit: (v) => commit({ ...condition2, value: v }),
4726
+ disabled: disabled || !condition2.field
4727
+ }
4728
+ )
4729
+ ] });
4730
+ }
4731
+
4732
+ // src/inspector/RuleEditor.tsx
4733
+ import { useMemo as useMemo12 } from "react";
4734
+
4735
+ // src/inspector/ruleScopes.ts
4736
+ function dataPointerPrefix(node) {
4737
+ let prefix = "";
4738
+ for (let i = 0; i < node.path.length - 1; i++) {
4739
+ const seg = node.path[i];
4740
+ if (seg.kind !== "prop") return void 0;
4741
+ prefix += `/${seg.key}`;
4742
+ }
4743
+ return prefix;
4744
+ }
4745
+ function scopePointers(state, node) {
4746
+ const options = /* @__PURE__ */ new Map();
4747
+ const prefix = dataPointerPrefix(node);
4748
+ const selfPointer = prefix === void 0 ? void 0 : `${prefix}/${node.key}`;
4749
+ const add = (pointer, prop2) => {
4750
+ if (pointer === selfPointer) return;
4751
+ if (!options.has(pointer))
4752
+ options.set(pointer, { pointer, type: prop2.type });
4753
+ };
4754
+ for (const [k, prop2] of Object.entries(state.schema.properties ?? {})) {
4755
+ add(`/${k}`, prop2);
4756
+ }
4757
+ if (prefix) {
4758
+ const siblings = findParentProperties(state, node);
4759
+ for (const [k, prop2] of Object.entries(siblings ?? {})) {
4760
+ add(`${prefix}/${k}`, prop2);
4761
+ }
4762
+ }
4763
+ return [...options.values()];
4764
+ }
4765
+ function findParentProperties(state, node) {
4766
+ let props = state.schema.properties;
4767
+ for (let i = 0; i < node.path.length - 1; i++) {
4768
+ const seg = node.path[i];
4769
+ if (seg.kind !== "prop" || !props) return void 0;
4770
+ const next = props[seg.key];
4771
+ props = next?.type === "array" ? next.items?.properties : next?.properties;
4772
+ if (!props) return void 0;
4773
+ }
4774
+ return props;
4775
+ }
4776
+
4777
+ // src/inspector/RuleEditor.tsx
4778
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4779
+ function RuleEditor({
4780
+ node,
4781
+ state
4782
+ }) {
4783
+ const scopes = useMemo12(() => scopePointers(state, node), [state, node]);
4784
+ return /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
4785
+ /* @__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." }),
4786
+ /* @__PURE__ */ jsx15(
4787
+ RuleFields,
4788
+ {
4789
+ rule: node.property["x-ui-rule"],
4790
+ scopes,
4791
+ effects: FIELD_EFFECT_OPTIONS,
4792
+ idPrefix: "rule",
4793
+ disabled: state.readOnly,
4794
+ onChange: (rule) => state.updateProperty(node.path, { "x-ui-rule": rule })
4795
+ }
4796
+ )
4797
+ ] });
4798
+ }
4799
+
4800
+ // src/inspector/ReferenceTab.tsx
4801
+ import { useMemo as useMemo13, useRef as useRef5 } from "react";
4802
+ import { JsonSchemaFormRenderer as JsonSchemaFormRenderer7 } from "@petrarca/sonnet-forms";
4803
+ import { Button as Button4 } from "@petrarca/sonnet-ui";
4804
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4805
+ function toOverrides(prop2, required) {
4806
+ return {
4807
+ title: prop2.title,
4808
+ description: prop2.description,
4809
+ // Coerce to string for the text field; non-string defaults (number, boolean)
4810
+ // are serialised and written back as strings -- v1 limitation.
4811
+ default: prop2.default == null ? void 0 : String(prop2.default),
4812
+ required
4813
+ };
4814
+ }
4815
+ function toPatch(values) {
4816
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
4817
+ return {
4818
+ title: str(values.title),
4819
+ description: str(values.description),
4820
+ default: str(values.default)
4821
+ };
4822
+ }
4823
+ function ReferenceTab({
4824
+ node,
4825
+ state
4826
+ }) {
4827
+ const schema = useMemo13(() => buildReferenceMetaSchema(), []);
4828
+ const values = useMemo13(
4829
+ () => toOverrides(node.property, node.required),
4830
+ [node.property, node.required]
4831
+ );
4832
+ const prevRef = useRef5(values);
4833
+ prevRef.current = values;
4834
+ const handleChange = (data) => {
4835
+ const next = data;
4836
+ const prev = prevRef.current;
4837
+ if (next.required !== prev.required) {
4838
+ state.setRequired(node.path, !!next.required);
4839
+ return;
4840
+ }
4841
+ state.updateProperty(node.path, toPatch(next));
4842
+ };
4843
+ return /* @__PURE__ */ jsxs14("div", { className: "space-y-4", children: [
4844
+ /* @__PURE__ */ jsxs14("p", { className: "text-sm text-muted-foreground", children: [
4845
+ "References ",
4846
+ /* @__PURE__ */ jsx16("span", { className: "font-medium", children: node.reference }),
4847
+ ". 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."
4848
+ ] }),
4849
+ /* @__PURE__ */ jsx16(
4850
+ JsonSchemaFormRenderer7,
4851
+ {
4852
+ schema,
4853
+ data: values,
4854
+ onChange: handleChange,
4855
+ deferStateUpdates: true,
4856
+ showActions: false,
4857
+ showExtraProperties: false,
4858
+ readOnly: state.readOnly
4859
+ },
4860
+ pathKey(node.path)
4861
+ ),
4862
+ !state.readOnly && state.canEditProperty(node.path) && /* @__PURE__ */ jsxs14("div", { className: "border-t pt-3", children: [
4863
+ /* @__PURE__ */ jsx16(
4864
+ Button4,
4865
+ {
4866
+ variant: "outline",
4867
+ size: "sm",
4868
+ onClick: () => state.changePropertyType(node.path, "string"),
4869
+ children: "Stop referencing"
4870
+ }
4871
+ ),
4872
+ /* @__PURE__ */ jsx16("p", { className: "mt-1 text-xs text-muted-foreground", children: "Removes the reference and turns this into an editable property." })
4873
+ ] })
4874
+ ] });
4875
+ }
4876
+
2525
4877
  // src/inspector/Inspector.tsx
2526
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
4878
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2527
4879
  var ENUM_CAPABLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer"]);
2528
4880
  function KeyField({
2529
4881
  fieldKey,
2530
4882
  onRename,
2531
4883
  readOnly
2532
4884
  }) {
2533
- const [editing, setEditing] = useState7(false);
2534
- const [value, setValue] = useState7(fieldKey);
2535
- const ref = useRef4(null);
2536
- useEffect4(() => {
4885
+ const [editing, setEditing] = useState8(false);
4886
+ const [value, setValue] = useState8(fieldKey);
4887
+ const ref = useRef6(null);
4888
+ useEffect5(() => {
2537
4889
  setValue(fieldKey);
2538
4890
  setEditing(false);
2539
4891
  }, [fieldKey]);
2540
- useEffect4(() => {
4892
+ useEffect5(() => {
2541
4893
  if (editing) {
2542
4894
  ref.current?.focus();
2543
4895
  ref.current?.select();
@@ -2553,14 +4905,14 @@ function KeyField({
2553
4905
  setEditing(false);
2554
4906
  };
2555
4907
  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, {}) })
4908
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
4909
+ /* @__PURE__ */ jsx17("span", { className: "text-sm font-semibold truncate", children: fieldKey }),
4910
+ !readOnly && /* @__PURE__ */ jsx17(IconButton4, { label: "Rename property", onClick: () => setEditing(true), children: /* @__PURE__ */ jsx17(Pencil, {}) })
2559
4911
  ] });
2560
4912
  }
2561
- return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1", children: [
2562
- /* @__PURE__ */ jsx11(
2563
- Input4,
4913
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
4914
+ /* @__PURE__ */ jsx17(
4915
+ Input5,
2564
4916
  {
2565
4917
  ref,
2566
4918
  value,
@@ -2577,8 +4929,8 @@ function KeyField({
2577
4929
  className: "h-7 text-sm font-medium"
2578
4930
  }
2579
4931
  ),
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, {}) })
4932
+ /* @__PURE__ */ jsx17(IconButton4, { label: "Confirm rename", onClick: commit, children: /* @__PURE__ */ jsx17(Check2, {}) }),
4933
+ /* @__PURE__ */ jsx17(IconButton4, { label: "Cancel rename", onClick: cancel, children: /* @__PURE__ */ jsx17(X, {}) })
2582
4934
  ] });
2583
4935
  }
2584
4936
  function findNode(nodes, path) {
@@ -2589,49 +4941,109 @@ function findNode(nodes, path) {
2589
4941
  }
2590
4942
  return void 0;
2591
4943
  }
4944
+ function InspectorHeader({
4945
+ node,
4946
+ state
4947
+ }) {
4948
+ const isDefinition = node.kind === "definition";
4949
+ const noRename = isDefinition ? state.capabilities.locked : state.readOnly || node.slotKind === "branch" || node.slotKind === "conditional";
4950
+ const onRename = (next) => isDefinition ? state.renameDefinition(node.key, next) : state.renameProperty(node.path, next);
4951
+ return /* @__PURE__ */ jsxs15("div", { className: "mb-3 space-y-1", children: [
4952
+ /* @__PURE__ */ jsx17(
4953
+ KeyField,
4954
+ {
4955
+ fieldKey: node.key,
4956
+ readOnly: noRename,
4957
+ onRename
4958
+ },
4959
+ pathKey(node.path)
4960
+ ),
4961
+ /* @__PURE__ */ jsx17("p", { className: "font-mono text-xs text-muted-foreground", children: isDefinition ? "definition" : node.typeLabel })
4962
+ ] });
4963
+ }
4964
+ function PropertyTabs({
4965
+ node,
4966
+ state,
4967
+ widgetOptions
4968
+ }) {
4969
+ const enumCapable = ENUM_CAPABLE_TYPES.has(node.property.type ?? "");
4970
+ const ruleUnsupported = node.path.some(
4971
+ (s) => s.kind === "branch" || s.kind === "condition" || s.kind === "slot"
4972
+ );
4973
+ const uiUnsupported = node.path.some((s) => s.kind !== "prop");
4974
+ return /* @__PURE__ */ jsxs15(Tabs2, { defaultValue: "data", children: [
4975
+ /* @__PURE__ */ jsxs15(TabsList2, { className: "mb-3", children: [
4976
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "data", children: "Data" }),
4977
+ enumCapable && /* @__PURE__ */ jsx17(TabsTrigger2, { value: "enum", children: "Enum" }),
4978
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "ui", children: "UI" }),
4979
+ !ruleUnsupported && /* @__PURE__ */ jsx17(TabsTrigger2, { value: "rule", children: "Rule" }),
4980
+ /* @__PURE__ */ jsx17(TabsTrigger2, { value: "table", children: "Table" })
4981
+ ] }),
4982
+ /* @__PURE__ */ jsx17(TabsContent2, { value: "data", children: /* @__PURE__ */ jsx17(DataTab, { node, state }) }),
4983
+ enumCapable && /* @__PURE__ */ jsx17(TabsContent2, { value: "enum", children: /* @__PURE__ */ jsx17(EnumEditor, { node, state }) }),
4984
+ !ruleUnsupported && /* @__PURE__ */ jsx17(TabsContent2, { value: "rule", children: /* @__PURE__ */ jsx17(RuleEditor, { node, state }) }),
4985
+ /* @__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 }) }),
4986
+ /* @__PURE__ */ jsx17(TabsContent2, { value: "table", children: /* @__PURE__ */ jsx17(TableHintsTab, { node, state }) })
4987
+ ] }, `${pathKey(node.path)}:${enumCapable}`);
4988
+ }
4989
+ function InspectorBody({
4990
+ node,
4991
+ state,
4992
+ widgetOptions
4993
+ }) {
4994
+ const lastSeg = node.path[node.path.length - 1];
4995
+ if (node.kind === "reference") {
4996
+ return /* @__PURE__ */ jsx17(ReferenceTab, { node, state });
4997
+ }
4998
+ if (node.readOnly) {
4999
+ 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." });
5000
+ }
5001
+ if (node.kind === "definition") {
5002
+ return /* @__PURE__ */ jsxs15("p", { className: "text-sm text-muted-foreground p-2", children: [
5003
+ "Reusable definition ",
5004
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: node.key }),
5005
+ ". Add or edit its fields in the tree; reference it from a property to reuse its shape. Renaming it here updates every reference."
5006
+ ] });
5007
+ }
5008
+ if (node.kind === "conditional") {
5009
+ return /* @__PURE__ */ jsx17(ConditionEditor, { node, state });
5010
+ }
5011
+ if (node.slotKind === "conditional" && lastSeg?.kind === "slot") {
5012
+ return /* @__PURE__ */ jsxs15("p", { className: "text-sm text-muted-foreground p-2", children: [
5013
+ "Add fields to this ",
5014
+ /* @__PURE__ */ jsx17("span", { className: "font-medium", children: node.key }),
5015
+ " ",
5016
+ "branch from the tree. They apply when the condition",
5017
+ " ",
5018
+ node.key === "then" ? "holds" : "does not hold",
5019
+ "."
5020
+ ] });
5021
+ }
5022
+ return /* @__PURE__ */ jsx17(PropertyTabs, { node, state, widgetOptions });
5023
+ }
2592
5024
  function Inspector({
2593
5025
  state,
2594
5026
  widgetOptions
2595
5027
  }) {
2596
- const tree = useMemo8(() => buildPropertyTree(state.schema), [state.schema]);
5028
+ const tree = useMemo14(
5029
+ () => buildPropertyTree(state.schema, state.refResolver),
5030
+ [state.schema, state.refResolver]
5031
+ );
2597
5032
  const node = state.selection ? findNode(tree, state.selection) : void 0;
2598
5033
  if (!node) {
2599
- return /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground", children: "Select a property to edit." });
5034
+ return /* @__PURE__ */ jsx17("p", { className: "text-sm text-muted-foreground", children: "Select a property to edit." });
2600
5035
  }
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}`)
5036
+ return /* @__PURE__ */ jsxs15("div", { children: [
5037
+ /* @__PURE__ */ jsx17(InspectorHeader, { node, state }),
5038
+ /* @__PURE__ */ jsx17(InspectorBody, { node, state, widgetOptions })
2627
5039
  ] });
2628
5040
  }
2629
5041
 
2630
5042
  // src/SchemaDesigner.tsx
2631
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
5043
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2632
5044
  function SchemaDesigner(props) {
2633
- const [capabilities, setCapabilities] = useState8(props.capabilities);
2634
- useEffect5(() => {
5045
+ const [capabilities, setCapabilities] = useState9(props.capabilities);
5046
+ useEffect6(() => {
2635
5047
  setCapabilities(props.capabilities);
2636
5048
  }, [props.capabilities]);
2637
5049
  const state = useDesignerState({ ...props, capabilities });
@@ -2657,17 +5069,17 @@ function SchemaDesigner(props) {
2657
5069
  maxWidth: 520,
2658
5070
  direction: "right-edge"
2659
5071
  });
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(
5072
+ const [inspectorCollapsed, setInspectorCollapsed] = useState9(false);
5073
+ const [treeCollapsed, setTreeCollapsed] = useState9(false);
5074
+ const [settingsOpen, setSettingsOpen] = useState9(false);
5075
+ return /* @__PURE__ */ jsxs16("div", { className: "flex w-full h-full overflow-hidden border rounded-lg bg-background", children: [
5076
+ !treeCollapsed && /* @__PURE__ */ jsxs16(Fragment5, { children: [
5077
+ /* @__PURE__ */ jsx18(
2666
5078
  "div",
2667
5079
  {
2668
5080
  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(
5081
+ 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",
5082
+ children: /* @__PURE__ */ jsx18(
2671
5083
  PropertyTree,
2672
5084
  {
2673
5085
  state,
@@ -2676,7 +5088,7 @@ function SchemaDesigner(props) {
2676
5088
  )
2677
5089
  }
2678
5090
  ),
2679
- state.capabilities.showPreview && /* @__PURE__ */ jsx12(
5091
+ state.capabilities.showPreview && /* @__PURE__ */ jsx18(
2680
5092
  ResizeHandle,
2681
5093
  {
2682
5094
  orientation: "vertical",
@@ -2686,15 +5098,15 @@ function SchemaDesigner(props) {
2686
5098
  }
2687
5099
  )
2688
5100
  ] }),
2689
- treeCollapsed && /* @__PURE__ */ jsx12("div", { className: "shrink-0 border-r flex flex-col items-center gap-1 p-1", children: /* @__PURE__ */ jsx12(
5101
+ treeCollapsed && /* @__PURE__ */ jsx18("div", { className: "shrink-0 border-r flex flex-col items-center gap-1 p-1", children: /* @__PURE__ */ jsx18(
2690
5102
  IconButton5,
2691
5103
  {
2692
5104
  label: "Expand properties",
2693
5105
  onClick: () => setTreeCollapsed(false),
2694
- children: /* @__PURE__ */ jsx12(PanelLeftOpen, {})
5106
+ children: /* @__PURE__ */ jsx18(PanelLeftOpen, {})
2695
5107
  }
2696
5108
  ) }),
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(
5109
+ 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
5110
  PreviewPane,
2699
5111
  {
2700
5112
  state,
@@ -2702,8 +5114,8 @@ function SchemaDesigner(props) {
2702
5114
  onCapabilitiesChange: setCapabilities
2703
5115
  }
2704
5116
  ) }),
2705
- !inspectorCollapsed && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2706
- /* @__PURE__ */ jsx12(
5117
+ !inspectorCollapsed && /* @__PURE__ */ jsxs16(Fragment5, { children: [
5118
+ /* @__PURE__ */ jsx18(
2707
5119
  ResizeHandle,
2708
5120
  {
2709
5121
  orientation: "vertical",
@@ -2712,57 +5124,57 @@ function SchemaDesigner(props) {
2712
5124
  onDoubleClick: resetInspectorWidth
2713
5125
  }
2714
5126
  ),
2715
- /* @__PURE__ */ jsxs10(
5127
+ /* @__PURE__ */ jsxs16(
2716
5128
  "div",
2717
5129
  {
2718
5130
  style: { width: inspectorWidth },
2719
5131
  className: "shrink-0 border-l overflow-auto p-4 bg-card",
2720
5132
  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(
5133
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-1 mb-2", children: [
5134
+ /* @__PURE__ */ jsx18("p", { className: "heading-meta", children: "Inspector" }),
5135
+ /* @__PURE__ */ jsxs16("span", { className: "ml-auto flex gap-0.5", children: [
5136
+ /* @__PURE__ */ jsx18(
2725
5137
  IconButton5,
2726
5138
  {
2727
5139
  label: "Form settings",
2728
5140
  onClick: () => setSettingsOpen(true),
2729
- children: /* @__PURE__ */ jsx12(Settings, {})
5141
+ children: /* @__PURE__ */ jsx18(Settings, {})
2730
5142
  }
2731
5143
  ),
2732
- /* @__PURE__ */ jsx12(
5144
+ /* @__PURE__ */ jsx18(
2733
5145
  IconButton5,
2734
5146
  {
2735
5147
  label: "Collapse inspector",
2736
5148
  onClick: () => setInspectorCollapsed(true),
2737
- children: /* @__PURE__ */ jsx12(PanelRightClose, {})
5149
+ children: /* @__PURE__ */ jsx18(PanelRightClose, {})
2738
5150
  }
2739
5151
  )
2740
5152
  ] })
2741
5153
  ] }),
2742
- /* @__PURE__ */ jsx12(Inspector, { state, widgetOptions: props.widgetOptions })
5154
+ /* @__PURE__ */ jsx18(Inspector, { state, widgetOptions: props.widgetOptions })
2743
5155
  ]
2744
5156
  }
2745
5157
  )
2746
5158
  ] }),
2747
- inspectorCollapsed && /* @__PURE__ */ jsxs10("div", { className: "shrink-0 border-l flex flex-col items-center gap-1 p-1", children: [
2748
- /* @__PURE__ */ jsx12(
5159
+ inspectorCollapsed && /* @__PURE__ */ jsxs16("div", { className: "shrink-0 border-l flex flex-col items-center gap-1 p-1", children: [
5160
+ /* @__PURE__ */ jsx18(
2749
5161
  IconButton5,
2750
5162
  {
2751
5163
  label: "Expand inspector",
2752
5164
  onClick: () => setInspectorCollapsed(false),
2753
- children: /* @__PURE__ */ jsx12(PanelRightOpen, {})
5165
+ children: /* @__PURE__ */ jsx18(PanelRightOpen, {})
2754
5166
  }
2755
5167
  ),
2756
- /* @__PURE__ */ jsx12(
5168
+ /* @__PURE__ */ jsx18(
2757
5169
  IconButton5,
2758
5170
  {
2759
5171
  label: "Form settings",
2760
5172
  onClick: () => setSettingsOpen(true),
2761
- children: /* @__PURE__ */ jsx12(Settings, {})
5173
+ children: /* @__PURE__ */ jsx18(Settings, {})
2762
5174
  }
2763
5175
  )
2764
5176
  ] }),
2765
- /* @__PURE__ */ jsx12(
5177
+ /* @__PURE__ */ jsx18(
2766
5178
  FormSettingsSheet,
2767
5179
  {
2768
5180
  open: settingsOpen,
@@ -2784,41 +5196,77 @@ export {
2784
5196
  TableHintsTab,
2785
5197
  TablePreview,
2786
5198
  UITab,
5199
+ addBranch,
5200
+ addConditional,
5201
+ addDefinition,
5202
+ addElse,
5203
+ addLayoutGroup,
2787
5204
  addProperty,
5205
+ branch,
2788
5206
  buildPropertyTree,
2789
5207
  capabilitiesSchema,
2790
5208
  changePropertyType,
2791
5209
  childContainer,
2792
- childrenOf,
5210
+ childSlots,
2793
5211
  compatibleWidgets,
5212
+ condition,
5213
+ def,
5214
+ definitionUsageCount,
5215
+ definitionsOf,
2794
5216
  deleteProperty,
2795
5217
  embedUiSchema,
2796
5218
  getProperty,
2797
5219
  getScopedLayout,
2798
5220
  getUiEntryAtPath,
2799
5221
  isContainer,
5222
+ isReservedKey,
2800
5223
  layoutScopes,
5224
+ moveBranch,
5225
+ moveLayoutGroup,
2801
5226
  moveProperty,
5227
+ newBranch,
5228
+ newCondition,
2802
5229
  newProperty,
2803
5230
  orderedKeys,
5231
+ parsePathKey,
2804
5232
  pathKey,
5233
+ placeFieldInGroup,
5234
+ prop,
2805
5235
  propertyKind,
2806
5236
  readEnumChoices,
2807
5237
  readTableHints,
5238
+ removeBranch,
5239
+ removeConditional,
5240
+ removeDefinition,
5241
+ removeElse,
5242
+ removeLayoutGroup,
5243
+ renameDefinition,
5244
+ renameLayoutGroup,
2808
5245
  renameProperty,
2809
5246
  resolveCapabilities,
2810
5247
  samePath,
5248
+ sameSegment,
2811
5249
  sampleRows,
5250
+ schemaAsRootProp,
5251
+ setCondition,
5252
+ setConditionalBehavior,
2812
5253
  setEnum,
2813
5254
  setGlobalOptions,
5255
+ setLayoutGroupRule,
2814
5256
  setPropertyLayout,
5257
+ setReference,
2815
5258
  setRequired,
2816
5259
  setRootLayout,
2817
5260
  setRootLayoutRows,
5261
+ setScopedGroupRows,
5262
+ setScopedLayoutGroups,
5263
+ setScopedLayoutMode,
2818
5264
  setScopedLayoutOptions,
2819
5265
  setScopedLayoutRows,
5266
+ setScopedLayoutVariant,
2820
5267
  setTableHints,
2821
5268
  setUiEntry,
5269
+ slot,
2822
5270
  updateProperty,
2823
5271
  useDesignerState,
2824
5272
  validateCapabilities