@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.d.ts +531 -0
- package/dist/index.js +2826 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
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
|