@petrarca/sonnet-schema-designer 0.4.2

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 ADDED
@@ -0,0 +1,2826 @@
1
+ // src/model/paths.ts
2
+ function pathKey(path) {
3
+ return path.join(".");
4
+ }
5
+ function samePath(a, b) {
6
+ if (a === null || b === null) return a === b;
7
+ return a.length === b.length && a.every((seg, i) => seg === b[i]);
8
+ }
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;
13
+ }
14
+ return void 0;
15
+ }
16
+ function getProperty(schema, path) {
17
+ if (path.length === 0) return void 0;
18
+ let container = schema.properties;
19
+ let prop;
20
+ 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);
25
+ }
26
+ return prop;
27
+ }
28
+ function isContainer(prop) {
29
+ return childContainer(prop) !== void 0;
30
+ }
31
+ function childrenOf(prop) {
32
+ return childContainer(prop) ?? {};
33
+ }
34
+
35
+ // 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";
40
+ return "scalar";
41
+ }
42
+ function arrayCardinality(min, max) {
43
+ if (min == null && max == null) return "";
44
+ if (min != null && min === max) return `${min}`;
45
+ return `${min ?? ""}..${max ?? ""}`;
46
+ }
47
+ function typeLabelOf(prop) {
48
+ if (prop.type === "array") {
49
+ const itemType = prop.items?.type ?? "any";
50
+ return `${itemType}[${arrayCardinality(prop.minItems, prop.maxItems)}]`;
51
+ }
52
+ return prop.type ?? "any";
53
+ }
54
+ function orderedKeys(properties, order) {
55
+ const keys = Object.keys(properties);
56
+ if (!order || order.length === 0) return keys;
57
+ const inOrder = order.filter((k) => k in properties);
58
+ const rest = keys.filter((k) => !inOrder.includes(k));
59
+ return [...inOrder, ...rest];
60
+ }
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;
64
+ const node = {
65
+ path,
66
+ key,
67
+ property: prop,
68
+ kind: propertyKind(prop),
69
+ required: !!requiredKeys?.includes(key),
70
+ typeLabel: typeLabelOf(prop),
71
+ widget,
72
+ children: []
73
+ };
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)
80
+ );
81
+ }
82
+ return node;
83
+ }
84
+ function buildPropertyTree(schema) {
85
+ return orderedKeys(schema.properties, schema["x-ui-order"]).map(
86
+ (key) => buildNode(key, schema.properties[key], [], schema.required)
87
+ );
88
+ }
89
+ function layoutScopes(schema) {
90
+ const scopes = [{ path: [], label: "Form (root)" }];
91
+ const walk = (nodes) => {
92
+ for (const node of nodes) {
93
+ if (node.kind === "object" || node.kind === "array") {
94
+ const suffix = node.kind === "array" ? " (items)" : "";
95
+ scopes.push({
96
+ path: node.path,
97
+ label: node.path.join(" \u203A ") + suffix
98
+ });
99
+ walk(node.children);
100
+ }
101
+ }
102
+ };
103
+ walk(buildPropertyTree(schema));
104
+ return scopes;
105
+ }
106
+
107
+ // src/model/mutate.ts
108
+ function clone(v) {
109
+ return structuredClone(v);
110
+ }
111
+ function newProperty(kind) {
112
+ switch (kind) {
113
+ case "object":
114
+ return { type: "object", properties: {} };
115
+ case "array":
116
+ return { type: "array", items: { type: "object", properties: {} } };
117
+ default:
118
+ return { type: "string" };
119
+ }
120
+ }
121
+ function resolveContainer(schema, parentPath) {
122
+ if (parentPath.length === 0) {
123
+ return {
124
+ properties: schema.properties,
125
+ getOrder: () => schema["x-ui-order"],
126
+ setOrder: (o) => {
127
+ if (o) schema["x-ui-order"] = o;
128
+ else delete schema["x-ui-order"];
129
+ },
130
+ getRequired: () => schema.required,
131
+ setRequired: (r) => {
132
+ if (r && r.length) schema.required = r;
133
+ else delete schema.required;
134
+ }
135
+ };
136
+ }
137
+ const owner = getProperty(schema, parentPath);
138
+ if (!owner) return null;
139
+ const properties = childContainer(owner);
140
+ if (!properties) return null;
141
+ const target = owner.type === "array" ? owner.items : owner;
142
+ return {
143
+ properties,
144
+ getOrder: () => target["x-ui-order"],
145
+ setOrder: (o) => {
146
+ if (o) target["x-ui-order"] = o;
147
+ else delete target["x-ui-order"];
148
+ },
149
+ getRequired: () => target.required,
150
+ setRequired: (r) => {
151
+ if (r && r.length) target.required = r;
152
+ else delete target.required;
153
+ }
154
+ };
155
+ }
156
+ function uniqueKey(base, properties) {
157
+ if (!(base in properties)) return base;
158
+ let i = 2;
159
+ while (`${base}_${i}` in properties) i++;
160
+ return `${base}_${i}`;
161
+ }
162
+ function addProperty(model, parentPath, kind) {
163
+ const schema = clone(model.schema);
164
+ const container = resolveContainer(schema, parentPath);
165
+ if (!container) return { model, path: parentPath };
166
+ const key = uniqueKey("property", container.properties);
167
+ container.properties[key] = newProperty(kind);
168
+ const order = container.getOrder();
169
+ if (order) container.setOrder([...order, key]);
170
+ return {
171
+ model: { schema, uiSchema: model.uiSchema },
172
+ path: [...parentPath, key]
173
+ };
174
+ }
175
+ function deleteProperty(model, path) {
176
+ if (path.length === 0) return model;
177
+ const schema = clone(model.schema);
178
+ const parentPath = path.slice(0, -1);
179
+ const key = path[path.length - 1];
180
+ const container = resolveContainer(schema, parentPath);
181
+ if (!container || !(key in container.properties)) return model;
182
+ delete container.properties[key];
183
+ const order = container.getOrder();
184
+ if (order) container.setOrder(order.filter((k) => k !== key));
185
+ const required = container.getRequired();
186
+ if (required) container.setRequired(required.filter((k) => k !== key));
187
+ let uiSchema = model.uiSchema;
188
+ if (parentPath.length === 0 && key in uiSchema) {
189
+ uiSchema = { ...uiSchema };
190
+ delete uiSchema[key];
191
+ }
192
+ return { schema, uiSchema };
193
+ }
194
+ function renameKeyInPlace(properties, prevKey, nextKey) {
195
+ const rebuilt = {};
196
+ for (const [k, v] of Object.entries(properties)) {
197
+ rebuilt[k === prevKey ? nextKey : k] = v;
198
+ }
199
+ for (const k of Object.keys(properties)) delete properties[k];
200
+ Object.assign(properties, rebuilt);
201
+ }
202
+ function renameUiEntry(uiSchema, parentPath, prevKey, nextKey) {
203
+ if (parentPath.length !== 0 || !(prevKey in uiSchema)) return uiSchema;
204
+ const next = { ...uiSchema };
205
+ next[nextKey] = next[prevKey];
206
+ delete next[prevKey];
207
+ return next;
208
+ }
209
+ function renameProperty(model, path, nextKey) {
210
+ if (path.length === 0) return { model, path };
211
+ const parentPath = path.slice(0, -1);
212
+ const prevKey = path[path.length - 1];
213
+ if (nextKey === prevKey || !nextKey) return { model, path };
214
+ const schema = clone(model.schema);
215
+ const container = resolveContainer(schema, parentPath);
216
+ if (!container || !(prevKey in container.properties)) return { model, path };
217
+ if (nextKey in container.properties) return { model, path };
218
+ 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));
224
+ const uiSchema = renameUiEntry(model.uiSchema, parentPath, prevKey, nextKey);
225
+ return {
226
+ model: { schema, uiSchema },
227
+ path: [...parentPath, nextKey]
228
+ };
229
+ }
230
+ function moveProperty(model, path, direction) {
231
+ if (path.length === 0) return model;
232
+ const schema = clone(model.schema);
233
+ const parentPath = path.slice(0, -1);
234
+ const key = path[path.length - 1];
235
+ const container = resolveContainer(schema, parentPath);
236
+ if (!container) return model;
237
+ const keys = Object.keys(container.properties);
238
+ const order = container.getOrder() ?? keys;
239
+ const full = [...order.filter((k) => keys.includes(k))];
240
+ for (const k of keys) if (!full.includes(k)) full.push(k);
241
+ const idx = full.indexOf(key);
242
+ const swap = idx + direction;
243
+ if (idx < 0 || swap < 0 || swap >= full.length) return model;
244
+ [full[idx], full[swap]] = [full[swap], full[idx]];
245
+ container.setOrder(full);
246
+ return { schema, uiSchema: model.uiSchema };
247
+ }
248
+ function setRequired(model, path, required) {
249
+ if (path.length === 0) return model;
250
+ const schema = clone(model.schema);
251
+ const parentPath = path.slice(0, -1);
252
+ const key = path[path.length - 1];
253
+ const container = resolveContainer(schema, parentPath);
254
+ if (!container) return model;
255
+ const current = new Set(container.getRequired() ?? []);
256
+ if (required) current.add(key);
257
+ else current.delete(key);
258
+ container.setRequired([...current]);
259
+ return { schema, uiSchema: model.uiSchema };
260
+ }
261
+ function updateProperty(model, path, patch) {
262
+ if (path.length === 0) return model;
263
+ const schema = clone(model.schema);
264
+ const prop = getProperty(schema, path);
265
+ if (!prop) return model;
266
+ Object.assign(prop, patch);
267
+ return { schema, uiSchema: model.uiSchema };
268
+ }
269
+ function changePropertyType(model, path, nextType) {
270
+ if (path.length === 0 || !nextType) return model;
271
+ 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" };
288
+ return { schema, uiSchema: model.uiSchema };
289
+ }
290
+ function normalizeChoices(choices) {
291
+ const seen = /* @__PURE__ */ new Set();
292
+ const clean = [];
293
+ for (const c of choices) {
294
+ const value = c.value.trim();
295
+ if (!value || seen.has(value)) continue;
296
+ seen.add(value);
297
+ clean.push({ value, label: c.label?.trim() || void 0 });
298
+ }
299
+ return clean;
300
+ }
301
+ function withEnumNames(entry, choices) {
302
+ const enumNames = {};
303
+ for (const c of choices) if (c.label) enumNames[c.value] = c.label;
304
+ const next = { ...entry ?? {} };
305
+ const options = {
306
+ ...next["x-ui-options"] ?? {}
307
+ };
308
+ if (Object.keys(enumNames).length) options.enumNames = enumNames;
309
+ else delete options.enumNames;
310
+ if (Object.keys(options).length) next["x-ui-options"] = options;
311
+ else delete next["x-ui-options"];
312
+ return Object.keys(next).length ? next : void 0;
313
+ }
314
+ function setEnum(model, path, choices) {
315
+ if (path.length === 0) return model;
316
+ const schema = clone(model.schema);
317
+ const prop = getProperty(schema, path);
318
+ if (!prop) return model;
319
+ const clean = normalizeChoices(choices);
320
+ if (clean.length === 0) delete prop.enum;
321
+ else prop.enum = clean.map((c) => c.value);
322
+ const nextEntry = withEnumNames(
323
+ getUiEntryAtPath(model.uiSchema, path),
324
+ clean
325
+ );
326
+ const uiSchema = setEntryIn(model.uiSchema, path, nextEntry);
327
+ return { schema, uiSchema };
328
+ }
329
+ function readEnumChoices(prop, uiEntry) {
330
+ if (!Array.isArray(prop.enum)) return [];
331
+ const enumNames = uiEntry?.["x-ui-options"]?.enumNames ?? {};
332
+ return prop.enum.map((v) => {
333
+ const value = String(v);
334
+ return { value, label: enumNames[value] };
335
+ });
336
+ }
337
+ function setTableHints(model, path, hints) {
338
+ if (path.length === 0) return model;
339
+ 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"];
344
+ if (hints.cellOptions && Object.keys(hints.cellOptions).length > 0) {
345
+ prop["x-ui-cell-options"] = hints.cellOptions;
346
+ } else {
347
+ delete prop["x-ui-cell-options"];
348
+ }
349
+ if (hints.column && Object.keys(hints.column).length > 0) {
350
+ prop["x-ui-column"] = hints.column;
351
+ } else {
352
+ delete prop["x-ui-column"];
353
+ }
354
+ return { schema, uiSchema: model.uiSchema };
355
+ }
356
+ function readTableHints(prop) {
357
+ 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"]
361
+ };
362
+ }
363
+ 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++) {
367
+ const fields = entry["x-ui-fields"];
368
+ entry = fields?.[path[i]];
369
+ }
370
+ return entry;
371
+ }
372
+ function setEntryIn(map, path, entry) {
373
+ const [head, ...rest] = path;
374
+ const next = { ...map };
375
+ if (rest.length === 0) {
376
+ if (entry && Object.keys(entry).length > 0) next[head] = entry;
377
+ else delete next[head];
378
+ return next;
379
+ }
380
+ const current = { ...next[head] ?? {} };
381
+ const childFields = current["x-ui-fields"] ?? {};
382
+ const updatedFields = setEntryIn(childFields, rest, entry);
383
+ if (Object.keys(updatedFields).length > 0) {
384
+ current["x-ui-fields"] = updatedFields;
385
+ } else {
386
+ delete current["x-ui-fields"];
387
+ }
388
+ if (Object.keys(current).length > 0) next[head] = current;
389
+ else delete next[head];
390
+ return next;
391
+ }
392
+ function setUiEntry(model, path, entry) {
393
+ if (path.length === 0) return model;
394
+ const uiSchema = setEntryIn(model.uiSchema, path, entry);
395
+ return { schema: model.schema, uiSchema };
396
+ }
397
+ function setPropertyLayout(model, path, layout) {
398
+ if (path.length === 0) return model;
399
+ 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;
403
+ if (!target) return model;
404
+ if (layout) target["x-ui-layout"] = layout;
405
+ else delete target["x-ui-layout"];
406
+ return { schema, uiSchema: model.uiSchema };
407
+ }
408
+ function setRootLayout(model, layout) {
409
+ const uiSchema = { ...model.uiSchema };
410
+ if (layout) uiSchema["x-ui-layout"] = layout;
411
+ else delete uiSchema["x-ui-layout"];
412
+ return { schema: model.schema, uiSchema };
413
+ }
414
+ function setRootLayoutRows(model, rows) {
415
+ const uiSchema = { ...model.uiSchema };
416
+ const layout = { ...uiSchema["x-ui-layout"] ?? {} };
417
+ if (rows && rows.length > 0) layout.rows = rows;
418
+ else delete layout.rows;
419
+ if (Object.keys(layout).length > 0) uiSchema["x-ui-layout"] = layout;
420
+ else delete uiSchema["x-ui-layout"];
421
+ return { schema: model.schema, uiSchema };
422
+ }
423
+ function setScopedLayoutRows(model, scopePath, rows) {
424
+ if (scopePath.length === 0) return setRootLayoutRows(model, rows);
425
+ 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;
429
+ if (!target) return model;
430
+ const layout = { ...target["x-ui-layout"] ?? {} };
431
+ if (rows && rows.length > 0) layout.rows = rows;
432
+ else delete layout.rows;
433
+ if (Object.keys(layout).length > 0) target["x-ui-layout"] = layout;
434
+ else delete target["x-ui-layout"];
435
+ return { schema, uiSchema: model.uiSchema };
436
+ }
437
+ function getScopedLayout(schema, uiSchema, scopePath) {
438
+ if (scopePath.length === 0) {
439
+ return uiSchema["x-ui-layout"];
440
+ }
441
+ const prop = getProperty(schema, scopePath);
442
+ const target = prop?.type === "array" ? prop.items : prop;
443
+ return target?.["x-ui-layout"];
444
+ }
445
+ function setScopedLayoutOptions(model, scopePath, options) {
446
+ const current = getScopedLayout(model.schema, model.uiSchema, scopePath) ?? {};
447
+ const merged = { ...current };
448
+ for (const key of ["direction", "gap", "labels", "compact"]) {
449
+ const v = options[key];
450
+ if (v === void 0 || typeof v === "number" && Number.isNaN(v))
451
+ delete merged[key];
452
+ else merged[key] = v;
453
+ }
454
+ const layout = Object.keys(merged).length > 0 ? merged : void 0;
455
+ if (scopePath.length === 0) return setRootLayout(model, layout);
456
+ return setPropertyLayout(model, scopePath, layout);
457
+ }
458
+ function setGlobalOptions(model, options) {
459
+ const uiSchema = { ...model.uiSchema };
460
+ if (options) uiSchema["x-ui-globalOptions"] = options;
461
+ else delete uiSchema["x-ui-globalOptions"];
462
+ return { schema: model.schema, uiSchema };
463
+ }
464
+
465
+ // src/model/compatibleWidgets.ts
466
+ var AUTO = { value: null, label: "Auto (resolve from schema)" };
467
+ var STRING_WIDGETS = [
468
+ { value: "text", label: "Text input" },
469
+ { value: "textarea", label: "Textarea" },
470
+ { value: "select", label: "Select" },
471
+ { value: "tags", label: "Tags" },
472
+ { value: "entity-select", label: "Entity select" },
473
+ { value: "json-editor", label: "JSON editor" }
474
+ ];
475
+ var NUMBER_WIDGETS = [
476
+ { value: "number", label: "Number" },
477
+ { value: "select", label: "Select" },
478
+ { value: "quantity", label: "Quantity" }
479
+ ];
480
+ var BOOLEAN_WIDGETS = [
481
+ { value: "checkbox", label: "Checkbox" }
482
+ ];
483
+ var OBJECT_WIDGETS = [
484
+ { value: "object", label: "Object (sub-form)" },
485
+ { value: "json-editor", label: "JSON editor" }
486
+ ];
487
+ var ARRAY_WIDGETS = [
488
+ { value: "array", label: "Array (repeatable)" },
489
+ { value: "select", label: "Multi-select" },
490
+ { value: "tags", label: "Tags" }
491
+ ];
492
+ function compatibleWidgets(prop) {
493
+ switch (prop.type) {
494
+ case "string":
495
+ return [AUTO, ...STRING_WIDGETS];
496
+ case "number":
497
+ case "integer":
498
+ return [AUTO, ...NUMBER_WIDGETS];
499
+ case "boolean":
500
+ return [AUTO, ...BOOLEAN_WIDGETS];
501
+ case "object":
502
+ return [AUTO, ...OBJECT_WIDGETS];
503
+ case "array":
504
+ return [AUTO, ...ARRAY_WIDGETS];
505
+ default:
506
+ return [AUTO];
507
+ }
508
+ }
509
+
510
+ // src/model/embed.ts
511
+ var EMBEDDABLE_KEYS = [
512
+ "x-ui-widget",
513
+ "x-ui-options",
514
+ "x-ui-title",
515
+ "x-ui-placeholder",
516
+ "x-ui-description"
517
+ ];
518
+ function embedUiSchema(schema, uiSchema) {
519
+ const next = structuredClone(schema);
520
+ for (const [fieldName, entry] of Object.entries(uiSchema)) {
521
+ const prop = next.properties[fieldName];
522
+ if (!prop || !entry) continue;
523
+ for (const key of EMBEDDABLE_KEYS) {
524
+ if (entry[key] !== void 0) {
525
+ prop[key] = entry[key];
526
+ }
527
+ }
528
+ }
529
+ return next;
530
+ }
531
+
532
+ // src/hooks/useDesignerState.ts
533
+ import { useCallback, useMemo, useRef, useState } from "react";
534
+
535
+ // src/capabilities.ts
536
+ import { z } from "zod";
537
+ import { warnLog } from "@petrarca/sonnet-core";
538
+ var capabilitiesSchema = z.object({
539
+ mode: z.enum(["data", "full"]).default("full").meta({
540
+ 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
+ }),
542
+ previews: z.array(z.enum(["form", "table", "json", "capabilities"])).optional().meta({
543
+ 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
+ }),
545
+ editablePaths: z.array(z.string()).optional().meta({
546
+ description: "Scoped editing. When set, the passed-in schema is fixed EVERYWHERE except the children of these JSON Pointer (RFC 6901) paths, which the user may add / delete / edit. Absent = fully editable. readOnly overrides this to fully locked."
547
+ })
548
+ }).meta({
549
+ id: "SchemaDesignerCapabilities",
550
+ title: "SchemaDesignerCapabilities",
551
+ description: "Capability profile for SchemaDesigner. Gates what a consumer can author. Every field is optional and defaults to the most permissive value (forward-compatible). Passing readOnly to the designer overrides this object to a fully-locked profile."
552
+ });
553
+ function defaultPreviews(mode) {
554
+ return mode === "data" ? ["json"] : ["form", "table", "json"];
555
+ }
556
+ function resolveCapabilities(capabilities, readOnly) {
557
+ const parsed = capabilitiesSchema.safeParse(capabilities ?? {});
558
+ const value = parsed.success ? parsed.data : capabilitiesSchema.parse({});
559
+ if (!parsed.success) {
560
+ warnLog(
561
+ "[SchemaDesigner] Invalid capabilities \u2014 falling back to defaults.",
562
+ parsed.error.issues
563
+ );
564
+ }
565
+ const mode = value.mode;
566
+ const previews = value.previews ?? defaultPreviews(mode);
567
+ const editablePaths = value.editablePaths ?? [];
568
+ return {
569
+ mode,
570
+ previews,
571
+ editablePaths,
572
+ locked: !!readOnly,
573
+ scoped: !readOnly && editablePaths.length > 0,
574
+ showCapabilities: previews.includes("capabilities"),
575
+ showPreview: previews.length > 0
576
+ };
577
+ }
578
+ function validateCapabilities(input) {
579
+ const parsed = capabilitiesSchema.safeParse(input);
580
+ if (parsed.success) return { ok: true, value: parsed.data };
581
+ const errors = parsed.error.issues.map((i) => {
582
+ const path = i.path.length ? i.path.join(".") : "(root)";
583
+ return `${path}: ${i.message}`;
584
+ });
585
+ return { ok: false, errors };
586
+ }
587
+
588
+ // src/model/scoping.ts
589
+ function escapeToken(token) {
590
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
591
+ }
592
+ function parentContainerPointer(schema, path) {
593
+ if (path.length === 0) return void 0;
594
+ let container = schema.properties;
595
+ let pointer = "/properties";
596
+ for (let i = 0; i < path.length; i++) {
597
+ if (!container) return void 0;
598
+ const seg = path[i];
599
+ const prop = container[seg];
600
+ if (!prop) return void 0;
601
+ if (i === path.length - 1) return pointer;
602
+ const next = childContainer(prop);
603
+ if (!next) return void 0;
604
+ pointer = prop.type === "array" ? `${pointer}/${escapeToken(seg)}/items/properties` : `${pointer}/${escapeToken(seg)}/properties`;
605
+ container = next;
606
+ }
607
+ return pointer;
608
+ }
609
+ function isPropertyEditable(schema, path, editablePaths) {
610
+ if (editablePaths.length === 0) return true;
611
+ const parent = parentContainerPointer(schema, path);
612
+ if (parent === void 0) return false;
613
+ return editablePaths.includes(parent);
614
+ }
615
+ function canAddChildTo(schema, path, editablePaths) {
616
+ if (editablePaths.length === 0) return true;
617
+ if (path.length === 0) return editablePaths.includes("/properties");
618
+ const own = ownChildContainerPointer(schema, path);
619
+ return own !== void 0 && editablePaths.includes(own);
620
+ }
621
+ function ownChildContainerPointer(schema, path) {
622
+ const parent = parentContainerPointer(schema, path);
623
+ if (parent === void 0) return void 0;
624
+ let container = schema.properties;
625
+ for (let i = 0; i < path.length; i++) {
626
+ const prop = container?.[path[i]];
627
+ if (!prop) return void 0;
628
+ if (i === path.length - 1) {
629
+ const seg = escapeToken(path[i]);
630
+ return prop.type === "array" ? `${parent}/${seg}/items/properties` : `${parent}/${seg}/properties`;
631
+ }
632
+ container = childContainer(prop);
633
+ }
634
+ return void 0;
635
+ }
636
+
637
+ // src/hooks/useDesignerState.ts
638
+ var EMPTY_SCHEMA = {
639
+ title: "",
640
+ type: "object",
641
+ properties: {}
642
+ };
643
+ function useDesignerState(args) {
644
+ const { onChange, readOnly = false } = args;
645
+ const controlled = args.schema !== void 0;
646
+ const [internal, setInternal] = useState(() => ({
647
+ schema: args.initialSchema ?? EMPTY_SCHEMA,
648
+ uiSchema: args.initialUiSchema ?? {}
649
+ }));
650
+ const [selection, setSelection] = useState(null);
651
+ const model = controlled ? { schema: args.schema, uiSchema: args.uiSchema ?? {} } : internal;
652
+ const modelRef = useRef(model);
653
+ modelRef.current = model;
654
+ const commit = useCallback(
655
+ (next) => {
656
+ if (!controlled) setInternal(next);
657
+ onChange?.(next.schema, next.uiSchema);
658
+ },
659
+ [controlled, onChange]
660
+ );
661
+ const select = useCallback((path) => {
662
+ setSelection((prev) => samePath(prev, path) ? prev : path);
663
+ }, []);
664
+ const capabilities = useMemo(
665
+ () => resolveCapabilities(args.capabilities, readOnly),
666
+ [args.capabilities, readOnly]
667
+ );
668
+ const canEditProperty = useCallback(
669
+ (path) => {
670
+ if (capabilities.locked) return false;
671
+ return isPropertyEditable(
672
+ modelRef.current.schema,
673
+ path,
674
+ capabilities.editablePaths
675
+ );
676
+ },
677
+ [capabilities.locked, capabilities.editablePaths]
678
+ );
679
+ const canAddChild = useCallback(
680
+ (path) => {
681
+ if (capabilities.locked) return false;
682
+ return canAddChildTo(
683
+ modelRef.current.schema,
684
+ path,
685
+ capabilities.editablePaths
686
+ );
687
+ },
688
+ [capabilities.locked, capabilities.editablePaths]
689
+ );
690
+ const ops = useMemo(
691
+ () => ({
692
+ addProperty(parentPath, kind) {
693
+ if (!canAddChild(parentPath)) return;
694
+ const res = addProperty(modelRef.current, parentPath, kind);
695
+ commit(res.model);
696
+ setSelection(res.path);
697
+ },
698
+ deleteProperty(path) {
699
+ if (!canEditProperty(path)) return;
700
+ commit(deleteProperty(modelRef.current, path));
701
+ setSelection((prev) => samePath(prev, path) ? null : prev);
702
+ },
703
+ renameProperty(path, nextKey) {
704
+ if (!canEditProperty(path)) return;
705
+ const res = renameProperty(modelRef.current, path, nextKey);
706
+ commit(res.model);
707
+ setSelection(res.path);
708
+ },
709
+ moveProperty(path, direction) {
710
+ if (!canEditProperty(path)) return;
711
+ commit(moveProperty(modelRef.current, path, direction));
712
+ },
713
+ setRequired(path, required) {
714
+ if (!canEditProperty(path)) return;
715
+ commit(setRequired(modelRef.current, path, required));
716
+ },
717
+ updateProperty(path, patch) {
718
+ if (!canEditProperty(path)) return;
719
+ commit(updateProperty(modelRef.current, path, patch));
720
+ },
721
+ changePropertyType(path, nextType) {
722
+ if (!canEditProperty(path)) return;
723
+ commit(changePropertyType(modelRef.current, path, nextType));
724
+ },
725
+ setEnum(path, choices) {
726
+ if (!canEditProperty(path)) return;
727
+ commit(setEnum(modelRef.current, path, choices));
728
+ },
729
+ // UI-schema / presentation ops use `readOnly` (not `capabilities.locked`)
730
+ // because they are not subject to scoped-path gating: presentation hints
731
+ // are always either fully editable or fully locked.
732
+ setUiEntry(path, entry) {
733
+ if (readOnly) return;
734
+ commit(setUiEntry(modelRef.current, path, entry));
735
+ },
736
+ setTableHints(path, hints) {
737
+ if (readOnly) return;
738
+ commit(setTableHints(modelRef.current, path, hints));
739
+ },
740
+ setPropertyLayout(path, layout) {
741
+ if (readOnly) return;
742
+ commit(setPropertyLayout(modelRef.current, path, layout));
743
+ },
744
+ setRootLayout(layout) {
745
+ if (readOnly) return;
746
+ commit(setRootLayout(modelRef.current, layout));
747
+ },
748
+ setRootLayoutRows(rows) {
749
+ if (readOnly) return;
750
+ commit(setRootLayoutRows(modelRef.current, rows));
751
+ },
752
+ setScopedLayoutRows(scopePath, rows) {
753
+ if (readOnly) return;
754
+ commit(setScopedLayoutRows(modelRef.current, scopePath, rows));
755
+ },
756
+ setScopedLayoutOptions(scopePath, options) {
757
+ if (readOnly) return;
758
+ commit(setScopedLayoutOptions(modelRef.current, scopePath, options));
759
+ },
760
+ setGlobalOptions(options) {
761
+ if (readOnly) return;
762
+ commit(setGlobalOptions(modelRef.current, options));
763
+ }
764
+ }),
765
+ [commit, readOnly, canEditProperty, canAddChild]
766
+ );
767
+ return {
768
+ schema: model.schema,
769
+ uiSchema: model.uiSchema,
770
+ selection,
771
+ readOnly,
772
+ capabilities,
773
+ canEditProperty,
774
+ canAddChild,
775
+ select,
776
+ ...ops
777
+ };
778
+ }
779
+
780
+ // src/SchemaDesigner.tsx
781
+ import { useEffect as useEffect5, useState as useState8 } from "react";
782
+ import { ResizeHandle, IconButton as IconButton5 } from "@petrarca/sonnet-ui";
783
+ import { useResizablePanel } from "@petrarca/sonnet-core/hooks";
784
+ import {
785
+ Settings,
786
+ PanelRightClose,
787
+ PanelRightOpen,
788
+ PanelLeftOpen
789
+ } from "lucide-react";
790
+
791
+ // src/FormSettingsSheet.tsx
792
+ import {
793
+ Sheet,
794
+ SheetContent,
795
+ SheetHeader,
796
+ SheetBody,
797
+ SheetTitle,
798
+ SheetDescription
799
+ } from "@petrarca/sonnet-ui";
800
+ import { JsonSchemaFormRenderer, FormSelect } from "@petrarca/sonnet-forms";
801
+
802
+ // src/inspector/layoutMetaSchemas.ts
803
+ function buildLayoutMetaSchema() {
804
+ return {
805
+ title: "",
806
+ type: "object",
807
+ properties: {
808
+ direction: {
809
+ type: "string",
810
+ title: "Direction",
811
+ enum: ["vertical", "horizontal"],
812
+ description: "Stack fields vertically or lay them out horizontally."
813
+ },
814
+ gap: {
815
+ type: "integer",
816
+ title: "Gap (px)",
817
+ minimum: 0,
818
+ description: "Space between fields."
819
+ },
820
+ labels: {
821
+ type: "boolean",
822
+ title: "Show labels"
823
+ },
824
+ compact: {
825
+ type: "boolean",
826
+ title: "Compact",
827
+ description: "Smaller control sizing."
828
+ }
829
+ }
830
+ };
831
+ }
832
+ function layoutToValues(layout) {
833
+ return {
834
+ direction: layout?.direction,
835
+ gap: layout?.gap,
836
+ labels: layout?.labels,
837
+ compact: layout?.compact
838
+ };
839
+ }
840
+ function valuesToLayout(values, existing) {
841
+ const next = { ...existing ?? {} };
842
+ if (values.direction === "vertical" || values.direction === "horizontal") {
843
+ next.direction = values.direction;
844
+ } else {
845
+ delete next.direction;
846
+ }
847
+ if (typeof values.gap === "number" && !Number.isNaN(values.gap)) {
848
+ next.gap = values.gap;
849
+ } else {
850
+ delete next.gap;
851
+ }
852
+ if (typeof values.labels === "boolean") next.labels = values.labels;
853
+ else delete next.labels;
854
+ if (typeof values.compact === "boolean") next.compact = values.compact;
855
+ else delete next.compact;
856
+ return Object.keys(next).length > 0 ? next : void 0;
857
+ }
858
+ function buildGlobalOptionsMetaSchema() {
859
+ return {
860
+ title: "",
861
+ type: "object",
862
+ properties: {
863
+ disabled: { type: "boolean", title: "Disable all fields" },
864
+ readonly: { type: "boolean", title: "Read-only all fields" },
865
+ hideLabels: {
866
+ type: "boolean",
867
+ title: "Hide all labels",
868
+ description: "Sets label: false on every field."
869
+ },
870
+ compact: { type: "boolean", title: "Compact all fields" }
871
+ }
872
+ };
873
+ }
874
+ function globalOptionsToValues(options) {
875
+ return {
876
+ disabled: options?.disabled,
877
+ readonly: options?.readonly,
878
+ hideLabels: options?.label === false,
879
+ compact: options?.compact
880
+ };
881
+ }
882
+ function valuesToGlobalOptions(values) {
883
+ const next = {};
884
+ if (values.disabled) next.disabled = true;
885
+ if (values.readonly) next.readonly = true;
886
+ if (values.hideLabels) next.label = false;
887
+ if (values.compact) next.compact = true;
888
+ return Object.keys(next).length > 0 ? next : void 0;
889
+ }
890
+
891
+ // src/FormSettingsSheet.tsx
892
+ import { jsx, jsxs } from "react/jsx-runtime";
893
+ function FormSettingsSheet({
894
+ open,
895
+ onOpenChange,
896
+ state
897
+ }) {
898
+ const globalOptions = state.uiSchema["x-ui-globalOptions"];
899
+ const optionValues = globalOptionsToValues(globalOptions);
900
+ const rootLayout = state.uiSchema["x-ui-layout"];
901
+ const labelPlacement = rootLayout?.labelPlacement ?? "top";
902
+ const setLabelPlacement = (value) => {
903
+ const next = { ...rootLayout ?? {} };
904
+ if (value === "start") next.labelPlacement = "start";
905
+ else delete next.labelPlacement;
906
+ state.setRootLayout(Object.keys(next).length ? next : void 0);
907
+ };
908
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { children: [
909
+ /* @__PURE__ */ jsxs(SheetHeader, { children: [
910
+ /* @__PURE__ */ jsx(SheetTitle, { children: "Form settings" }),
911
+ /* @__PURE__ */ jsx(SheetDescription, { children: "Options applied to every field in the form." })
912
+ ] }),
913
+ /* @__PURE__ */ jsxs(SheetBody, { className: "space-y-6", children: [
914
+ /* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
915
+ /* @__PURE__ */ jsx("p", { className: "heading-meta", children: "Layout" }),
916
+ /* @__PURE__ */ jsx(
917
+ FormSelect,
918
+ {
919
+ id: "root-labelPlacement",
920
+ name: "labelPlacement",
921
+ label: "Label placement",
922
+ value: labelPlacement,
923
+ options: [
924
+ { value: "top", label: "Above" },
925
+ { value: "start", label: "Inline (start)" }
926
+ ],
927
+ onChange: (v) => setLabelPlacement(v === "start" ? "start" : "top"),
928
+ disabled: state.readOnly,
929
+ clearable: false
930
+ }
931
+ )
932
+ ] }),
933
+ /* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
934
+ /* @__PURE__ */ jsx("p", { className: "heading-meta", children: "Global options" }),
935
+ /* @__PURE__ */ jsx(
936
+ JsonSchemaFormRenderer,
937
+ {
938
+ schema: buildGlobalOptionsMetaSchema(),
939
+ data: optionValues,
940
+ onChange: (data) => state.setGlobalOptions(
941
+ valuesToGlobalOptions(data)
942
+ ),
943
+ showActions: false,
944
+ showExtraProperties: false,
945
+ readOnly: state.readOnly
946
+ }
947
+ )
948
+ ] })
949
+ ] })
950
+ ] }) });
951
+ }
952
+
953
+ // src/tree/PropertyTree.tsx
954
+ import { useEffect, useRef as useRef2, useState as useState2 } from "react";
955
+ import { cn } from "@petrarca/sonnet-core";
956
+ import { IconButton, Input } from "@petrarca/sonnet-ui";
957
+ import {
958
+ Plus,
959
+ Trash2,
960
+ ChevronUp,
961
+ ChevronDown,
962
+ PanelLeftClose,
963
+ Lock
964
+ } from "lucide-react";
965
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
966
+ function PropertyTree({
967
+ state,
968
+ onCollapse
969
+ }) {
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" }),
974
+ /* @__PURE__ */ jsxs2("span", { className: "ml-auto flex gap-1", children: [
975
+ state.canAddChild([]) && /* @__PURE__ */ jsx2(
976
+ IconButton,
977
+ {
978
+ label: "Add property",
979
+ onClick: () => state.addProperty([], "scalar"),
980
+ children: /* @__PURE__ */ jsx2(Plus, {})
981
+ }
982
+ ),
983
+ onCollapse && /* @__PURE__ */ jsx2(IconButton, { label: "Collapse properties", onClick: onCollapse, children: /* @__PURE__ */ jsx2(PanelLeftClose, {}) })
984
+ ] })
985
+ ] }),
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(
987
+ PropertyRow,
988
+ {
989
+ node,
990
+ state,
991
+ depth: 0
992
+ },
993
+ pathKey(node.path)
994
+ )) })
995
+ ] });
996
+ }
997
+ function NameEditor({
998
+ initial,
999
+ onCommit,
1000
+ onCancel
1001
+ }) {
1002
+ const [value, setValue] = useState2(initial);
1003
+ const ref = useRef2(null);
1004
+ useEffect(() => {
1005
+ ref.current?.focus();
1006
+ ref.current?.select();
1007
+ }, []);
1008
+ const commit = () => {
1009
+ const trimmed = value.trim();
1010
+ if (trimmed && trimmed !== initial) onCommit(trimmed);
1011
+ else onCancel();
1012
+ };
1013
+ return /* @__PURE__ */ jsx2(
1014
+ Input,
1015
+ {
1016
+ ref,
1017
+ value,
1018
+ onChange: (e) => setValue(e.target.value),
1019
+ onClick: (e) => e.stopPropagation(),
1020
+ onBlur: commit,
1021
+ onKeyDown: (e) => {
1022
+ if (e.key === "Enter") {
1023
+ e.preventDefault();
1024
+ commit();
1025
+ } else if (e.key === "Escape") {
1026
+ e.preventDefault();
1027
+ onCancel();
1028
+ }
1029
+ },
1030
+ className: "h-6 text-sm py-0"
1031
+ }
1032
+ );
1033
+ }
1034
+ function RowLabel({
1035
+ node,
1036
+ editable,
1037
+ onStartRename
1038
+ }) {
1039
+ const title = editable ? `${node.key}${node.required ? " (required)" : ""} -- double-click to rename` : `${node.key} -- fixed (not editable)`;
1040
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1041
+ /* @__PURE__ */ jsxs2(
1042
+ "span",
1043
+ {
1044
+ className: "flex-1 min-w-0 truncate text-sm font-medium",
1045
+ onDoubleClick: onStartRename,
1046
+ title,
1047
+ children: [
1048
+ node.key,
1049
+ node.required && /* @__PURE__ */ jsx2("span", { className: "ml-0.5 text-destructive", "aria-label": "required", children: "*" })
1050
+ ]
1051
+ }
1052
+ ),
1053
+ !editable && /* @__PURE__ */ jsx2(
1054
+ Lock,
1055
+ {
1056
+ className: "h-3 w-3 shrink-0 text-muted-foreground/50",
1057
+ "aria-label": "fixed"
1058
+ }
1059
+ ),
1060
+ /* @__PURE__ */ jsx2(
1061
+ "span",
1062
+ {
1063
+ className: "shrink-0 max-w-[45%] truncate font-mono text-[11px] text-muted-foreground/70 group-hover/row:hidden",
1064
+ title: node.typeLabel,
1065
+ children: node.typeLabel
1066
+ }
1067
+ )
1068
+ ] });
1069
+ }
1070
+ function RowActions({
1071
+ node,
1072
+ state,
1073
+ editable,
1074
+ canAddHere
1075
+ }) {
1076
+ 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
+ canAddHere && /* @__PURE__ */ jsx2(
1104
+ IconButton,
1105
+ {
1106
+ label: "Add child property",
1107
+ revealOnHover: true,
1108
+ onClick: (e) => {
1109
+ e.stopPropagation();
1110
+ state.addProperty(node.path, "scalar");
1111
+ },
1112
+ children: /* @__PURE__ */ jsx2(Plus, {})
1113
+ }
1114
+ ),
1115
+ editable && /* @__PURE__ */ jsx2(
1116
+ IconButton,
1117
+ {
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, {})
1126
+ }
1127
+ )
1128
+ ] });
1129
+ }
1130
+ function PropertyRow({
1131
+ node,
1132
+ state,
1133
+ depth
1134
+ }) {
1135
+ const selected = samePath(state.selection, node.path);
1136
+ 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;
1140
+ const startRename = (e) => {
1141
+ e.stopPropagation();
1142
+ if (editable) setEditing(true);
1143
+ };
1144
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1145
+ /* @__PURE__ */ jsxs2(
1146
+ "div",
1147
+ {
1148
+ className: cn(
1149
+ "group/row relative flex items-center gap-2 px-3 py-2 cursor-pointer",
1150
+ selected && "bg-accent"
1151
+ ),
1152
+ style: { paddingLeft: 12 + depth * 16 },
1153
+ onClick: () => state.select(node.path),
1154
+ children: [
1155
+ editing ? /* @__PURE__ */ jsx2(
1156
+ NameEditor,
1157
+ {
1158
+ initial: node.key,
1159
+ onCommit: (next) => {
1160
+ setEditing(false);
1161
+ state.renameProperty(node.path, next);
1162
+ },
1163
+ onCancel: () => setEditing(false)
1164
+ }
1165
+ ) : /* @__PURE__ */ jsx2(
1166
+ RowLabel,
1167
+ {
1168
+ node,
1169
+ editable,
1170
+ onStartRename: startRename
1171
+ }
1172
+ ),
1173
+ showActions && !editing && // Absolutely positioned so the buttons do not reserve row width at
1174
+ // rest -- the name/type use the full row. The actions overlay the
1175
+ // right edge; each button reveals on hover via revealOnHover. The
1176
+ // background is applied only on hover so it does not cover the type
1177
+ // label at rest (accent for the selected row, background otherwise).
1178
+ /* @__PURE__ */ jsx2(
1179
+ "span",
1180
+ {
1181
+ className: cn(
1182
+ "absolute right-2 flex gap-0.5 rounded-md pl-2",
1183
+ selected ? "group-hover/row:bg-accent" : "group-hover/row:bg-background"
1184
+ ),
1185
+ children: /* @__PURE__ */ jsx2(
1186
+ RowActions,
1187
+ {
1188
+ node,
1189
+ state,
1190
+ editable,
1191
+ canAddHere
1192
+ }
1193
+ )
1194
+ }
1195
+ )
1196
+ ]
1197
+ }
1198
+ ),
1199
+ node.children.map((child) => /* @__PURE__ */ jsx2(
1200
+ PropertyRow,
1201
+ {
1202
+ node: child,
1203
+ state,
1204
+ depth: depth + 1
1205
+ },
1206
+ pathKey(child.path)
1207
+ ))
1208
+ ] });
1209
+ }
1210
+
1211
+ // src/preview/PreviewPane.tsx
1212
+ import { useMemo as useMemo4, useState as useState5 } from "react";
1213
+ import { Check, Copy } from "lucide-react";
1214
+ import {
1215
+ Button as Button3,
1216
+ Tabs,
1217
+ TabsList,
1218
+ TabsTrigger,
1219
+ TabsContent,
1220
+ TabsSeparator
1221
+ } from "@petrarca/sonnet-ui";
1222
+ import { JsonEditor as JsonEditor2 } from "@petrarca/sonnet-ui/json-editor";
1223
+ import { JsonSchemaFormRenderer as JsonSchemaFormRenderer3 } from "@petrarca/sonnet-forms";
1224
+ import { extractSchemaDefaults } from "@petrarca/sonnet-core/schema";
1225
+ import { copyTextToClipboard } from "@petrarca/sonnet-core";
1226
+
1227
+ // src/preview/TablePreview.tsx
1228
+ import { useMemo as useMemo2 } from "react";
1229
+ import { EntityTable } from "@petrarca/sonnet-ui";
1230
+ import { toPageResult } from "@petrarca/sonnet-core";
1231
+
1232
+ // src/preview/sampleRows.ts
1233
+ function sampleString(prop, key, i) {
1234
+ switch (prop.format) {
1235
+ case "email":
1236
+ return `user${i + 1}@example.com`;
1237
+ case "date":
1238
+ return `2024-0${i % 9 + 1}-15`;
1239
+ case "date-time":
1240
+ return `2024-0${i % 9 + 1}-15T09:30:00Z`;
1241
+ case "uri":
1242
+ case "url":
1243
+ return `https://example.com/${key}/${i + 1}`;
1244
+ case "uuid":
1245
+ return `00000000-0000-4000-8000-00000000000${i + 1}`;
1246
+ default:
1247
+ return `${titleCase(key)} ${i + 1}`;
1248
+ }
1249
+ }
1250
+ function titleCase(key) {
1251
+ const s = key.replace(/[_-]+/g, " ").trim();
1252
+ return s.charAt(0).toUpperCase() + s.slice(1);
1253
+ }
1254
+ function sampleValue(prop, key, i) {
1255
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
1256
+ return prop.enum[i % prop.enum.length];
1257
+ }
1258
+ switch (prop.type) {
1259
+ case "string":
1260
+ return sampleString(prop, key, i);
1261
+ case "number":
1262
+ return Math.round((i + 1) * 1.5 * 100) / 100;
1263
+ case "integer":
1264
+ return i + 1;
1265
+ case "boolean":
1266
+ return i % 2 === 0;
1267
+ case "array":
1268
+ return [];
1269
+ case "object":
1270
+ return {};
1271
+ default:
1272
+ return null;
1273
+ }
1274
+ }
1275
+ function sampleRows(schema, count = 3) {
1276
+ const entries = Object.entries(schema.properties);
1277
+ return Array.from({ length: count }, (_, i) => {
1278
+ const row = {};
1279
+ for (const [key, prop] of entries) {
1280
+ row[key] = sampleValue(prop, key, i);
1281
+ }
1282
+ return row;
1283
+ });
1284
+ }
1285
+
1286
+ // src/preview/TablePreview.tsx
1287
+ import { jsx as jsx3 } from "react/jsx-runtime";
1288
+ function TablePreview({
1289
+ schema,
1290
+ uiSchema
1291
+ }) {
1292
+ const data = useMemo2(() => toPageResult(sampleRows(schema)), [schema]);
1293
+ return /* @__PURE__ */ jsx3(
1294
+ EntityTable,
1295
+ {
1296
+ schema,
1297
+ uiSchema,
1298
+ data,
1299
+ isLoading: false,
1300
+ isError: false,
1301
+ entityLabel: "rows"
1302
+ }
1303
+ );
1304
+ }
1305
+
1306
+ // 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";
1310
+ import {
1311
+ Plus as Plus2,
1312
+ Trash2 as Trash22,
1313
+ ChevronUp as ChevronUp2,
1314
+ ChevronDown as ChevronDown2,
1315
+ GripVertical
1316
+ } from "lucide-react";
1317
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1318
+ var DRAG_MIME = "application/x-sonnet-property";
1319
+ function orderedKeysOf(properties, order) {
1320
+ const keys = Object.keys(properties);
1321
+ if (!order || order.length === 0) return keys;
1322
+ const inOrder = order.filter((k) => k in properties);
1323
+ return [...inOrder, ...keys.filter((k) => !inOrder.includes(k))];
1324
+ }
1325
+ function scopeTarget(schema, scopePath) {
1326
+ const prop = getProperty(schema, scopePath);
1327
+ if (!prop) return void 0;
1328
+ return prop.type === "array" ? prop.items : prop;
1329
+ }
1330
+ 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);
1334
+ return {
1335
+ keys: orderedKeysOf(container?.properties ?? {}, container?.["x-ui-order"]),
1336
+ rows: layout?.rows ?? [],
1337
+ layout,
1338
+ isArray
1339
+ };
1340
+ }
1341
+ function scopeIsArray(schema, scopePath) {
1342
+ if (scopePath.length === 0) return false;
1343
+ return getProperty(schema, scopePath)?.type === "array";
1344
+ }
1345
+ var SCALAR_META_SCHEMA = buildLayoutMetaSchema();
1346
+ var SCALAR_UI_SCHEMA = {
1347
+ "x-ui-layout": {
1348
+ labelPlacement: "start",
1349
+ // Flow row: controls sit left-to-right at their natural width (label hugs
1350
+ // control via horizontal labelPlacement), wrapping as needed.
1351
+ rows: [
1352
+ {
1353
+ fields: ["direction", "gap", "labels", "compact"],
1354
+ arrange: "flow",
1355
+ gap: 16
1356
+ }
1357
+ ],
1358
+ compact: true
1359
+ },
1360
+ // Per-field widths for the toolbar. Top-level fields take their per-field
1361
+ // uiSchema directly under the field name. Direction (a select) gets more
1362
+ // room; the numeric Gap input stays narrow.
1363
+ direction: { "x-ui-width": "lg" },
1364
+ gap: { "x-ui-width": "sm" }
1365
+ };
1366
+ function scalarsFrom(values) {
1367
+ const full = valuesToLayout(values, void 0) ?? {};
1368
+ return {
1369
+ direction: full.direction,
1370
+ gap: full.gap,
1371
+ labels: full.labels,
1372
+ compact: full.compact
1373
+ };
1374
+ }
1375
+ function ArrayItemLayoutBar({
1376
+ layout,
1377
+ readOnly,
1378
+ onChange
1379
+ }) {
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(
1383
+ JsonSchemaFormRenderer2,
1384
+ {
1385
+ schema: SCALAR_META_SCHEMA,
1386
+ uiSchema: SCALAR_UI_SCHEMA,
1387
+ data: layoutToValues(layout),
1388
+ onChange: (data) => onChange(scalarsFrom(data)),
1389
+ showActions: false,
1390
+ showExtraProperties: false,
1391
+ readOnly
1392
+ }
1393
+ )
1394
+ ] });
1395
+ }
1396
+ function FormLayoutDesigner({
1397
+ state
1398
+ }) {
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];
1403
+ const effectivePath = activeScope.path;
1404
+ const {
1405
+ keys: allKeys,
1406
+ rows,
1407
+ layout,
1408
+ isArray
1409
+ } = resolveScope(state.schema, state.uiSchema, effectivePath);
1410
+ const assigned = new Set(rows.flatMap((r) => r.fields));
1411
+ const available = allKeys.filter((k) => !assigned.has(k));
1412
+ const [dragKey, setDragKey] = useState3(null);
1413
+ const commit = (next) => state.setScopedLayoutRows(effectivePath, next);
1414
+ const place = (key, rowIndex) => {
1415
+ if (!key) return;
1416
+ const next = rows.map((r) => ({
1417
+ ...r,
1418
+ fields: r.fields.filter((f) => f !== key)
1419
+ }));
1420
+ if (rowIndex !== null && next[rowIndex]) {
1421
+ next[rowIndex] = {
1422
+ ...next[rowIndex],
1423
+ fields: [...next[rowIndex].fields, key]
1424
+ };
1425
+ }
1426
+ commit(next);
1427
+ };
1428
+ const addRow = () => commit([...rows, { fields: [] }]);
1429
+ const removeRow = (i) => commit(rows.filter((_, idx) => idx !== i));
1430
+ const moveRow = (i, dir) => {
1431
+ const j = i + dir;
1432
+ if (j < 0 || j >= rows.length) return;
1433
+ const next = [...rows];
1434
+ [next[i], next[j]] = [next[j], next[i]];
1435
+ commit(next);
1436
+ };
1437
+ const setColumns = (i, columns) => {
1438
+ const next = rows.map(
1439
+ (r, idx) => idx === i ? { ...r, columns: columns.trim() ? columns : void 0 } : r
1440
+ );
1441
+ commit(next);
1442
+ };
1443
+ const setGroup = (i, group) => {
1444
+ const next = rows.map(
1445
+ (r, idx) => idx === i ? { ...r, group: group.trim() ? group : void 0 } : r
1446
+ );
1447
+ commit(next);
1448
+ };
1449
+ const setArrange = (i, arrange) => {
1450
+ const next = rows.map(
1451
+ (r, idx) => (
1452
+ // "grid" is the default -- omit it to keep the config clean.
1453
+ idx === i ? { ...r, arrange: arrange === "flow" ? "flow" : void 0 } : r
1454
+ )
1455
+ );
1456
+ commit(next);
1457
+ };
1458
+ const setWidth = (i, cell, width) => {
1459
+ const next = rows.map((r, idx) => {
1460
+ if (idx !== i) return r;
1461
+ const widths = [...r.widths ?? []];
1462
+ while (widths.length < r.fields.length) widths.push("full");
1463
+ widths[cell] = width;
1464
+ const allFull = widths.every((w) => w === "full");
1465
+ return { ...r, widths: allFull ? void 0 : widths };
1466
+ });
1467
+ commit(next);
1468
+ };
1469
+ 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,
1475
+ {
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
1485
+ }
1486
+ ) })
1487
+ ] }),
1488
+ isArray && /* @__PURE__ */ jsx4(
1489
+ ArrayItemLayoutBar,
1490
+ {
1491
+ layout,
1492
+ readOnly,
1493
+ onChange: (opts) => state.setScopedLayoutOptions(effectivePath, opts)
1494
+ }
1495
+ ),
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(
1528
+ Button,
1529
+ {
1530
+ size: "sm",
1531
+ variant: "outline",
1532
+ className: "gap-1.5",
1533
+ onClick: addRow,
1534
+ children: [
1535
+ /* @__PURE__ */ jsx4(Plus2, { className: "h-4 w-4" }),
1536
+ "Add row"
1537
+ ]
1538
+ }
1539
+ ) })
1540
+ ] }),
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(
1542
+ RowCard,
1543
+ {
1544
+ row,
1545
+ index: i,
1546
+ total: rows.length,
1547
+ readOnly,
1548
+ dragActive: !!dragKey,
1549
+ onDropKey: (key) => place(key ?? dragKey ?? "", i),
1550
+ onRemoveKey: (key) => place(key, null),
1551
+ onDragStartKey: (key) => setDragKey(key),
1552
+ onDragEndKey: () => setDragKey(null),
1553
+ onMoveUp: () => moveRow(i, -1),
1554
+ onMoveDown: () => moveRow(i, 1),
1555
+ onDelete: () => removeRow(i),
1556
+ onColumns: (v) => setColumns(i, v),
1557
+ onGroup: (v) => setGroup(i, v),
1558
+ onWidth: (cell, w) => setWidth(i, cell, w),
1559
+ onArrange: (a) => setArrange(i, a)
1560
+ },
1561
+ i
1562
+ )) })
1563
+ ] })
1564
+ ] })
1565
+ ] });
1566
+ }
1567
+ var WIDTH_LABELS = {
1568
+ full: "Full",
1569
+ sm: "Small",
1570
+ md: "Medium",
1571
+ lg: "Large"
1572
+ };
1573
+ var WIDTH_SHORT = {
1574
+ full: "F",
1575
+ sm: "S",
1576
+ md: "M",
1577
+ lg: "L"
1578
+ };
1579
+ function Chip({
1580
+ label,
1581
+ readOnly,
1582
+ onRemove,
1583
+ onDragStart,
1584
+ onDragEnd,
1585
+ width,
1586
+ onWidth,
1587
+ widthDisabled
1588
+ }) {
1589
+ return /* @__PURE__ */ jsxs3(
1590
+ "div",
1591
+ {
1592
+ draggable: !readOnly,
1593
+ onDragStart: (e) => {
1594
+ e.dataTransfer.setData(DRAG_MIME, label);
1595
+ e.dataTransfer.effectAllowed = "move";
1596
+ onDragStart();
1597
+ },
1598
+ 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",
1600
+ 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(
1604
+ "select",
1605
+ {
1606
+ value: width ?? "full",
1607
+ onChange: (e) => onWidth(e.target.value),
1608
+ disabled: readOnly || widthDisabled,
1609
+ title: widthDisabled ? "Overridden by raw CSS columns" : "Column width",
1610
+ 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))
1612
+ }
1613
+ ),
1614
+ onRemove && !readOnly && /* @__PURE__ */ jsx4(IconButton2, { label: "Remove from row", onClick: onRemove, children: /* @__PURE__ */ jsx4(Trash22, {}) })
1615
+ ]
1616
+ }
1617
+ );
1618
+ }
1619
+ function RowCard({
1620
+ row,
1621
+ index,
1622
+ total,
1623
+ readOnly,
1624
+ dragActive,
1625
+ onDropKey,
1626
+ onRemoveKey,
1627
+ onDragStartKey,
1628
+ onDragEndKey,
1629
+ onMoveUp,
1630
+ onMoveDown,
1631
+ onDelete,
1632
+ onColumns,
1633
+ onGroup,
1634
+ onWidth,
1635
+ onArrange
1636
+ }) {
1637
+ const [over, setOver] = useState3(false);
1638
+ 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: [
1642
+ "Row ",
1643
+ index + 1
1644
+ ] }),
1645
+ !readOnly && /* @__PURE__ */ jsxs3("span", { className: "ml-auto flex gap-0.5", children: [
1646
+ /* @__PURE__ */ jsx4(
1647
+ IconButton2,
1648
+ {
1649
+ label: "Move row up",
1650
+ disabled: index === 0,
1651
+ onClick: onMoveUp,
1652
+ children: /* @__PURE__ */ jsx4(ChevronUp2, {})
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ jsx4(
1656
+ IconButton2,
1657
+ {
1658
+ label: "Move row down",
1659
+ disabled: index === total - 1,
1660
+ onClick: onMoveDown,
1661
+ children: /* @__PURE__ */ jsx4(ChevronDown2, {})
1662
+ }
1663
+ ),
1664
+ /* @__PURE__ */ jsx4(
1665
+ IconButton2,
1666
+ {
1667
+ label: "Delete row",
1668
+ tone: "destructive",
1669
+ onClick: onDelete,
1670
+ children: /* @__PURE__ */ jsx4(Trash22, {})
1671
+ }
1672
+ )
1673
+ ] })
1674
+ ] }),
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,
1679
+ {
1680
+ value: row.group ?? "",
1681
+ onChange: (e) => onGroup(e.target.value),
1682
+ placeholder: "Section title (optional)",
1683
+ disabled: readOnly,
1684
+ className: "h-7 text-sm flex-1"
1685
+ }
1686
+ )
1687
+ ] }),
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(
1691
+ "select",
1692
+ {
1693
+ value: isFlow ? "flow" : "grid",
1694
+ onChange: (e) => onArrange(e.target.value),
1695
+ disabled: readOnly,
1696
+ title: "Grid: sized columns. Inline: fields at natural width, wrapping.",
1697
+ className: "h-7 rounded-md border bg-background px-2 text-sm text-foreground disabled:opacity-50",
1698
+ children: [
1699
+ /* @__PURE__ */ jsx4("option", { value: "grid", children: "Grid (columns)" }),
1700
+ /* @__PURE__ */ jsx4("option", { value: "flow", children: "Inline (natural width)" })
1701
+ ]
1702
+ }
1703
+ )
1704
+ ] }),
1705
+ /* @__PURE__ */ jsx4(
1706
+ "div",
1707
+ {
1708
+ className: `min-h-10 rounded-md border border-dashed p-2 flex flex-wrap gap-1 ${over && dragActive ? "border-primary bg-accent" : "border-border"}`,
1709
+ onDragOver: (e) => {
1710
+ if (dragActive) {
1711
+ e.preventDefault();
1712
+ setOver(true);
1713
+ }
1714
+ },
1715
+ onDragLeave: () => setOver(false),
1716
+ onDrop: (e) => {
1717
+ setOver(false);
1718
+ onDropKey(e.dataTransfer.getData(DRAG_MIME) || null);
1719
+ },
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(
1721
+ Chip,
1722
+ {
1723
+ label: key,
1724
+ readOnly,
1725
+ onRemove: () => onRemoveKey(key),
1726
+ onDragStart: () => onDragStartKey(key),
1727
+ onDragEnd: onDragEndKey,
1728
+ width: row.widths?.[cell] ?? "full",
1729
+ onWidth: isFlow ? void 0 : (w) => onWidth(cell, w),
1730
+ widthDisabled: !!row.columns
1731
+ },
1732
+ key
1733
+ ))
1734
+ }
1735
+ ),
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,
1742
+ {
1743
+ value: row.columns ?? "",
1744
+ onChange: (e) => onColumns(e.target.value),
1745
+ placeholder: "e.g. 1fr 1fr",
1746
+ disabled: readOnly,
1747
+ className: "h-7 text-sm flex-1"
1748
+ }
1749
+ )
1750
+ ] }),
1751
+ /* @__PURE__ */ jsxs3("p", { className: "mt-1 pl-[4.5rem] text-xs text-muted-foreground", children: [
1752
+ "Raw ",
1753
+ /* @__PURE__ */ jsx4("span", { className: "font-mono", children: "grid-template-columns" }),
1754
+ ". When set, it overrides the per-property widths. Blank = use widths."
1755
+ ] })
1756
+ ] })
1757
+ ] });
1758
+ }
1759
+
1760
+ // src/preview/CapabilitiesPanel.tsx
1761
+ import { useEffect as useEffect2, useState as useState4 } from "react";
1762
+ import { Button as Button2 } from "@petrarca/sonnet-ui";
1763
+ 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
1768
+ }) {
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]);
1776
+ const handleApply = () => {
1777
+ let parsed;
1778
+ try {
1779
+ parsed = JSON.parse(text || "{}");
1780
+ } catch {
1781
+ setError("Invalid JSON.");
1782
+ return;
1783
+ }
1784
+ const result = validateCapabilities(parsed);
1785
+ if (!result.ok) {
1786
+ setError(result.errors.join("; "));
1787
+ return;
1788
+ }
1789
+ setError(null);
1790
+ onApply?.(result.value);
1791
+ };
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(
1796
+ Button2,
1797
+ {
1798
+ type: "button",
1799
+ size: "sm",
1800
+ onClick: handleApply,
1801
+ className: "ml-auto",
1802
+ children: "Apply"
1803
+ }
1804
+ )
1805
+ ] }),
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(
1808
+ JsonEditor,
1809
+ {
1810
+ value: text,
1811
+ editablePaths: ["/"],
1812
+ fillContainer: true,
1813
+ onChange: setText
1814
+ }
1815
+ ) })
1816
+ ] });
1817
+ }
1818
+
1819
+ // src/preview/PreviewPane.tsx
1820
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1821
+ function ReadonlyJson({ value }) {
1822
+ const [copied, setCopied] = useState5(false);
1823
+ const handleCopy = () => {
1824
+ void copyTextToClipboard(value).then(() => {
1825
+ setCopied(true);
1826
+ setTimeout(() => setCopied(false), 1500);
1827
+ });
1828
+ };
1829
+ return /* @__PURE__ */ jsxs5("div", { className: "absolute inset-0 rounded-md border overflow-hidden", children: [
1830
+ /* @__PURE__ */ jsxs5(
1831
+ Button3,
1832
+ {
1833
+ type: "button",
1834
+ variant: "outline",
1835
+ size: "sm",
1836
+ onClick: handleCopy,
1837
+ className: "absolute right-2 top-2 z-10 gap-1.5",
1838
+ children: [
1839
+ copied ? /* @__PURE__ */ jsx6(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx6(Copy, { className: "h-3.5 w-3.5" }),
1840
+ copied ? "Copied" : "Copy"
1841
+ ]
1842
+ }
1843
+ ),
1844
+ /* @__PURE__ */ jsx6(JsonEditor2, { value, editablePaths: [], fillContainer: true }, value)
1845
+ ] });
1846
+ }
1847
+ 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" })
1859
+ ] })
1860
+ ] });
1861
+ }
1862
+ function PreviewPane({
1863
+ state,
1864
+ capabilities,
1865
+ onCapabilitiesChange
1866
+ }) {
1867
+ const { schema, uiSchema } = state;
1868
+ const caps = state.capabilities;
1869
+ const flags = {
1870
+ isFull: caps.mode === "full",
1871
+ showForm: caps.previews.includes("form"),
1872
+ showTable: caps.previews.includes("table"),
1873
+ showJson: caps.previews.includes("json"),
1874
+ showCapabilities: caps.showCapabilities
1875
+ };
1876
+ const [data, setData] = useState5(
1877
+ () => extractSchemaDefaults(schema)
1878
+ );
1879
+ const schemaText = useMemo4(() => JSON.stringify(schema, null, 2), [schema]);
1880
+ const uiSchemaText = useMemo4(
1881
+ () => JSON.stringify(uiSchema ?? {}, null, 2),
1882
+ [uiSchema]
1883
+ );
1884
+ const embeddedText = useMemo4(
1885
+ () => JSON.stringify(embedUiSchema(schema, uiSchema ?? {}), null, 2),
1886
+ [schema, uiSchema]
1887
+ );
1888
+ 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(
1892
+ JsonSchemaFormRenderer3,
1893
+ {
1894
+ schema,
1895
+ data,
1896
+ uiSchema,
1897
+ onChange: setData,
1898
+ showActions: false,
1899
+ showExtraProperties: false
1900
+ }
1901
+ ) }),
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(
1905
+ SourceTabPanels,
1906
+ {
1907
+ flags,
1908
+ schemaText,
1909
+ uiSchemaText,
1910
+ embeddedText,
1911
+ capabilities,
1912
+ onCapabilitiesChange
1913
+ }
1914
+ )
1915
+ ] });
1916
+ }
1917
+ function SourceTabPanels({
1918
+ flags,
1919
+ schemaText,
1920
+ uiSchemaText,
1921
+ embeddedText,
1922
+ capabilities,
1923
+ onCapabilitiesChange
1924
+ }) {
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(
1930
+ CapabilitiesPanel,
1931
+ {
1932
+ capabilities,
1933
+ onApply: onCapabilitiesChange
1934
+ }
1935
+ ) })
1936
+ ] });
1937
+ }
1938
+
1939
+ // src/inspector/Inspector.tsx
1940
+ import { useEffect as useEffect4, useMemo as useMemo8, useRef as useRef4, useState as useState7 } from "react";
1941
+ import {
1942
+ Tabs as Tabs2,
1943
+ TabsList as TabsList2,
1944
+ TabsTrigger as TabsTrigger2,
1945
+ TabsContent as TabsContent2,
1946
+ Input as Input4,
1947
+ IconButton as IconButton4
1948
+ } from "@petrarca/sonnet-ui";
1949
+ import { Pencil, Check as Check2, X } from "lucide-react";
1950
+
1951
+ // src/inspector/DataTab.tsx
1952
+ import { useMemo as useMemo5, useRef as useRef3 } from "react";
1953
+ import { JsonSchemaFormRenderer as JsonSchemaFormRenderer4 } from "@petrarca/sonnet-forms";
1954
+
1955
+ // src/inspector/metaSchemas.ts
1956
+ var PROPERTY_TYPES = [
1957
+ "string",
1958
+ "number",
1959
+ "integer",
1960
+ "boolean",
1961
+ "object",
1962
+ "array"
1963
+ ];
1964
+ var STRING_FORMATS = [
1965
+ "",
1966
+ "email",
1967
+ "uri",
1968
+ "url",
1969
+ "date",
1970
+ "date-time",
1971
+ "time",
1972
+ "uuid"
1973
+ ];
1974
+ function validationProps(type) {
1975
+ switch (type) {
1976
+ case "string":
1977
+ return {
1978
+ format: { type: "string", title: "Format", enum: STRING_FORMATS },
1979
+ minLength: { type: "integer", title: "Min length", minimum: 0 },
1980
+ maxLength: { type: "integer", title: "Max length", minimum: 0 },
1981
+ pattern: { type: "string", title: "Pattern (regex)" }
1982
+ };
1983
+ case "number":
1984
+ case "integer":
1985
+ return {
1986
+ minimum: { type: "number", title: "Minimum" },
1987
+ maximum: { type: "number", title: "Maximum" }
1988
+ };
1989
+ case "array":
1990
+ return {
1991
+ minItems: { type: "integer", title: "Min items", minimum: 0 },
1992
+ maxItems: { type: "integer", title: "Max items", minimum: 0 }
1993
+ };
1994
+ default:
1995
+ return {};
1996
+ }
1997
+ }
1998
+ function buildDataMetaSchema(type) {
1999
+ const isContainer2 = type === "object" || type === "array";
2000
+ const properties = {
2001
+ type: { type: "string", title: "Type", enum: [...PROPERTY_TYPES] },
2002
+ title: { type: "string", title: "Label (title)" },
2003
+ description: {
2004
+ type: "string",
2005
+ title: "Description",
2006
+ "x-ui-widget": "textarea"
2007
+ },
2008
+ required: { type: "boolean", title: "Required" },
2009
+ ...isContainer2 ? {} : { default: { type: "string", title: "Default" } },
2010
+ ...validationProps(type)
2011
+ };
2012
+ return { title: "", type: "object", properties };
2013
+ }
2014
+ function propertyToDataValues(prop, required) {
2015
+ return {
2016
+ type: prop.type ?? "string",
2017
+ title: prop.title,
2018
+ description: prop.description,
2019
+ default: prop.default,
2020
+ 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
2029
+ };
2030
+ }
2031
+ function dataValuesToPatch(values) {
2032
+ const patch = {};
2033
+ const setStr = (k, v) => {
2034
+ if (typeof v === "string" && v.length > 0)
2035
+ patch[k] = v;
2036
+ else patch[k] = void 0;
2037
+ };
2038
+ const setNum = (k, v) => {
2039
+ if (typeof v === "number" && !Number.isNaN(v))
2040
+ patch[k] = v;
2041
+ else patch[k] = void 0;
2042
+ };
2043
+ setStr("title", values.title);
2044
+ setStr("description", values.description);
2045
+ setStr("default", values.default);
2046
+ setStr("format", values.format);
2047
+ setStr("pattern", values.pattern);
2048
+ setNum("minLength", values.minLength);
2049
+ setNum("maxLength", values.maxLength);
2050
+ setNum("minimum", values.minimum);
2051
+ setNum("maximum", values.maximum);
2052
+ setNum("minItems", values.minItems);
2053
+ setNum("maxItems", values.maxItems);
2054
+ return patch;
2055
+ }
2056
+
2057
+ // src/inspector/DataTab.tsx
2058
+ import { jsx as jsx7 } from "react/jsx-runtime";
2059
+ function DataTab({ node, state }) {
2060
+ const type = node.property.type ?? "string";
2061
+ const schema = useMemo5(() => buildDataMetaSchema(type), [type]);
2062
+ const values = useMemo5(
2063
+ () => propertyToDataValues(node.property, node.required),
2064
+ [node.property, node.required]
2065
+ );
2066
+ const prevRef = useRef3(values);
2067
+ prevRef.current = values;
2068
+ const handleChange = (data) => {
2069
+ const next = data;
2070
+ const prev = prevRef.current;
2071
+ if (next.type !== prev.type) {
2072
+ state.changePropertyType(
2073
+ node.path,
2074
+ next.type
2075
+ );
2076
+ return;
2077
+ }
2078
+ if (next.required !== prev.required) {
2079
+ state.setRequired(node.path, !!next.required);
2080
+ return;
2081
+ }
2082
+ state.updateProperty(node.path, dataValuesToPatch(next));
2083
+ };
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
+ );
2096
+ }
2097
+
2098
+ // src/inspector/UITab.tsx
2099
+ import { useMemo as useMemo6 } from "react";
2100
+ import { JsonSchemaFormRenderer as JsonSchemaFormRenderer5 } from "@petrarca/sonnet-forms";
2101
+ import { JsonEditor as JsonEditor3 } from "@petrarca/sonnet-ui/json-editor";
2102
+ import {
2103
+ WIDGET_OPTION_DEFS,
2104
+ optionDefsToSchema
2105
+ } 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";
2108
+ var PRESENTATION_META_SCHEMA = {
2109
+ title: "",
2110
+ type: "object",
2111
+ properties: {
2112
+ "x-ui-title": {
2113
+ type: "string",
2114
+ title: "Label override",
2115
+ description: "Overrides the property title in the form."
2116
+ },
2117
+ "x-ui-placeholder": { type: "string", title: "Placeholder" },
2118
+ "x-ui-description": {
2119
+ type: "string",
2120
+ title: "Help text",
2121
+ "x-ui-widget": "textarea"
2122
+ },
2123
+ "x-ui-width": {
2124
+ type: "string",
2125
+ title: "Field width",
2126
+ description: "Constrain the field's slot width in the form.",
2127
+ enum: ["full", "sm", "md", "lg"],
2128
+ "x-ui-options": {
2129
+ enumNames: {
2130
+ full: "Full width",
2131
+ sm: "Small",
2132
+ md: "Medium",
2133
+ lg: "Large"
2134
+ }
2135
+ }
2136
+ }
2137
+ }
2138
+ };
2139
+ function PresentationEditor({
2140
+ fieldName,
2141
+ uiEntry,
2142
+ readOnly,
2143
+ onChange
2144
+ }) {
2145
+ const values = {
2146
+ "x-ui-title": typeof uiEntry["x-ui-title"] === "string" ? uiEntry["x-ui-title"] : "",
2147
+ "x-ui-placeholder": uiEntry["x-ui-placeholder"] ?? "",
2148
+ "x-ui-description": uiEntry["x-ui-description"] ?? "",
2149
+ "x-ui-width": uiEntry["x-ui-width"] ?? "full"
2150
+ };
2151
+ const handleChange = (data) => {
2152
+ const next = { ...uiEntry };
2153
+ for (const key of [
2154
+ "x-ui-title",
2155
+ "x-ui-placeholder",
2156
+ "x-ui-description"
2157
+ ]) {
2158
+ const v = data[key];
2159
+ if (typeof v === "string" && v.length > 0) next[key] = v;
2160
+ else delete next[key];
2161
+ }
2162
+ const width = data["x-ui-width"];
2163
+ if (width === "sm" || width === "md" || width === "lg") {
2164
+ next["x-ui-width"] = width;
2165
+ } else {
2166
+ delete next["x-ui-width"];
2167
+ }
2168
+ onChange(next);
2169
+ };
2170
+ return /* @__PURE__ */ jsxs6("div", { children: [
2171
+ /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Presentation (form overrides)" }),
2172
+ /* @__PURE__ */ jsx8(
2173
+ JsonSchemaFormRenderer5,
2174
+ {
2175
+ schema: PRESENTATION_META_SCHEMA,
2176
+ data: values,
2177
+ onChange: handleChange,
2178
+ showActions: false,
2179
+ showExtraProperties: false,
2180
+ readOnly
2181
+ },
2182
+ `${fieldName}:text`
2183
+ )
2184
+ ] });
2185
+ }
2186
+ function OptionsEditor({
2187
+ widget,
2188
+ optionsSchema,
2189
+ currentOptions,
2190
+ readOnly,
2191
+ onChange
2192
+ }) {
2193
+ if (optionsSchema) {
2194
+ return /* @__PURE__ */ jsxs6("div", { children: [
2195
+ /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options" }),
2196
+ /* @__PURE__ */ jsx8(
2197
+ JsonSchemaFormRenderer5,
2198
+ {
2199
+ schema: optionsSchema,
2200
+ data: currentOptions,
2201
+ onChange,
2202
+ showActions: false,
2203
+ showExtraProperties: false,
2204
+ readOnly
2205
+ },
2206
+ widget
2207
+ )
2208
+ ] });
2209
+ }
2210
+ return /* @__PURE__ */ jsxs6("div", { children: [
2211
+ /* @__PURE__ */ jsx8("p", { className: "heading-meta mb-2", children: "Options (raw JSON)" }),
2212
+ /* @__PURE__ */ jsx8(
2213
+ JsonEditor3,
2214
+ {
2215
+ value: JSON.stringify(currentOptions ?? {}, null, 2),
2216
+ editablePaths: readOnly ? [] : ["/"],
2217
+ height: "180px",
2218
+ className: "rounded-md border overflow-hidden",
2219
+ onChange: (text) => {
2220
+ try {
2221
+ onChange(JSON.parse(text));
2222
+ } catch {
2223
+ }
2224
+ }
2225
+ }
2226
+ )
2227
+ ] });
2228
+ }
2229
+ function UITab({
2230
+ node,
2231
+ state,
2232
+ widgetOptions
2233
+ }) {
2234
+ const pathKey2 = node.path.join(".");
2235
+ const uiEntry = getUiEntryAtPath(state.uiSchema, node.path) ?? {};
2236
+ const currentWidget = typeof uiEntry["x-ui-widget"] === "string" ? uiEntry["x-ui-widget"] : null;
2237
+ const currentOptions = uiEntry["x-ui-options"] ?? {};
2238
+ const rootLayout = state.uiSchema["x-ui-layout"];
2239
+ const inheritedLabelPlacement = rootLayout?.labelPlacement ?? "top";
2240
+ const widgetChoices = useMemo6(
2241
+ () => compatibleWidgets(node.property),
2242
+ [node.property]
2243
+ );
2244
+ const optionsSchema = useMemo6(() => {
2245
+ if (!currentWidget) return null;
2246
+ const merged = { ...WIDGET_OPTION_DEFS, ...widgetOptions ?? {} };
2247
+ const descriptors = merged[currentWidget];
2248
+ return descriptors && descriptors.length ? optionDefsToSchema(descriptors) : null;
2249
+ }, [currentWidget, widgetOptions]);
2250
+ const setWidget = (value) => {
2251
+ const next = { ...uiEntry };
2252
+ if (value) next["x-ui-widget"] = value;
2253
+ else delete next["x-ui-widget"];
2254
+ state.setUiEntry(node.path, next);
2255
+ };
2256
+ const setOptions = (opts) => {
2257
+ const next = { ...uiEntry };
2258
+ const cleaned = Object.fromEntries(
2259
+ Object.entries(opts).filter(([, v]) => v !== void 0 && v !== "")
2260
+ );
2261
+ if (Object.keys(cleaned).length) next["x-ui-options"] = cleaned;
2262
+ else delete next["x-ui-options"];
2263
+ state.setUiEntry(node.path, next);
2264
+ };
2265
+ const setLabelPlacement = (value) => {
2266
+ const opts = { ...currentOptions };
2267
+ if (value === "top" || value === "start") {
2268
+ opts.labelPlacement = value;
2269
+ } else {
2270
+ delete opts.labelPlacement;
2271
+ }
2272
+ const next = { ...uiEntry };
2273
+ if (Object.keys(opts).length) next["x-ui-options"] = opts;
2274
+ else delete next["x-ui-options"];
2275
+ state.setUiEntry(node.path, next);
2276
+ };
2277
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
2278
+ /* @__PURE__ */ jsx8(
2279
+ FormSelect3,
2280
+ {
2281
+ id: `${pathKey2}-widget`,
2282
+ name: "widget",
2283
+ label: "Widget",
2284
+ value: currentWidget ?? "__auto__",
2285
+ options: widgetChoices.map((c) => ({
2286
+ value: c.value ?? "__auto__",
2287
+ label: c.label
2288
+ })),
2289
+ onChange: (v) => setWidget(v === "__auto__" ? null : v ?? null),
2290
+ disabled: state.readOnly,
2291
+ clearable: false
2292
+ }
2293
+ ),
2294
+ currentWidget && /* @__PURE__ */ jsx8(
2295
+ OptionsEditor,
2296
+ {
2297
+ widget: `${pathKey2}:${currentWidget}`,
2298
+ optionsSchema,
2299
+ currentOptions,
2300
+ readOnly: state.readOnly,
2301
+ onChange: setOptions
2302
+ }
2303
+ ),
2304
+ /* @__PURE__ */ jsx8(
2305
+ PresentationEditor,
2306
+ {
2307
+ fieldName: pathKey2,
2308
+ uiEntry,
2309
+ readOnly: state.readOnly,
2310
+ onChange: (entry) => state.setUiEntry(node.path, entry)
2311
+ }
2312
+ ),
2313
+ /* @__PURE__ */ jsx8(
2314
+ FormSelect3,
2315
+ {
2316
+ id: `${pathKey2}-labelPlacement`,
2317
+ name: "labelPlacement",
2318
+ label: "Label placement",
2319
+ value: currentOptions.labelPlacement ?? "__inherit__",
2320
+ options: [
2321
+ {
2322
+ value: "__inherit__",
2323
+ label: `Inherit (${inheritedLabelPlacement === "start" ? "inline" : "above"})`
2324
+ },
2325
+ { value: "top", label: "Above" },
2326
+ { value: "start", label: "Inline (start)" }
2327
+ ],
2328
+ onChange: (v) => setLabelPlacement(v ?? "__inherit__"),
2329
+ disabled: state.readOnly,
2330
+ clearable: false
2331
+ }
2332
+ )
2333
+ ] });
2334
+ }
2335
+
2336
+ // 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";
2339
+ import { Plus as Plus3, Trash2 as Trash23 } from "lucide-react";
2340
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2341
+ function EnumEditor({
2342
+ node,
2343
+ state
2344
+ }) {
2345
+ const [rows, setRows] = useState6(
2346
+ () => readEnumChoices(node.property, getUiEntryAtPath(state.uiSchema, node.path))
2347
+ );
2348
+ useEffect3(() => {
2349
+ setRows(
2350
+ readEnumChoices(
2351
+ node.property,
2352
+ getUiEntryAtPath(state.uiSchema, node.path)
2353
+ )
2354
+ );
2355
+ }, [node.path.join(".")]);
2356
+ const canEditLabels = true;
2357
+ const commit = (next) => {
2358
+ setRows(next);
2359
+ state.setEnum(node.path, next);
2360
+ };
2361
+ const updateRow = (i, patch) => {
2362
+ commit(rows.map((r, idx) => idx === i ? { ...r, ...patch } : r));
2363
+ };
2364
+ const removeRow = (i) => commit(rows.filter((_, idx) => idx !== i));
2365
+ 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." })
2370
+ ] }),
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" })
2376
+ ] }),
2377
+ rows.map((row, i) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
2378
+ /* @__PURE__ */ jsx9(
2379
+ Input3,
2380
+ {
2381
+ value: row.value,
2382
+ onChange: (e) => updateRow(i, { value: e.target.value }),
2383
+ placeholder: "value",
2384
+ disabled: state.readOnly,
2385
+ className: "h-8 text-sm flex-1"
2386
+ }
2387
+ ),
2388
+ canEditLabels && /* @__PURE__ */ jsx9(
2389
+ Input3,
2390
+ {
2391
+ value: row.label ?? "",
2392
+ onChange: (e) => updateRow(i, { label: e.target.value }),
2393
+ placeholder: row.value || "label",
2394
+ disabled: state.readOnly,
2395
+ className: "h-8 text-sm flex-1"
2396
+ }
2397
+ ),
2398
+ !state.readOnly && /* @__PURE__ */ jsx9(
2399
+ IconButton3,
2400
+ {
2401
+ label: "Remove value",
2402
+ tone: "destructive",
2403
+ onClick: () => removeRow(i),
2404
+ children: /* @__PURE__ */ jsx9(Trash23, {})
2405
+ }
2406
+ )
2407
+ ] }, i))
2408
+ ] }),
2409
+ !state.readOnly && /* @__PURE__ */ jsxs7(
2410
+ Button4,
2411
+ {
2412
+ variant: "outline",
2413
+ size: "sm",
2414
+ className: "gap-1.5",
2415
+ onClick: addRow,
2416
+ children: [
2417
+ /* @__PURE__ */ jsx9(Plus3, { className: "h-4 w-4" }),
2418
+ "Add value"
2419
+ ]
2420
+ }
2421
+ )
2422
+ ] });
2423
+ }
2424
+
2425
+ // src/inspector/TableHintsTab.tsx
2426
+ import { useMemo as useMemo7 } from "react";
2427
+ 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";
2431
+ var CELL_NAMES = Object.keys(CELL_RENDERERS);
2432
+ var COLUMN_META_SCHEMA = {
2433
+ title: "",
2434
+ type: "object",
2435
+ properties: {
2436
+ hidden: { type: "boolean", title: "Hide column" },
2437
+ order: { type: "integer", title: "Column order" },
2438
+ width: { type: "integer", title: "Width (px)", minimum: 0 },
2439
+ minWidth: { type: "integer", title: "Min width (px)", minimum: 0 }
2440
+ }
2441
+ };
2442
+ function TableHintsTab({
2443
+ node,
2444
+ state
2445
+ }) {
2446
+ const hints = readTableHints(node.property);
2447
+ const cellOptions = useMemo7(
2448
+ () => JSON.stringify(hints.cellOptions ?? {}, null, 2),
2449
+ [hints.cellOptions]
2450
+ );
2451
+ const commit = (patch) => state.setTableHints(node.path, { ...hints, ...patch });
2452
+ const columnValues = {
2453
+ hidden: hints.column?.hidden,
2454
+ order: hints.column?.order,
2455
+ width: hints.column?.width,
2456
+ minWidth: hints.column?.minWidth
2457
+ };
2458
+ const onColumnChange = (data) => {
2459
+ const v = data;
2460
+ const column = {};
2461
+ if (typeof v.hidden === "boolean") column.hidden = v.hidden;
2462
+ if (typeof v.order === "number" && !Number.isNaN(v.order))
2463
+ column.order = v.order;
2464
+ if (typeof v.width === "number" && !Number.isNaN(v.width))
2465
+ column.width = v.width;
2466
+ if (typeof v.minWidth === "number" && !Number.isNaN(v.minWidth))
2467
+ column.minWidth = v.minWidth;
2468
+ commit({ column });
2469
+ };
2470
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-4", children: [
2471
+ /* @__PURE__ */ jsx10(
2472
+ FormSelect4,
2473
+ {
2474
+ id: `${node.key}-cell`,
2475
+ name: "cell",
2476
+ label: "Cell renderer",
2477
+ value: hints.cell ?? "__auto__",
2478
+ options: [
2479
+ { value: "__auto__", label: "Auto (infer from type)" },
2480
+ ...CELL_NAMES.map((n) => ({ value: n, label: n }))
2481
+ ],
2482
+ onChange: (v) => commit({ cell: !v || v === "__auto__" ? void 0 : v }),
2483
+ disabled: state.readOnly,
2484
+ clearable: false
2485
+ }
2486
+ ),
2487
+ /* @__PURE__ */ jsxs8("div", { children: [
2488
+ /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Column" }),
2489
+ /* @__PURE__ */ jsx10(
2490
+ JsonSchemaFormRenderer6,
2491
+ {
2492
+ schema: COLUMN_META_SCHEMA,
2493
+ data: columnValues,
2494
+ onChange: onColumnChange,
2495
+ showActions: false,
2496
+ showExtraProperties: false,
2497
+ readOnly: state.readOnly
2498
+ },
2499
+ node.path.join(".")
2500
+ )
2501
+ ] }),
2502
+ /* @__PURE__ */ jsxs8("div", { children: [
2503
+ /* @__PURE__ */ jsx10("p", { className: "heading-meta mb-2", children: "Cell options (JSON)" }),
2504
+ /* @__PURE__ */ jsx10(
2505
+ JsonEditor4,
2506
+ {
2507
+ value: cellOptions,
2508
+ editablePaths: state.readOnly ? [] : ["/"],
2509
+ height: "160px",
2510
+ className: "rounded-md border overflow-hidden",
2511
+ onChange: (text) => {
2512
+ try {
2513
+ commit({
2514
+ cellOptions: JSON.parse(text)
2515
+ });
2516
+ } catch {
2517
+ }
2518
+ }
2519
+ }
2520
+ )
2521
+ ] })
2522
+ ] });
2523
+ }
2524
+
2525
+ // src/inspector/Inspector.tsx
2526
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2527
+ var ENUM_CAPABLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer"]);
2528
+ function KeyField({
2529
+ fieldKey,
2530
+ onRename,
2531
+ readOnly
2532
+ }) {
2533
+ const [editing, setEditing] = useState7(false);
2534
+ const [value, setValue] = useState7(fieldKey);
2535
+ const ref = useRef4(null);
2536
+ useEffect4(() => {
2537
+ setValue(fieldKey);
2538
+ setEditing(false);
2539
+ }, [fieldKey]);
2540
+ useEffect4(() => {
2541
+ if (editing) {
2542
+ ref.current?.focus();
2543
+ ref.current?.select();
2544
+ }
2545
+ }, [editing]);
2546
+ const commit = () => {
2547
+ const trimmed = value.trim();
2548
+ if (trimmed && trimmed !== fieldKey) onRename(trimmed);
2549
+ setEditing(false);
2550
+ };
2551
+ const cancel = () => {
2552
+ setValue(fieldKey);
2553
+ setEditing(false);
2554
+ };
2555
+ 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, {}) })
2559
+ ] });
2560
+ }
2561
+ return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1", children: [
2562
+ /* @__PURE__ */ jsx11(
2563
+ Input4,
2564
+ {
2565
+ ref,
2566
+ value,
2567
+ onChange: (e) => setValue(e.target.value),
2568
+ onKeyDown: (e) => {
2569
+ if (e.key === "Enter") {
2570
+ e.preventDefault();
2571
+ commit();
2572
+ } else if (e.key === "Escape") {
2573
+ e.preventDefault();
2574
+ cancel();
2575
+ }
2576
+ },
2577
+ className: "h-7 text-sm font-medium"
2578
+ }
2579
+ ),
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, {}) })
2582
+ ] });
2583
+ }
2584
+ function findNode(nodes, path) {
2585
+ for (const node of nodes) {
2586
+ if (samePath(node.path, path)) return node;
2587
+ const inChild = findNode(node.children, path);
2588
+ if (inChild) return inChild;
2589
+ }
2590
+ return void 0;
2591
+ }
2592
+ function Inspector({
2593
+ state,
2594
+ widgetOptions
2595
+ }) {
2596
+ const tree = useMemo8(() => buildPropertyTree(state.schema), [state.schema]);
2597
+ const node = state.selection ? findNode(tree, state.selection) : void 0;
2598
+ if (!node) {
2599
+ return /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground", children: "Select a property to edit." });
2600
+ }
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}`)
2627
+ ] });
2628
+ }
2629
+
2630
+ // src/SchemaDesigner.tsx
2631
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2632
+ function SchemaDesigner(props) {
2633
+ const [capabilities, setCapabilities] = useState8(props.capabilities);
2634
+ useEffect5(() => {
2635
+ setCapabilities(props.capabilities);
2636
+ }, [props.capabilities]);
2637
+ const state = useDesignerState({ ...props, capabilities });
2638
+ const {
2639
+ panelWidth: inspectorWidth,
2640
+ handlePointerDown: startInspectorResize,
2641
+ handleDoubleClick: resetInspectorWidth,
2642
+ separatorProps: inspectorSepProps
2643
+ } = useResizablePanel({
2644
+ defaultWidth: 360,
2645
+ minWidth: 300,
2646
+ maxWidth: 640,
2647
+ direction: "left-edge"
2648
+ });
2649
+ const {
2650
+ panelWidth: treeWidth,
2651
+ handlePointerDown: startTreeResize,
2652
+ handleDoubleClick: resetTreeWidth,
2653
+ separatorProps: treeSepProps
2654
+ } = useResizablePanel({
2655
+ defaultWidth: 288,
2656
+ minWidth: 220,
2657
+ maxWidth: 520,
2658
+ direction: "right-edge"
2659
+ });
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(
2666
+ "div",
2667
+ {
2668
+ 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(
2671
+ PropertyTree,
2672
+ {
2673
+ state,
2674
+ onCollapse: () => setTreeCollapsed(true)
2675
+ }
2676
+ )
2677
+ }
2678
+ ),
2679
+ state.capabilities.showPreview && /* @__PURE__ */ jsx12(
2680
+ ResizeHandle,
2681
+ {
2682
+ orientation: "vertical",
2683
+ ...treeSepProps,
2684
+ onPointerDown: startTreeResize,
2685
+ onDoubleClick: resetTreeWidth
2686
+ }
2687
+ )
2688
+ ] }),
2689
+ treeCollapsed && /* @__PURE__ */ jsx12("div", { className: "shrink-0 border-r flex flex-col items-center gap-1 p-1", children: /* @__PURE__ */ jsx12(
2690
+ IconButton5,
2691
+ {
2692
+ label: "Expand properties",
2693
+ onClick: () => setTreeCollapsed(false),
2694
+ children: /* @__PURE__ */ jsx12(PanelLeftOpen, {})
2695
+ }
2696
+ ) }),
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(
2698
+ PreviewPane,
2699
+ {
2700
+ state,
2701
+ capabilities,
2702
+ onCapabilitiesChange: setCapabilities
2703
+ }
2704
+ ) }),
2705
+ !inspectorCollapsed && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2706
+ /* @__PURE__ */ jsx12(
2707
+ ResizeHandle,
2708
+ {
2709
+ orientation: "vertical",
2710
+ ...inspectorSepProps,
2711
+ onPointerDown: startInspectorResize,
2712
+ onDoubleClick: resetInspectorWidth
2713
+ }
2714
+ ),
2715
+ /* @__PURE__ */ jsxs10(
2716
+ "div",
2717
+ {
2718
+ style: { width: inspectorWidth },
2719
+ className: "shrink-0 border-l overflow-auto p-4 bg-card",
2720
+ 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(
2725
+ IconButton5,
2726
+ {
2727
+ label: "Form settings",
2728
+ onClick: () => setSettingsOpen(true),
2729
+ children: /* @__PURE__ */ jsx12(Settings, {})
2730
+ }
2731
+ ),
2732
+ /* @__PURE__ */ jsx12(
2733
+ IconButton5,
2734
+ {
2735
+ label: "Collapse inspector",
2736
+ onClick: () => setInspectorCollapsed(true),
2737
+ children: /* @__PURE__ */ jsx12(PanelRightClose, {})
2738
+ }
2739
+ )
2740
+ ] })
2741
+ ] }),
2742
+ /* @__PURE__ */ jsx12(Inspector, { state, widgetOptions: props.widgetOptions })
2743
+ ]
2744
+ }
2745
+ )
2746
+ ] }),
2747
+ inspectorCollapsed && /* @__PURE__ */ jsxs10("div", { className: "shrink-0 border-l flex flex-col items-center gap-1 p-1", children: [
2748
+ /* @__PURE__ */ jsx12(
2749
+ IconButton5,
2750
+ {
2751
+ label: "Expand inspector",
2752
+ onClick: () => setInspectorCollapsed(false),
2753
+ children: /* @__PURE__ */ jsx12(PanelRightOpen, {})
2754
+ }
2755
+ ),
2756
+ /* @__PURE__ */ jsx12(
2757
+ IconButton5,
2758
+ {
2759
+ label: "Form settings",
2760
+ onClick: () => setSettingsOpen(true),
2761
+ children: /* @__PURE__ */ jsx12(Settings, {})
2762
+ }
2763
+ )
2764
+ ] }),
2765
+ /* @__PURE__ */ jsx12(
2766
+ FormSettingsSheet,
2767
+ {
2768
+ open: settingsOpen,
2769
+ onOpenChange: setSettingsOpen,
2770
+ state
2771
+ }
2772
+ )
2773
+ ] });
2774
+ }
2775
+ export {
2776
+ DataTab,
2777
+ EnumEditor,
2778
+ FormLayoutDesigner,
2779
+ FormSettingsSheet,
2780
+ Inspector,
2781
+ PreviewPane,
2782
+ PropertyTree,
2783
+ SchemaDesigner,
2784
+ TableHintsTab,
2785
+ TablePreview,
2786
+ UITab,
2787
+ addProperty,
2788
+ buildPropertyTree,
2789
+ capabilitiesSchema,
2790
+ changePropertyType,
2791
+ childContainer,
2792
+ childrenOf,
2793
+ compatibleWidgets,
2794
+ deleteProperty,
2795
+ embedUiSchema,
2796
+ getProperty,
2797
+ getScopedLayout,
2798
+ getUiEntryAtPath,
2799
+ isContainer,
2800
+ layoutScopes,
2801
+ moveProperty,
2802
+ newProperty,
2803
+ orderedKeys,
2804
+ pathKey,
2805
+ propertyKind,
2806
+ readEnumChoices,
2807
+ readTableHints,
2808
+ renameProperty,
2809
+ resolveCapabilities,
2810
+ samePath,
2811
+ sampleRows,
2812
+ setEnum,
2813
+ setGlobalOptions,
2814
+ setPropertyLayout,
2815
+ setRequired,
2816
+ setRootLayout,
2817
+ setRootLayoutRows,
2818
+ setScopedLayoutOptions,
2819
+ setScopedLayoutRows,
2820
+ setTableHints,
2821
+ setUiEntry,
2822
+ updateProperty,
2823
+ useDesignerState,
2824
+ validateCapabilities
2825
+ };
2826
+ //# sourceMappingURL=index.js.map