@jxsuite/studio 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/studio.js +4248 -3412
  2. package/dist/studio.js.map +49 -43
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +3 -2
  7. package/src/canvas/canvas-render.js +152 -20
  8. package/src/canvas/canvas-utils.js +21 -23
  9. package/src/editor/component-inline-edit.js +54 -41
  10. package/src/editor/content-inline-edit.js +46 -47
  11. package/src/editor/context-menu.js +63 -39
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -10
  14. package/src/editor/shortcuts.js +69 -39
  15. package/src/files/components.js +15 -4
  16. package/src/files/files.js +72 -24
  17. package/src/panels/activity-bar.js +29 -7
  18. package/src/panels/block-action-bar.js +104 -80
  19. package/src/panels/dnd.js +32 -28
  20. package/src/panels/editors.js +7 -14
  21. package/src/panels/elements-panel.js +9 -3
  22. package/src/panels/events-panel.js +44 -39
  23. package/src/panels/git-panel.js +45 -3
  24. package/src/panels/layers-panel.js +16 -11
  25. package/src/panels/left-panel.js +108 -43
  26. package/src/panels/overlays.js +91 -41
  27. package/src/panels/panel-events.js +9 -12
  28. package/src/panels/properties-panel.js +179 -104
  29. package/src/panels/right-panel.js +85 -37
  30. package/src/panels/shared.js +0 -22
  31. package/src/panels/signals-panel.js +125 -54
  32. package/src/panels/statusbar.js +23 -8
  33. package/src/panels/style-inputs.js +4 -2
  34. package/src/panels/style-panel.js +128 -105
  35. package/src/panels/stylebook-panel.js +11 -15
  36. package/src/panels/tab-strip.js +124 -0
  37. package/src/panels/toolbar.js +39 -9
  38. package/src/reactivity.js +28 -0
  39. package/src/settings/content-types-editor.js +56 -7
  40. package/src/settings/defs-editor.js +56 -7
  41. package/src/settings/schema-field-ui.js +60 -25
  42. package/src/state.js +0 -456
  43. package/src/store.js +97 -124
  44. package/src/studio.js +112 -121
  45. package/src/tabs/tab.js +153 -0
  46. package/src/tabs/transact.js +406 -0
  47. package/src/workspace/workspace.js +89 -0
@@ -4,7 +4,10 @@
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
- import { getState, update, updateSession, updateUi, undo, redo, subscribe } from "../store.js";
7
+ import { updateSession, updateUi } from "../store.js";
8
+ import { undo as tabUndo, redo as tabRedo } from "../tabs/transact.js";
9
+ import { effect, effectScope } from "../reactivity.js";
10
+ import { activeTab } from "../workspace/workspace.js";
8
11
  import { getEffectiveMedia } from "../site-context.js";
9
12
  import { mediaDisplayName } from "./shared.js";
10
13
  import { view } from "../view.js";
@@ -15,8 +18,8 @@ let _rootEl = null;
15
18
  /** @type {any} */
16
19
  let _ctx = null;
17
20
 
18
- /** @type {(() => void) | null} */
19
- let _unsub = null;
21
+ /** @type {import("@vue/reactivity").EffectScope | null} */
22
+ let _scope = null;
20
23
 
21
24
  const toolbarIconMap = /** @type {Record<string, any>} */ ({
22
25
  "sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
@@ -59,12 +62,28 @@ function tbBtnTpl(label, onClick, iconTag) {
59
62
  export function mount(rootEl, ctx) {
60
63
  _rootEl = rootEl;
61
64
  _ctx = ctx;
62
- _unsub = subscribe(() => render());
65
+ _scope = effectScope();
66
+ _scope.run(() => {
67
+ effect(() => {
68
+ const tab = activeTab.value;
69
+ if (!tab) return;
70
+ // Read reactive properties to establish tracking
71
+ void tab.doc.document;
72
+ void tab.doc.dirty;
73
+ void tab.doc.mode;
74
+ void tab.session.selection;
75
+ void tab.session.ui.editingFunction;
76
+ void tab.session.ui.featureToggles;
77
+ void tab.session.ui.leftTab;
78
+ void tab.session.ui.rightTab;
79
+ render();
80
+ });
81
+ });
63
82
  }
64
83
 
65
84
  export function unmount() {
66
- _unsub?.();
67
- _unsub = null;
85
+ _scope?.stop();
86
+ _scope = null;
68
87
  _rootEl = null;
69
88
  _ctx = null;
70
89
  }
@@ -79,7 +98,18 @@ export function render() {
79
98
  }
80
99
 
81
100
  function toolbarTemplate() {
82
- const S = getState();
101
+ const tab = activeTab.value;
102
+ if (!tab) return html``;
103
+ const S = /** @type {any} */ ({
104
+ document: tab.doc.document,
105
+ ui: tab.session.ui,
106
+ mode: tab.doc.mode,
107
+ selection: tab.session.selection,
108
+ dirty: tab.doc.dirty,
109
+ documentPath: tab.documentPath,
110
+ fileHandle: tab.fileHandle,
111
+ documentStack: tab.session.documentStack,
112
+ });
83
113
  const canvasMode = _ctx.getCanvasMode();
84
114
  const hasStack = S.documentStack && S.documentStack.length > 0;
85
115
  const hasFunc = !!S.ui.editingFunction;
@@ -203,8 +233,8 @@ function toolbarTemplate() {
203
233
  ${tbBtnTpl("Save", _ctx.saveFile, "sp-icon-save-floppy")}
204
234
  </sp-action-group>
205
235
  <sp-action-group compact size="s">
206
- ${tbBtnTpl("Undo", () => update(undo(getState())), "sp-icon-undo")}
207
- ${tbBtnTpl("Redo", () => update(redo(getState())), "sp-icon-redo")}
236
+ ${tbBtnTpl("Undo", () => tabUndo(activeTab.value), "sp-icon-undo")}
237
+ ${tbBtnTpl("Redo", () => tabRedo(activeTab.value), "sp-icon-redo")}
208
238
  </sp-action-group>
209
239
  <div class="tb-spacer"></div>
210
240
  ${S.documentPath
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Re-exports from @vue/reactivity. All studio code imports reactivity primitives from here (not
3
+ * directly from the package) for grep-ability and potential future wrapping.
4
+ *
5
+ * Version is pinned to match @jxsuite/runtime — reactive proxies from one version don't track in
6
+ * effects from another. See migrate-reactivity-workspace.md for rationale.
7
+ *
8
+ * Batching note: Vue's reactivity batches synchronous effect re-runs within a microtask. Use
9
+ * queueMicrotask() or Promise.resolve().then() to wait for the flush if needed (@vue/reactivity
10
+ * standalone doesn't ship nextTick).
11
+ */
12
+ export {
13
+ reactive,
14
+ ref,
15
+ computed,
16
+ readonly,
17
+ shallowReactive,
18
+ shallowRef,
19
+ effect,
20
+ effectScope,
21
+ getCurrentScope,
22
+ onScopeDispose,
23
+ pauseTracking,
24
+ resetTracking,
25
+ toRaw,
26
+ isReactive,
27
+ isRef,
28
+ } from "@vue/reactivity";
@@ -8,14 +8,19 @@
8
8
  import { html, render as litRender } from "lit-html";
9
9
  import { getPlatform } from "../platform.js";
10
10
  import { projectState } from "../store.js";
11
- import { fieldCardTpl, addFieldFormTpl, schemaForType } from "./schema-field-ui.js";
11
+ import {
12
+ fieldCardTpl,
13
+ addFieldFormTpl,
14
+ schemaForType,
15
+ detectFieldFormat,
16
+ } from "./schema-field-ui.js";
12
17
 
13
18
  // ─── Module state ─────────────────────────────────────────────────────────────
14
19
 
15
20
  /** @type {string | null} */
16
21
  let selectedContentType = null;
17
22
  let showAddField = false;
18
- let newFieldState = { name: "", type: "string", required: false };
23
+ let newFieldState = { name: "", type: "string", format: "", required: false };
19
24
  let showNewContentType = false;
20
25
  let newContentTypeName = "";
21
26
 
@@ -76,7 +81,7 @@ function handleAddField(rerender) {
76
81
  if (!schema) return;
77
82
 
78
83
  if (!schema.properties) schema.properties = {};
79
- schema.properties[name] = schemaForType(newFieldState.type);
84
+ schema.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
80
85
 
81
86
  if (newFieldState.required) {
82
87
  if (!schema.required) schema.required = [];
@@ -84,7 +89,7 @@ function handleAddField(rerender) {
84
89
  }
85
90
 
86
91
  showAddField = false;
87
- newFieldState = { name: "", type: "string", required: false };
92
+ newFieldState = { name: "", type: "string", format: "", required: false };
88
93
  rerender();
89
94
  saveProjectConfig();
90
95
  }
@@ -158,7 +163,27 @@ function handleChangeType(fieldName, newType, rerender) {
158
163
  const schema = getSelectedSchema();
159
164
  if (!schema?.properties?.[fieldName]) return;
160
165
 
161
- schema.properties[fieldName] = schemaForType(newType);
166
+ const oldFormat =
167
+ newType === "string" || newType === "array"
168
+ ? detectFieldFormat(schema.properties[fieldName])
169
+ : undefined;
170
+ schema.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
171
+ rerender();
172
+ saveProjectConfig();
173
+ }
174
+
175
+ /**
176
+ * @param {string} fieldName
177
+ * @param {string} format
178
+ * @param {() => void} rerender
179
+ */
180
+ function handleChangeFormat(fieldName, format, rerender) {
181
+ const schema = getSelectedSchema();
182
+ if (!schema?.properties?.[fieldName]) return;
183
+
184
+ const prop = schema.properties[fieldName];
185
+ const type = prop.type || "string";
186
+ schema.properties[fieldName] = schemaForType(type, format || undefined);
162
187
  rerender();
163
188
  saveProjectConfig();
164
189
  }
@@ -278,7 +303,29 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
278
303
  const parent = schema?.properties?.[parentName];
279
304
  if (!parent?.properties?.[childName]) return;
280
305
 
281
- parent.properties[childName] = schemaForType(newType);
306
+ const oldFormat =
307
+ newType === "string" || newType === "array"
308
+ ? detectFieldFormat(parent.properties[childName])
309
+ : undefined;
310
+ parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
311
+ rerender();
312
+ saveProjectConfig();
313
+ }
314
+
315
+ /**
316
+ * @param {string} parentName
317
+ * @param {string} childName
318
+ * @param {string} format
319
+ * @param {() => void} rerender
320
+ */
321
+ function handleChangeNestedFormat(parentName, childName, format, rerender) {
322
+ const schema = getSelectedSchema();
323
+ const parent = schema?.properties?.[parentName];
324
+ if (!parent?.properties?.[childName]) return;
325
+
326
+ const prop = parent.properties[childName];
327
+ const type = prop.type || "string";
328
+ parent.properties[childName] = schemaForType(type, format || undefined);
282
329
  rerender();
283
330
  saveProjectConfig();
284
331
  }
@@ -381,12 +428,14 @@ export function renderContentTypesEditor(container) {
381
428
  onToggleRequired: (n) => handleToggleRequired(n, rerender),
382
429
  onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
383
430
  onChangeType: (n, t) => handleChangeType(n, t, rerender),
431
+ onChangeFormat: (n, f) => handleChangeFormat(n, f, rerender),
384
432
  onChangeRefTarget: (n, target) => handleChangeRefTarget(n, target, rerender),
385
433
  onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
386
434
  onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
387
435
  onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
388
436
  onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
389
437
  onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
438
+ onChangeNestedFormat: (p, c, f) => handleChangeNestedFormat(p, c, f, rerender),
390
439
  };
391
440
 
392
441
  const fieldCards = Object.entries(properties).map(([name, def]) =>
@@ -423,7 +472,7 @@ export function renderContentTypesEditor(container) {
423
472
  onConfirm: () => handleAddField(rerender),
424
473
  onCancel: () => {
425
474
  showAddField = false;
426
- newFieldState = { name: "", type: "string", required: false };
475
+ newFieldState = { name: "", type: "string", format: "", required: false };
427
476
  rerender();
428
477
  },
429
478
  })
@@ -9,14 +9,19 @@
9
9
  import { html, render as litRender } from "lit-html";
10
10
  import { getPlatform } from "../platform.js";
11
11
  import { projectState } from "../store.js";
12
- import { fieldCardTpl, addFieldFormTpl, schemaForType } from "./schema-field-ui.js";
12
+ import {
13
+ fieldCardTpl,
14
+ addFieldFormTpl,
15
+ schemaForType,
16
+ detectFieldFormat,
17
+ } from "./schema-field-ui.js";
13
18
 
14
19
  // ─── Module state ─────────────────────────────────────────────────────────────
15
20
 
16
21
  /** @type {string | null} */
17
22
  let selectedDef = null;
18
23
  let showAddField = false;
19
- let newFieldState = { name: "", type: "string", required: false };
24
+ let newFieldState = { name: "", type: "string", format: "", required: false };
20
25
  let showNewDef = false;
21
26
  let newDefName = "";
22
27
 
@@ -70,7 +75,7 @@ function handleAddField(rerender) {
70
75
  if (!def) return;
71
76
 
72
77
  if (!def.properties) def.properties = {};
73
- def.properties[name] = schemaForType(newFieldState.type);
78
+ def.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
74
79
 
75
80
  if (newFieldState.required) {
76
81
  if (!def.required) def.required = [];
@@ -78,7 +83,7 @@ function handleAddField(rerender) {
78
83
  }
79
84
 
80
85
  showAddField = false;
81
- newFieldState = { name: "", type: "string", required: false };
86
+ newFieldState = { name: "", type: "string", format: "", required: false };
82
87
  rerender();
83
88
  saveProjectConfig();
84
89
  }
@@ -150,7 +155,27 @@ function handleChangeType(fieldName, newType, rerender) {
150
155
  const def = getSelectedDef();
151
156
  if (!def?.properties?.[fieldName]) return;
152
157
 
153
- def.properties[fieldName] = schemaForType(newType);
158
+ const oldFormat =
159
+ newType === "string" || newType === "array"
160
+ ? detectFieldFormat(def.properties[fieldName])
161
+ : undefined;
162
+ def.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
163
+ rerender();
164
+ saveProjectConfig();
165
+ }
166
+
167
+ /**
168
+ * @param {string} fieldName
169
+ * @param {string} format
170
+ * @param {() => void} rerender
171
+ */
172
+ function handleChangeFormat(fieldName, format, rerender) {
173
+ const def = getSelectedDef();
174
+ if (!def?.properties?.[fieldName]) return;
175
+
176
+ const prop = def.properties[fieldName];
177
+ const type = prop.type || "string";
178
+ def.properties[fieldName] = schemaForType(type, format || undefined);
154
179
  rerender();
155
180
  saveProjectConfig();
156
181
  }
@@ -256,7 +281,29 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
256
281
  const parent = def?.properties?.[parentName];
257
282
  if (!parent?.properties?.[childName]) return;
258
283
 
259
- parent.properties[childName] = schemaForType(newType);
284
+ const oldFormat =
285
+ newType === "string" || newType === "array"
286
+ ? detectFieldFormat(parent.properties[childName])
287
+ : undefined;
288
+ parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
289
+ rerender();
290
+ saveProjectConfig();
291
+ }
292
+
293
+ /**
294
+ * @param {string} parentName
295
+ * @param {string} childName
296
+ * @param {string} format
297
+ * @param {() => void} rerender
298
+ */
299
+ function handleChangeNestedFormat(parentName, childName, format, rerender) {
300
+ const def = getSelectedDef();
301
+ const parent = def?.properties?.[parentName];
302
+ if (!parent?.properties?.[childName]) return;
303
+
304
+ const prop = parent.properties[childName];
305
+ const type = prop.type || "string";
306
+ parent.properties[childName] = schemaForType(type, format || undefined);
260
307
  rerender();
261
308
  saveProjectConfig();
262
309
  }
@@ -358,11 +405,13 @@ export function renderDefsEditor(container) {
358
405
  onToggleRequired: (n) => handleToggleRequired(n, rerender),
359
406
  onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
360
407
  onChangeType: (n, t) => handleChangeType(n, t, rerender),
408
+ onChangeFormat: (n, f) => handleChangeFormat(n, f, rerender),
361
409
  onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
362
410
  onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
363
411
  onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
364
412
  onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
365
413
  onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
414
+ onChangeNestedFormat: (p, c, f) => handleChangeNestedFormat(p, c, f, rerender),
366
415
  };
367
416
 
368
417
  const fieldCards = Object.entries(properties).map(([name, fieldDef]) =>
@@ -392,7 +441,7 @@ export function renderDefsEditor(container) {
392
441
  onConfirm: () => handleAddField(rerender),
393
442
  onCancel: () => {
394
443
  showAddField = false;
395
- newFieldState = { name: "", type: "string", required: false };
444
+ newFieldState = { name: "", type: "string", format: "", required: false };
396
445
  rerender();
397
446
  },
398
447
  })
@@ -5,17 +5,9 @@
5
5
 
6
6
  import { html, nothing } from "lit-html";
7
7
 
8
- export const FIELD_TYPES = [
9
- "string",
10
- "number",
11
- "boolean",
12
- "array",
13
- "object",
14
- "date",
15
- "image",
16
- "gallery",
17
- "reference",
18
- ];
8
+ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "reference"];
9
+
10
+ export const FORMAT_OPTIONS = ["", "image", "date", "color"];
19
11
 
20
12
  /**
21
13
  * @typedef {{
@@ -34,6 +26,7 @@ export const FIELD_TYPES = [
34
26
  * onToggleRequired: (name: string) => void;
35
27
  * onRename: (oldName: string, newName: string) => void;
36
28
  * onChangeType: (name: string, newType: string) => void;
29
+ * onChangeFormat?: (name: string, format: string) => void;
37
30
  * onChangeRefTarget?: (name: string, target: string) => void;
38
31
  * onAddNestedField?: (
39
32
  * parentName: string,
@@ -43,6 +36,7 @@ export const FIELD_TYPES = [
43
36
  * onToggleNestedRequired?: (parentName: string, childName: string) => void;
44
37
  * onRenameNested?: (parentName: string, oldChild: string, newChild: string) => void;
45
38
  * onChangeNestedType?: (parentName: string, childName: string, newType: string) => void;
39
+ * onChangeNestedFormat?: (parentName: string, childName: string, format: string) => void;
46
40
  * }} FieldHandlers
47
41
  */
48
42
 
@@ -54,12 +48,20 @@ export const FIELD_TYPES = [
54
48
  */
55
49
  export function detectFieldType(schema) {
56
50
  if (schema.$ref) return "reference";
57
- if (schema.format === "image") return "image";
58
- if (schema.format === "date") return "date";
59
- if (schema.type === "array" && schema.items?.format === "image") return "gallery";
60
51
  return schema.type || "string";
61
52
  }
62
53
 
54
+ /**
55
+ * Detect the format from a JSON Schema property definition.
56
+ *
57
+ * @param {SchemaProperty} schema
58
+ * @returns {string}
59
+ */
60
+ export function detectFieldFormat(schema) {
61
+ if (schema.type === "array" && schema.items?.format) return schema.items.format;
62
+ return schema.format || "";
63
+ }
64
+
63
65
  /**
64
66
  * Render a single schema field as an inline-editable form row.
65
67
  *
@@ -72,6 +74,7 @@ export function detectFieldType(schema) {
72
74
  */
73
75
  export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
74
76
  const type = detectFieldType(fieldSchema);
77
+ const format = detectFieldFormat(fieldSchema);
75
78
  const isNested = type === "object";
76
79
  const isRef = type === "reference";
77
80
  const nestedProps = fieldSchema.properties || {};
@@ -100,6 +103,11 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
100
103
  }}
101
104
  ></sp-textfield>
102
105
  ${typePickerTpl(type, (newType) => handlers.onChangeType(fieldName, newType))}
106
+ ${type === "string" || type === "array"
107
+ ? formatPickerTpl(format, (f) => {
108
+ if (handlers.onChangeFormat) handlers.onChangeFormat(fieldName, f);
109
+ })
110
+ : nothing}
103
111
  <sp-switch
104
112
  size="s"
105
113
  ?checked=${isRequired}
@@ -166,6 +174,7 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
166
174
  */
167
175
  function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
168
176
  const type = detectFieldType(childSchema);
177
+ const format = detectFieldFormat(childSchema);
169
178
 
170
179
  return html`
171
180
  <div class="schema-field-card schema-field-card--nested">
@@ -195,6 +204,12 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
195
204
  if (handlers.onChangeNestedType)
196
205
  handlers.onChangeNestedType(parentName, childName, newType);
197
206
  })}
207
+ ${type === "string" || type === "array"
208
+ ? formatPickerTpl(format, (f) => {
209
+ if (handlers.onChangeNestedFormat)
210
+ handlers.onChangeNestedFormat(parentName, childName, f);
211
+ })
212
+ : nothing}
198
213
  <sp-switch
199
214
  size="s"
200
215
  ?checked=${isRequired}
@@ -290,10 +305,30 @@ export function typePickerTpl(value, onChange) {
290
305
  `;
291
306
  }
292
307
 
308
+ /**
309
+ * Render the format picker as an sp-picker dropdown.
310
+ *
311
+ * @param {string} value
312
+ * @param {(format: string) => void} onChange
313
+ * @returns {any}
314
+ */
315
+ export function formatPickerTpl(value, onChange) {
316
+ return html`
317
+ <sp-picker
318
+ size="s"
319
+ label="Format"
320
+ value=${value}
321
+ @change=${(/** @type {any} */ e) => onChange(e.target.value)}
322
+ >
323
+ ${FORMAT_OPTIONS.map((f) => html`<sp-menu-item value=${f}>${f || "(none)"}</sp-menu-item>`)}
324
+ </sp-picker>
325
+ `;
326
+ }
327
+
293
328
  /**
294
329
  * Render the add-field form (inline, not a dialog).
295
330
  *
296
- * @param {{ name: string; type: string; required: boolean }} state
331
+ * @param {{ name: string; type: string; format: string; required: boolean }} state
297
332
  * @param {{
298
333
  * onInput: (field: string, value: any) => void;
299
334
  * onConfirm: () => void;
@@ -315,6 +350,9 @@ export function addFieldFormTpl(state, handlers) {
315
350
  }}
316
351
  ></sp-textfield>
317
352
  ${typePickerTpl(state.type, (t) => handlers.onInput("type", t))}
353
+ ${state.type === "string" || state.type === "array"
354
+ ? formatPickerTpl(state.format || "", (f) => handlers.onInput("format", f))
355
+ : nothing}
318
356
  <sp-switch
319
357
  size="s"
320
358
  ?checked=${state.required}
@@ -329,31 +367,28 @@ export function addFieldFormTpl(state, handlers) {
329
367
  }
330
368
 
331
369
  /**
332
- * Build a JSON Schema property definition from a type string.
370
+ * Build a JSON Schema property definition from a type and optional format.
333
371
  *
334
372
  * @param {string} type
373
+ * @param {string} [format]
335
374
  * @returns {object}
336
375
  */
337
- export function schemaForType(type) {
376
+ export function schemaForType(type, format) {
338
377
  switch (type) {
339
378
  case "number":
340
379
  return { type: "number" };
341
380
  case "boolean":
342
381
  return { type: "boolean" };
343
382
  case "array":
344
- return { type: "array", items: { type: "string" } };
383
+ return format
384
+ ? { type: "array", items: { type: "string", format } }
385
+ : { type: "array", items: { type: "string" } };
345
386
  case "object":
346
387
  return { type: "object", properties: {}, required: [] };
347
- case "date":
348
- return { type: "string", format: "date" };
349
- case "image":
350
- return { type: "string", format: "image" };
351
- case "gallery":
352
- return { type: "array", items: { type: "string", format: "image" } };
353
388
  case "reference":
354
389
  return { $ref: "#/contentTypes/" };
355
390
  default:
356
- return { type: "string" };
391
+ return format ? { type: "string", format } : { type: "string" };
357
392
  }
358
393
  }
359
394