@jxsuite/studio 0.19.0 → 0.21.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 (99) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +13476 -12330
  3. package/dist/studio.js.map +152 -148
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +19 -7
  8. package/src/browse/browse-modal.js +16 -13
  9. package/src/browse/browse.js +44 -35
  10. package/src/canvas/canvas-diff.js +24 -16
  11. package/src/canvas/canvas-helpers.js +24 -19
  12. package/src/canvas/canvas-live-render.js +113 -39
  13. package/src/canvas/canvas-render.js +119 -73
  14. package/src/canvas/canvas-utils.js +51 -21
  15. package/src/canvas/nested-site-style.js +50 -0
  16. package/src/editor/component-inline-edit.js +14 -13
  17. package/src/editor/content-inline-edit.js +22 -10
  18. package/src/editor/context-menu.js +28 -14
  19. package/src/editor/convert-targets.js +60 -0
  20. package/src/editor/convert-to-component.js +20 -19
  21. package/src/editor/convert-to-repeater.js +226 -0
  22. package/src/editor/inline-edit.js +69 -28
  23. package/src/editor/insertion-helper.js +16 -12
  24. package/src/editor/shortcuts.js +14 -4
  25. package/src/editor/slash-menu.js +40 -19
  26. package/src/files/components.js +1 -1
  27. package/src/files/file-ops.js +37 -16
  28. package/src/files/files.js +322 -68
  29. package/src/github/github-auth.js +122 -0
  30. package/src/github/github-publish.js +126 -0
  31. package/src/markdown/md-convert.js +172 -107
  32. package/src/new-project/new-project-modal.js +204 -0
  33. package/src/panels/activity-bar.js +27 -25
  34. package/src/panels/ai-panel.js +406 -0
  35. package/src/panels/block-action-bar.js +77 -28
  36. package/src/panels/canvas-dnd.js +8 -2
  37. package/src/panels/data-explorer.js +19 -13
  38. package/src/panels/dnd.js +144 -92
  39. package/src/panels/editors.js +55 -35
  40. package/src/panels/elements-panel.js +33 -19
  41. package/src/panels/events-panel.js +15 -10
  42. package/src/panels/git-panel.js +475 -99
  43. package/src/panels/head-panel.js +121 -81
  44. package/src/panels/imports-panel.js +94 -65
  45. package/src/panels/layers-panel.js +64 -43
  46. package/src/panels/left-panel.js +105 -48
  47. package/src/panels/overlays.js +1 -0
  48. package/src/panels/panel-events.js +45 -30
  49. package/src/panels/preview-render.js +23 -18
  50. package/src/panels/properties-panel.js +378 -257
  51. package/src/panels/pseudo-preview.js +2 -2
  52. package/src/panels/quick-search.js +11 -9
  53. package/src/panels/right-panel.js +42 -16
  54. package/src/panels/shared.js +4 -4
  55. package/src/panels/signals-panel.js +415 -166
  56. package/src/panels/statusbar.js +10 -6
  57. package/src/panels/style-inputs.js +67 -35
  58. package/src/panels/style-panel.js +239 -134
  59. package/src/panels/style-utils.js +34 -16
  60. package/src/panels/stylebook-layers-panel.js +12 -10
  61. package/src/panels/stylebook-panel.js +134 -66
  62. package/src/panels/tab-strip.js +5 -2
  63. package/src/panels/toolbar.js +333 -109
  64. package/src/panels/welcome-screen.js +121 -0
  65. package/src/platform.js +2 -4
  66. package/src/platforms/devserver.js +113 -7
  67. package/src/resize-edges.js +98 -0
  68. package/src/services/cem-export.js +12 -7
  69. package/src/services/code-services.js +30 -12
  70. package/src/settings/content-types-editor.js +8 -8
  71. package/src/settings/css-vars-editor.js +30 -24
  72. package/src/settings/defs-editor.js +12 -7
  73. package/src/settings/general-settings.js +85 -3
  74. package/src/settings/head-editor.js +30 -24
  75. package/src/settings/schema-field-ui.js +58 -39
  76. package/src/settings/settings-modal.js +12 -9
  77. package/src/site-context.js +41 -31
  78. package/src/state.js +70 -29
  79. package/src/store.js +36 -35
  80. package/src/studio.js +120 -90
  81. package/src/tabs/tab.js +67 -43
  82. package/src/tabs/transact.js +23 -13
  83. package/src/ui/button-group.js +24 -24
  84. package/src/ui/color-selector.js +28 -23
  85. package/src/ui/field-row.js +3 -3
  86. package/src/ui/icons.js +2 -2
  87. package/src/ui/layers.js +31 -1
  88. package/src/ui/media-picker.js +7 -7
  89. package/src/ui/spectrum.js +5 -1
  90. package/src/ui/unit-selector.js +18 -16
  91. package/src/ui/value-selector.js +21 -15
  92. package/src/ui/widgets.js +22 -19
  93. package/src/utils/canvas-media.js +5 -4
  94. package/src/utils/edit-display.js +32 -26
  95. package/src/utils/google-fonts.js +11 -11
  96. package/src/utils/inherited-style.js +9 -8
  97. package/src/utils/studio-utils.js +18 -18
  98. package/src/view.js +58 -1
  99. package/src/workspace/workspace.js +29 -7
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Content inline edit bridge — extracted from studio.js (Phase 4k). Rich-text editing entry point
3
3
  * for edit/content mode. Bridges startEditing() with Jx document state mutations.
4
+ *
5
+ * @typedef {import("./inline-edit.js").JxContentResult} JxContentResult
6
+ *
7
+ * @typedef {import("./inline-edit.js").SlashCommand} SlashCommand
4
8
  */
5
9
 
6
10
  import {
@@ -22,8 +26,8 @@ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
22
26
  /**
23
27
  * Enter rich-text inline editing on a canvas element (edit/content mode).
24
28
  *
25
- * @param {any} el
26
- * @param {any} path
29
+ * @param {HTMLElement} el
30
+ * @param {JxPath} path
27
31
  */
28
32
  export function enterInlineEdit(el, path) {
29
33
  // Restore raw template expressions before editing.
@@ -39,9 +43,9 @@ export function enterInlineEdit(el, path) {
39
43
 
40
44
  startEditing(el, path, {
41
45
  onCommit(
42
- /** @type {any} */ commitPath,
43
- /** @type {any} */ children,
44
- /** @type {any} */ textContent,
46
+ /** @type {JxPath} */ commitPath,
47
+ /** @type {(JxMutableNode | string)[] | null} */ children,
48
+ /** @type {string | null} */ textContent,
45
49
  ) {
46
50
  const node = getNodeAtPath(activeTab.value?.doc.document, commitPath);
47
51
  if (children) {
@@ -59,13 +63,17 @@ export function enterInlineEdit(el, path) {
59
63
  }
60
64
  },
61
65
 
62
- onSplit(/** @type {any} */ splitPath, /** @type {any} */ before, /** @type {any} */ after) {
66
+ onSplit(
67
+ /** @type {JxPath} */ splitPath,
68
+ /** @type {JxContentResult} */ before,
69
+ /** @type {JxContentResult} */ after,
70
+ ) {
63
71
  const tag = "p";
64
72
 
65
73
  // Insert new element after with "after" content
66
- const parentPath = /** @type {any} */ (parentElementPath(splitPath));
74
+ const parentPath = /** @type {JxPath} */ (parentElementPath(splitPath));
67
75
  const idx = /** @type {number} */ (childIndex(splitPath));
68
- /** @type {any} */
76
+ /** @type {JxMutableNode} */
69
77
  const newNode = { tagName: tag };
70
78
  if (after.textContent != null) {
71
79
  newNode.textContent = after.textContent;
@@ -108,7 +116,11 @@ export function enterInlineEdit(el, path) {
108
116
  });
109
117
  },
110
118
 
111
- onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
119
+ onInsert(
120
+ /** @type {JxPath} */ afterPath,
121
+ /** @type {SlashCommand} */ cmd,
122
+ /** @type {JxContentResult | undefined} */ commitData,
123
+ ) {
112
124
  const isEmpty =
113
125
  !commitData ||
114
126
  (commitData.textContent != null && commitData.textContent.trim() === "") ||
@@ -148,7 +160,7 @@ export function enterInlineEdit(el, path) {
148
160
  }
149
161
 
150
162
  const elementDef = defaultDef(cmd.tag);
151
- const parentPath = /** @type {any} */ (parentElementPath(afterPath));
163
+ const parentPath = /** @type {JxPath} */ (parentElementPath(afterPath));
152
164
  const idx = /** @type {number} */ (childIndex(afterPath));
153
165
  const newPath = [...parentPath, "children", idx + 1];
154
166
 
@@ -1,5 +1,6 @@
1
1
  // ─── Clipboard & Context Menu ─────────────────────────────────────────────────
2
2
  import { html } from "lit-html";
3
+ import { ref } from "lit-html/directives/ref.js";
3
4
  import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
4
5
  import { activeTab, workspace } from "../workspace/workspace.js";
5
6
  import {
@@ -12,6 +13,7 @@ import {
12
13
  } from "../tabs/transact.js";
13
14
  import { statusMessage } from "../panels/statusbar.js";
14
15
  import { convertToComponent } from "./convert-to-component.js";
16
+ import { convertToRepeater } from "./convert-to-repeater.js";
15
17
  import { componentRegistry } from "../files/components.js";
16
18
  import { renderPopover } from "../ui/layers.js";
17
19
  import { startLayerTitleEdit } from "../panels/layers-panel.js";
@@ -21,7 +23,7 @@ import { startLayerTitleEdit } from "../panels/layers-panel.js";
21
23
  *
22
24
  * @typedef {import("../state.js").JxPath} JxPath
23
25
  *
24
- * @typedef {import("../state.js").JxNode} JxNode
26
+ * @typedef {JxMutableNode} JxNode
25
27
  */
26
28
 
27
29
  // ─── Clipboard ────────────────────────────────────────────────────────────────
@@ -169,6 +171,15 @@ export function showContextMenu(e, path, opts = {}) {
169
171
  label: "Wrap in Div",
170
172
  action: () => transactDoc(activeTab.value, (t) => mutateWrapNode(t, path)),
171
173
  });
174
+ // Don't show Repeat if already inside a repeater (path ends with "children", "map")
175
+ if (
176
+ !(path.length >= 2 && path[path.length - 2] === "children" && path[path.length - 1] === "map")
177
+ ) {
178
+ items.push({
179
+ label: "Repeat...",
180
+ action: () => convertToRepeater(),
181
+ });
182
+ }
172
183
  items.push({
173
184
  label: "Set Title",
174
185
  action: () => {
@@ -231,7 +242,22 @@ export function showContextMenu(e, path, opts = {}) {
231
242
  y = e.clientY;
232
243
 
233
244
  _ctxHandle = renderPopover(
234
- html`<sp-popover open style="position:fixed;z-index:10000;left:${x}px;top:${y}px">
245
+ html`<sp-popover
246
+ open
247
+ style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
248
+ ${ref((el) => {
249
+ if (!el) return;
250
+ requestAnimationFrame(() => {
251
+ const popover = /** @type {HTMLElement} */ (el);
252
+ const menuRect = popover.getBoundingClientRect();
253
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
254
+ if (y + menuRect.height > window.innerHeight)
255
+ y = window.innerHeight - menuRect.height - 4;
256
+ popover.style.left = `${x}px`;
257
+ popover.style.top = `${y}px`;
258
+ });
259
+ })}
260
+ >
235
261
  <sp-menu>
236
262
  ${items.map((item) =>
237
263
  item.label === "—"
@@ -254,16 +280,4 @@ export function showContextMenu(e, path, opts = {}) {
254
280
  },
255
281
  },
256
282
  );
257
-
258
- requestAnimationFrame(() => {
259
- const popover = /** @type {HTMLElement | null} */ (
260
- _ctxHandle?.host.querySelector("sp-popover")
261
- );
262
- if (!popover) return;
263
- const menuRect = popover.getBoundingClientRect();
264
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
265
- if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
266
- popover.style.left = `${x}px`;
267
- popover.style.top = `${y}px`;
268
- });
269
283
  }
@@ -0,0 +1,60 @@
1
+ import elementsMeta from "../../data/elements-meta.json";
2
+
3
+ /** @typedef {{ label: string; tag: string; description: string }} SlashCommand */
4
+
5
+ const TAG_LABELS = /** @type {Record<string, string>} */ ({
6
+ p: "Paragraph",
7
+ h1: "Heading 1",
8
+ h2: "Heading 2",
9
+ h3: "Heading 3",
10
+ h4: "Heading 4",
11
+ h5: "Heading 5",
12
+ h6: "Heading 6",
13
+ blockquote: "Blockquote",
14
+ div: "Div",
15
+ section: "Section",
16
+ article: "Article",
17
+ aside: "Aside",
18
+ main: "Main",
19
+ header: "Header",
20
+ footer: "Footer",
21
+ nav: "Nav",
22
+ search: "Search",
23
+ ul: "Bulleted List",
24
+ ol: "Numbered List",
25
+ });
26
+
27
+ const groups = /** @type {Record<string, string[]>} */ (elementsMeta.$convertGroups || {});
28
+
29
+ /**
30
+ * Get the list of tags the current element can be converted to.
31
+ *
32
+ * @param {string} currentTag
33
+ * @param {boolean} isEmpty
34
+ * @returns {SlashCommand[]}
35
+ */
36
+ export function getConvertTargets(currentTag, isEmpty) {
37
+ const def = /** @type {Record<string, unknown> | undefined} */ (
38
+ /** @type {Record<string, Record<string, unknown>>} */ (elementsMeta.$defs)?.[currentTag]
39
+ );
40
+ if (!def?.$convertTo) return [];
41
+
42
+ const groupNames =
43
+ isEmpty && def.$convertToWhenEmpty
44
+ ? /** @type {string[]} */ (def.$convertToWhenEmpty)
45
+ : [/** @type {string} */ (def.$convertTo)];
46
+
47
+ /** @type {Set<string>} */
48
+ const tags = new Set();
49
+ for (const name of groupNames) {
50
+ for (const tag of groups[name] || []) {
51
+ if (tag !== currentTag) tags.add(tag);
52
+ }
53
+ }
54
+
55
+ return [...tags].map((tag) => ({
56
+ label: TAG_LABELS[tag] || tag.charAt(0).toUpperCase() + tag.slice(1),
57
+ tag,
58
+ description: "",
59
+ }));
60
+ }
@@ -1,5 +1,6 @@
1
1
  // ─── Convert to Component ─────────────────────────────────────────────────────
2
2
  import { html, render as litRender } from "lit-html";
3
+ import { ref } from "lit-html/directives/ref.js";
3
4
  import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
4
5
  import { activeTab } from "../workspace/workspace.js";
5
6
  import { transact } from "../tabs/transact.js";
@@ -39,15 +40,17 @@ export async function convertToComponent() {
39
40
  transact(tab, (doc) => {
40
41
  // Navigate to parent's children array and replace the node
41
42
  const pp = parentElementPath(selectionPath) ?? [];
42
- const idx = childIndex(selectionPath);
43
+ const idx = /** @type {number} */ (childIndex(selectionPath));
43
44
  let parent = doc;
44
45
  for (const seg of pp) parent = parent[seg];
46
+ if (!parent.children) parent.children = [];
45
47
  parent.children[idx] = { tagName: name };
46
48
 
47
49
  // Ensure $elements exists and add the $ref
48
50
  if (!doc.$elements) doc.$elements = [];
49
51
  const alreadyReferenced = doc.$elements.some(
50
- (/** @type {any} */ el) => el && el.$ref === refPath,
52
+ (/** @type {JxMutableNode | string | { $ref: string }} */ el) =>
53
+ el && typeof el === "object" && "$ref" in el && el.$ref === refPath,
51
54
  );
52
55
  if (!alreadyReferenced) {
53
56
  doc.$elements.push({ $ref: refPath });
@@ -60,15 +63,15 @@ export async function convertToComponent() {
60
63
  await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
61
64
  await loadComponentRegistry();
62
65
  statusMessage(`Converted to <${name}>`);
63
- } catch (/** @type {any} */ err) {
64
- statusMessage(`Error saving component: ${err.message}`);
66
+ } catch (/** @type {unknown} */ err) {
67
+ statusMessage(`Error saving component: ${/** @type {Error} */ (err).message}`);
65
68
  }
66
69
  }
67
70
 
68
71
  /**
69
72
  * Derive a default tag name from a node.
70
73
  *
71
- * @param {any} node
74
+ * @param {JxMutableNode} node
72
75
  * @returns {string}
73
76
  */
74
77
  function deriveDefaultName(node) {
@@ -80,8 +83,8 @@ function deriveDefaultName(node) {
80
83
  /**
81
84
  * Deep clone a node and strip page-specific keys.
82
85
  *
83
- * @param {any} node
84
- * @returns {any}
86
+ * @param {JxMutableNode} node
87
+ * @returns {JxMutableNode}
85
88
  */
86
89
  function extractComponentDef(node) {
87
90
  const clone = structuredClone(node);
@@ -105,7 +108,7 @@ function validateName(val) {
105
108
  if (!VALID_NAME.test(val)) {
106
109
  return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
107
110
  }
108
- const exists = componentRegistry.some((/** @type {any} */ c) => c.tagName === val);
111
+ const exists = componentRegistry.some((/** @type {JxMutableNode} */ c) => c.tagName === val);
109
112
  if (exists) {
110
113
  return { valid: false, error: `Component <${val}> already exists` };
111
114
  }
@@ -134,7 +137,7 @@ function promptComponentName(defaultName) {
134
137
  }
135
138
 
136
139
  function onInput(/** @type {Event} */ e) {
137
- value = /** @type {any} */ (e.target).value || "";
140
+ value = /** @type {HTMLInputElement} */ (e.target).value || "";
138
141
  const result = validateName(value);
139
142
  error = result.valid ? "" : result.error;
140
143
  rerender();
@@ -170,6 +173,14 @@ function promptComponentName(defaultName) {
170
173
  ?negative=${!!error}
171
174
  @input=${onInput}
172
175
  @keydown=${onKeydown}
176
+ ${ref((el) => {
177
+ if (el)
178
+ requestAnimationFrame(() => {
179
+ /** @type {HTMLElement} */ (el).focus();
180
+ const input = /** @type {HTMLElement} */ (el).shadowRoot?.querySelector("input");
181
+ if (input) input.select();
182
+ });
183
+ })}
173
184
  >
174
185
  <sp-help-text slot="negative-help-text">${error}</sp-help-text>
175
186
  </sp-textfield>
@@ -177,16 +188,6 @@ function promptComponentName(defaultName) {
177
188
  `;
178
189
  }
179
190
 
180
- requestAnimationFrame(() => {
181
- const layer = document.getElementById("layer-dialog");
182
- const tf = /** @type {any} */ (layer?.querySelector("sp-textfield"));
183
- if (tf) {
184
- tf.focus();
185
- const input = tf.shadowRoot?.querySelector("input");
186
- if (input) input.select();
187
- }
188
- });
189
-
190
191
  return buildTpl();
191
192
  });
192
193
  }
@@ -0,0 +1,226 @@
1
+ // ─── Convert to Repeater ──────────────────────────────────────────────────────
2
+ import { html, render as litRender, nothing } from "lit-html";
3
+ import { ref } from "lit-html/directives/ref.js";
4
+ import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
5
+ import { activeTab } from "../workspace/workspace.js";
6
+ import { transactDoc } from "../tabs/transact.js";
7
+ import { showDialog } from "../ui/layers.js";
8
+ import { defCategory } from "../panels/signals-panel.js";
9
+
10
+ /**
11
+ * @typedef {{
12
+ * items: { $ref: string } | unknown[];
13
+ * filter?: { $ref: string };
14
+ * sort?: { $ref: string };
15
+ * newDef?: { name: string };
16
+ * }} RepeaterConfig
17
+ */
18
+
19
+ /** Convert the currently selected element into a repeater template. */
20
+ export async function convertToRepeater() {
21
+ const tab = activeTab.value;
22
+ if (!tab?.session.selection || tab.session.selection.length < 2) return;
23
+
24
+ const path = tab.session.selection;
25
+ const node = getNodeAtPath(tab.doc.document, path);
26
+ if (!node) return;
27
+
28
+ const defs = tab.doc.document.state || {};
29
+ const config = await promptRepeaterConfig(defs);
30
+ if (!config) return;
31
+
32
+ transactDoc(tab, (t) => {
33
+ const doc = t.doc.document;
34
+ if (config.newDef) {
35
+ if (!doc.state) doc.state = {};
36
+ doc.state[config.newDef.name] = { type: "array", default: [] };
37
+ }
38
+ const pp = parentElementPath(path);
39
+ if (!pp) return;
40
+ const idx = /** @type {number} */ (childIndex(path));
41
+ const parent = getNodeAtPath(doc, pp);
42
+ if (!parent?.children) return;
43
+ const element = parent.children[idx];
44
+
45
+ /** @type {Record<string, unknown>} */
46
+ const repeater = {
47
+ $prototype: "Array",
48
+ items: config.items,
49
+ map: element,
50
+ };
51
+ if (config.filter) repeater.filter = config.filter;
52
+ if (config.sort) repeater.sort = config.sort;
53
+
54
+ /** @type {(string | JxMutableNode)[]} */ (parent.children)[idx] = {
55
+ tagName: "div",
56
+ children: /** @type {any} */ (repeater),
57
+ };
58
+ });
59
+ }
60
+
61
+ /**
62
+ * @param {Record<string, unknown>} defs
63
+ * @returns {Promise<RepeaterConfig | null>}
64
+ */
65
+ function promptRepeaterConfig(defs) {
66
+ const arrayDefs = Object.entries(defs).filter(
67
+ ([, d]) =>
68
+ /** @type {any} */ (d)?.type === "array" ||
69
+ Array.isArray(/** @type {any} */ (d)?.default) ||
70
+ /** @type {any} */ (d)?.$prototype === "Array",
71
+ );
72
+ const fnDefs = Object.entries(defs).filter(([, d]) => defCategory(d) === "function");
73
+
74
+ let source = arrayDefs.length > 0 ? arrayDefs[0][0] : "__new__";
75
+ let newDefName = "";
76
+ let filterDef = "";
77
+ let sortDef = "";
78
+ let error = "";
79
+
80
+ return showDialog((done) => {
81
+ function confirm() {
82
+ if (source === "__new__") {
83
+ const name = newDefName.trim();
84
+ if (!name) {
85
+ error = "Enter a name for the new state definition.";
86
+ rerender();
87
+ return;
88
+ }
89
+ if (defs[name]) {
90
+ error = `"${name}" already exists.`;
91
+ rerender();
92
+ return;
93
+ }
94
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
95
+ error = "Invalid identifier name.";
96
+ rerender();
97
+ return;
98
+ }
99
+ done({
100
+ items: { $ref: `#/$defs/${name}` },
101
+ filter: filterDef ? { $ref: `#/$defs/${filterDef}` } : undefined,
102
+ sort: sortDef ? { $ref: `#/$defs/${sortDef}` } : undefined,
103
+ newDef: { name },
104
+ });
105
+ } else {
106
+ done({
107
+ items: { $ref: `#/$defs/${source}` },
108
+ filter: filterDef ? { $ref: `#/$defs/${filterDef}` } : undefined,
109
+ sort: sortDef ? { $ref: `#/$defs/${sortDef}` } : undefined,
110
+ });
111
+ }
112
+ }
113
+
114
+ function rerender() {
115
+ const layer = document.getElementById("layer-dialog");
116
+ const slot = layer?.lastElementChild;
117
+ if (slot) litRender(buildTpl(), /** @type {HTMLElement} */ (slot));
118
+ }
119
+
120
+ function buildTpl() {
121
+ return html`
122
+ <sp-dialog-wrapper
123
+ open
124
+ underlay
125
+ headline="Repeat..."
126
+ confirm-label="Create Repeater"
127
+ cancel-label="Cancel"
128
+ size="s"
129
+ @confirm=${confirm}
130
+ @cancel=${() => done(null)}
131
+ @close=${() => done(null)}
132
+ >
133
+ <div style="display:flex;flex-direction:column;gap:12px">
134
+ <label>
135
+ <sp-field-label size="s">Items source</sp-field-label>
136
+ <sp-picker
137
+ label="Items source"
138
+ size="s"
139
+ .value=${source}
140
+ @change=${(/** @type {Event} */ e) => {
141
+ source = /** @type {HTMLInputElement} */ (e.target).value;
142
+ error = "";
143
+ rerender();
144
+ }}
145
+ >
146
+ ${arrayDefs.map(
147
+ ([name]) => html`<sp-menu-item value=${name}>${name}</sp-menu-item>`,
148
+ )}
149
+ <sp-menu-divider></sp-menu-divider>
150
+ <sp-menu-item value="__new__">Create new...</sp-menu-item>
151
+ </sp-picker>
152
+ </label>
153
+
154
+ ${source === "__new__"
155
+ ? html`
156
+ <label>
157
+ <sp-field-label size="s">New definition name</sp-field-label>
158
+ <sp-textfield
159
+ size="s"
160
+ placeholder="myItems"
161
+ .value=${newDefName}
162
+ ?negative=${!!error}
163
+ @input=${(/** @type {Event} */ e) => {
164
+ newDefName = /** @type {HTMLInputElement} */ (e.target).value || "";
165
+ error = "";
166
+ rerender();
167
+ }}
168
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
169
+ if (e.key === "Enter") confirm();
170
+ }}
171
+ ${ref((el) => {
172
+ if (el && source === "__new__")
173
+ requestAnimationFrame(() => /** @type {HTMLElement} */ (el).focus());
174
+ })}
175
+ >
176
+ <sp-help-text slot="negative-help-text">${error}</sp-help-text>
177
+ </sp-textfield>
178
+ </label>
179
+ `
180
+ : nothing}
181
+ ${fnDefs.length > 0
182
+ ? html`
183
+ <label>
184
+ <sp-field-label size="s">Filter (optional)</sp-field-label>
185
+ <sp-picker
186
+ label="Filter"
187
+ size="s"
188
+ .value=${filterDef}
189
+ @change=${(/** @type {Event} */ e) => {
190
+ filterDef = /** @type {HTMLInputElement} */ (e.target).value;
191
+ rerender();
192
+ }}
193
+ >
194
+ <sp-menu-item value="">None</sp-menu-item>
195
+ ${fnDefs.map(
196
+ ([name]) => html`<sp-menu-item value=${name}>${name}</sp-menu-item>`,
197
+ )}
198
+ </sp-picker>
199
+ </label>
200
+ <label>
201
+ <sp-field-label size="s">Sort (optional)</sp-field-label>
202
+ <sp-picker
203
+ label="Sort"
204
+ size="s"
205
+ .value=${sortDef}
206
+ @change=${(/** @type {Event} */ e) => {
207
+ sortDef = /** @type {HTMLInputElement} */ (e.target).value;
208
+ rerender();
209
+ }}
210
+ >
211
+ <sp-menu-item value="">None</sp-menu-item>
212
+ ${fnDefs.map(
213
+ ([name]) => html`<sp-menu-item value=${name}>${name}</sp-menu-item>`,
214
+ )}
215
+ </sp-picker>
216
+ </label>
217
+ `
218
+ : nothing}
219
+ </div>
220
+ </sp-dialog-wrapper>
221
+ `;
222
+ }
223
+
224
+ return buildTpl();
225
+ });
226
+ }