@jxsuite/studio 0.28.4 → 0.29.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 (81) hide show
  1. package/dist/studio.js +6769 -5399
  2. package/dist/studio.js.map +86 -79
  3. package/package.json +5 -4
  4. package/src/browse/browse-modal.ts +2 -2
  5. package/src/browse/browse.ts +20 -19
  6. package/src/canvas/canvas-diff.ts +3 -3
  7. package/src/canvas/canvas-helpers.ts +15 -2
  8. package/src/canvas/canvas-live-render.ts +168 -86
  9. package/src/canvas/canvas-patcher.ts +656 -0
  10. package/src/canvas/canvas-perf.ts +68 -0
  11. package/src/canvas/canvas-render.ts +135 -23
  12. package/src/canvas/canvas-subtree-render.ts +113 -0
  13. package/src/canvas/canvas-utils.ts +4 -0
  14. package/src/editor/component-inline-edit.ts +4 -35
  15. package/src/editor/context-menu.ts +92 -74
  16. package/src/editor/convert-to-component.ts +11 -2
  17. package/src/editor/convert-to-repeater.ts +4 -5
  18. package/src/editor/inline-edit.ts +9 -9
  19. package/src/editor/inline-format.ts +11 -11
  20. package/src/editor/merge-tags.ts +120 -0
  21. package/src/editor/shortcuts.ts +4 -4
  22. package/src/files/components.ts +37 -0
  23. package/src/files/file-ops.ts +28 -7
  24. package/src/files/files.ts +25 -21
  25. package/src/github/github-auth.ts +14 -2
  26. package/src/github/github-publish.ts +12 -2
  27. package/src/panels/activity-bar.ts +1 -1
  28. package/src/panels/ai-panel.ts +67 -39
  29. package/src/panels/block-action-bar.ts +72 -1
  30. package/src/panels/canvas-dnd.ts +55 -26
  31. package/src/panels/data-explorer.ts +5 -3
  32. package/src/panels/dnd.ts +12 -15
  33. package/src/panels/editors.ts +5 -23
  34. package/src/panels/elements-panel.ts +2 -12
  35. package/src/panels/events-panel.ts +1 -1
  36. package/src/panels/git-panel.ts +6 -6
  37. package/src/panels/head-panel.ts +1 -1
  38. package/src/panels/layers-panel.ts +177 -147
  39. package/src/panels/preview-render.ts +15 -0
  40. package/src/panels/properties-panel.ts +16 -27
  41. package/src/panels/quick-search.ts +2 -2
  42. package/src/panels/right-panel.ts +2 -6
  43. package/src/panels/shared.ts +3 -0
  44. package/src/panels/signals-panel.ts +24 -22
  45. package/src/panels/statusbar.ts +15 -6
  46. package/src/panels/style-panel.ts +3 -3
  47. package/src/panels/style-utils.ts +3 -3
  48. package/src/panels/stylebook-panel.ts +4 -4
  49. package/src/panels/tab-bar.ts +198 -0
  50. package/src/panels/tab-strip.ts +2 -2
  51. package/src/panels/toolbar.ts +6 -75
  52. package/src/platforms/devserver.ts +60 -34
  53. package/src/reactivity.ts +2 -0
  54. package/src/services/cem-export.ts +23 -2
  55. package/src/services/code-services.ts +16 -2
  56. package/src/services/monaco-setup.ts +2 -1
  57. package/src/settings/content-types-editor.ts +16 -16
  58. package/src/settings/css-vars-editor.ts +2 -2
  59. package/src/settings/defs-editor.ts +14 -14
  60. package/src/settings/general-settings.ts +6 -6
  61. package/src/settings/head-editor.ts +2 -2
  62. package/src/site-context.ts +1 -1
  63. package/src/state.ts +66 -12
  64. package/src/store.ts +15 -3
  65. package/src/studio.ts +73 -33
  66. package/src/tabs/patch-ops.ts +143 -0
  67. package/src/tabs/tab.ts +15 -1
  68. package/src/tabs/transact.ts +454 -27
  69. package/src/types.ts +41 -0
  70. package/src/ui/color-selector.ts +7 -7
  71. package/src/ui/icons.ts +0 -30
  72. package/src/ui/media-picker.ts +2 -2
  73. package/src/ui/panel-resize.ts +6 -1
  74. package/src/ui/unit-selector.ts +2 -2
  75. package/src/ui/value-selector.ts +3 -3
  76. package/src/utils/canvas-media.ts +6 -6
  77. package/src/utils/edit-display.ts +125 -83
  78. package/src/utils/google-fonts.ts +1 -1
  79. package/src/utils/inherited-style.ts +3 -2
  80. package/src/utils/studio-utils.ts +1 -1
  81. package/src/view.ts +4 -1
@@ -7,6 +7,7 @@
7
7
  import { html, nothing } from "lit-html";
8
8
  import { classMap } from "lit-html/directives/class-map.js";
9
9
  import { ifDefined } from "lit-html/directives/if-defined.js";
10
+ import { repeat } from "lit-html/directives/repeat.js";
10
11
  import {
11
12
  VOID_ELEMENTS,
12
13
  childIndex,
@@ -123,7 +124,7 @@ export function renderLayersTemplate(ctx: {
123
124
  const rows = flattenTree(tab!.doc.document);
124
125
  const collapsed = (view._layersCollapsed ||= new Set());
125
126
 
126
- const layerRows: TemplateResult[] = [];
127
+ const layerRows: { key: string; tpl: TemplateResult }[] = [];
127
128
  for (const { node, path, depth, nodeType } of rows) {
128
129
  let hidden = false;
129
130
  for (let d = 1; d <= path.length; d++) {
@@ -143,15 +144,18 @@ export function renderLayersTemplate(ctx: {
143
144
 
144
145
  if (nodeType === "text") {
145
146
  const textPreview = String(node).length > 40 ? `${String(node).slice(0, 40)}…` : String(node);
146
- layerRows.push(html`
147
- <div
148
- class="layer-row"
149
- style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
150
- >
151
- <span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
152
- <span class="layer-label">${textPreview}</span>
153
- </div>
154
- `);
147
+ layerRows.push({
148
+ key: pathKey(path),
149
+ tpl: html`
150
+ <div
151
+ class="layer-row"
152
+ style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
153
+ >
154
+ <span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
155
+ <span class="layer-label">${textPreview}</span>
156
+ </div>
157
+ `,
158
+ });
155
159
  continue;
156
160
  }
157
161
 
@@ -183,7 +187,10 @@ export function renderLayersTemplate(ctx: {
183
187
  Object.keys(jxNode.cases).length > 0;
184
188
  const isExpandable =
185
189
  hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
186
- const isVoidEl = VOID_ELEMENTS.has((jxNode.tagName || "div").toLowerCase());
190
+ // Array nodes can't accept dropped children (their content is the single map template), so they
191
+ // Block the make-child drop instruction like void elements do.
192
+ const isVoidEl =
193
+ VOID_ELEMENTS.has((jxNode.tagName || "div").toLowerCase()) || nodeType === "map";
187
194
 
188
195
  /** @type {string} */
189
196
  let badgeClass;
@@ -203,6 +210,14 @@ export function renderLayersTemplate(ctx: {
203
210
  badgeClass = "layer-tag switch-tag";
204
211
  badgeText = "⇄";
205
212
  badgeTitle = "$switch";
213
+ } else if (jxNode.tagName === "slot") {
214
+ const slotName = jxNode.attributes?.name;
215
+ badgeClass = "layer-tag slot-tag";
216
+ badgeText = "▣";
217
+ badgeTitle =
218
+ typeof slotName === "string" && slotName.trim()
219
+ ? `Slot "${slotName.trim()}"`
220
+ : "Default slot";
206
221
  } else {
207
222
  badgeClass = "layer-tag";
208
223
  badgeText = jxNode.tagName || "div";
@@ -221,14 +236,18 @@ export function renderLayersTemplate(ctx: {
221
236
  labelItalic = false;
222
237
  }
223
238
 
224
- const isElement = nodeType === "element";
239
+ // Array (repeater) nodes are first-class structural nodes — movable/draggable/deletable like
240
+ // Elements. Both sit at a numeric child index; templates (path tail "map") and case nodes do
241
+ // Not, so they stay selectable/editable but not structurally manipulable.
242
+ const isStructural =
243
+ (nodeType === "element" || nodeType === "map") && typeof childIndex(path) === "number";
225
244
  const isRoot = tab?.doc.mode === "content" ? path.length === 0 : path.length < 2;
226
- const idx = isElement ? (childIndex(path) as number) : 0;
227
- const parentPath = isElement && !isRoot ? (parentElementPath(path) as JxPath) : null;
245
+ const idx = isStructural ? (childIndex(path) as number) : 0;
246
+ const parentPath = isStructural && !isRoot ? (parentElementPath(path) as JxPath) : null;
228
247
  const parentNode = parentPath ? getNodeAtPath(tab!.doc.document, parentPath) : null;
229
248
  const siblingCount = childList(parentNode).length;
230
- const canMoveUp = isElement && !isRoot && idx > 0;
231
- const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
249
+ const canMoveUp = isStructural && !isRoot && idx > 0;
250
+ const canMoveDown = isStructural && !isRoot && idx < siblingCount - 1;
232
251
  const prevSibling = canMoveUp && parentNode ? childList(parentNode)[idx - 1] : null;
233
252
  const prevIsContainer = (() => {
234
253
  if (!prevSibling || typeof prevSibling !== "object") {
@@ -257,142 +276,149 @@ export function renderLayersTemplate(ctx: {
257
276
  (c) => typeof c === "object" && c !== null && !isInlineElement(c, prevSibling),
258
277
  );
259
278
  })();
260
- const canMoveIn = isElement && !isRoot && prevIsContainer;
279
+ const canMoveIn = isStructural && !isRoot && prevIsContainer;
261
280
  const grandparentPath =
262
- isElement && parentPath && parentPath.length >= 2
281
+ isStructural && parentPath && parentPath.length >= 2
263
282
  ? (parentElementPath(parentPath) as JxPath)
264
283
  : null;
265
- const canMoveOut = isElement && !isRoot && Boolean(grandparentPath);
284
+ const canMoveOut = isStructural && !isRoot && Boolean(grandparentPath);
266
285
 
267
- layerRows.push(html`
268
- <div
269
- class=${classMap({ "layer-row": true, selected: isSelected })}
270
- data-path=${key}
271
- data-dnd-row=${isElement ? key : nothing}
272
- data-dnd-depth=${isElement ? depth : nothing}
273
- data-dnd-void=${isElement && isVoidEl ? "" : nothing}
274
- data-dnd-expanded=${isElement && isExpandable && !collapsed.has(key) ? "" : nothing}
275
- @click=${() => {
276
- activeTab.value!.session.selection = path;
277
- panToElement(path);
278
- }}
279
- @dblclick=${isElement
280
- ? (e: MouseEvent) => {
281
- e.stopPropagation();
282
- startLayerTitleEdit(path, ctx.rerender);
283
- }
284
- : nothing}
285
- @contextmenu=${isElement
286
- ? (e: MouseEvent) =>
287
- showContextMenu(e, path, {
288
- onEditComponent: ctx.navigateToComponent,
289
- rerender: ctx.rerender,
290
- })
291
- : nothing}
292
- >
293
- <span class="layer-indent" style="width:${depth * 16}px"></span>
294
- <span class="layer-toggle"
295
- >${isExpandable
286
+ layerRows.push({
287
+ key,
288
+ tpl: html`
289
+ <div
290
+ class=${classMap({ "layer-row": true, selected: isSelected })}
291
+ data-path=${key}
292
+ data-dnd-row=${isStructural ? key : nothing}
293
+ data-dnd-depth=${isStructural ? depth : nothing}
294
+ data-dnd-void=${isStructural && isVoidEl ? "" : nothing}
295
+ data-dnd-expanded=${isStructural && isExpandable && !collapsed.has(key) ? "" : nothing}
296
+ @click=${() => {
297
+ activeTab.value!.session.selection = path;
298
+ panToElement(path);
299
+ }}
300
+ @dblclick=${isStructural
301
+ ? (e: MouseEvent) => {
302
+ e.stopPropagation();
303
+ startLayerTitleEdit(path, ctx.rerender);
304
+ }
305
+ : nothing}
306
+ @contextmenu=${isStructural
307
+ ? (e: MouseEvent) =>
308
+ showContextMenu(e, path, {
309
+ onEditComponent: ctx.navigateToComponent,
310
+ rerender: ctx.rerender,
311
+ })
312
+ : nothing}
313
+ >
314
+ <span class="layer-indent" style="width:${depth * 16}px"></span>
315
+ <span class="layer-toggle"
316
+ >${isExpandable
317
+ ? html`
318
+ ${collapsed.has(key)
319
+ ? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
320
+ : html`<sp-icon-chevron-down></sp-icon-chevron-down>`}
321
+ `
322
+ : nothing}</span
323
+ >
324
+ <span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
325
+ <span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
326
+ >${labelText}</span
327
+ >
328
+ ${isStructural && !isRoot
296
329
  ? html`
297
- ${collapsed.has(key)
298
- ? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
299
- : html`<sp-icon-chevron-down></sp-icon-chevron-down>`}
330
+ <span class="layer-actions">
331
+ <span class="layer-drag-handle" title="Drag to reorder">⠿</span>
332
+ ${canMoveUp
333
+ ? html`<sp-action-button
334
+ quiet
335
+ size="xs"
336
+ title="Move up"
337
+ @click=${(e: MouseEvent) => {
338
+ e.stopPropagation();
339
+ (e.currentTarget as HTMLElement).blur();
340
+ const pp = parentPath as JxPath;
341
+ transactDoc(activeTab.value!, (t) =>
342
+ mutateMoveNode(t, path, pp, idx - 1),
343
+ );
344
+ }}
345
+ >
346
+ <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
347
+ </sp-action-button>`
348
+ : nothing}
349
+ ${canMoveDown
350
+ ? html`<sp-action-button
351
+ quiet
352
+ size="xs"
353
+ title="Move down"
354
+ @click=${(e: MouseEvent) => {
355
+ e.stopPropagation();
356
+ (e.currentTarget as HTMLElement).blur();
357
+ const pp = parentPath as JxPath;
358
+ transactDoc(activeTab.value!, (t) =>
359
+ mutateMoveNode(t, path, pp, idx + 2),
360
+ );
361
+ }}
362
+ >
363
+ <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
364
+ </sp-action-button>`
365
+ : nothing}
366
+ ${canMoveIn
367
+ ? html`<sp-action-button
368
+ quiet
369
+ size="xs"
370
+ title="Move into previous sibling"
371
+ @click=${(e: MouseEvent) => {
372
+ e.stopPropagation();
373
+ (e.currentTarget as HTMLElement).blur();
374
+ const pp = parentPath as JxPath;
375
+ const prevPath = [...pp, "children", idx - 1];
376
+ const prev = getNodeAtPath(activeTab.value!.doc.document, prevPath);
377
+ const len = childList(prev).length;
378
+ transactDoc(activeTab.value!, (t) =>
379
+ mutateMoveNode(t, path, prevPath, len),
380
+ );
381
+ }}
382
+ >
383
+ <sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
384
+ </sp-action-button>`
385
+ : nothing}
386
+ ${canMoveOut
387
+ ? html`<sp-action-button
388
+ quiet
389
+ size="xs"
390
+ title="Move out of parent"
391
+ @click=${(e: MouseEvent) => {
392
+ e.stopPropagation();
393
+ (e.currentTarget as HTMLElement).blur();
394
+ const gp = grandparentPath as JxPath;
395
+ const parentIdx = childIndex(parentPath!) as number;
396
+ transactDoc(activeTab.value!, (t) =>
397
+ mutateMoveNode(t, path, gp, parentIdx + 1),
398
+ );
399
+ }}
400
+ >
401
+ <sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
402
+ </sp-action-button>`
403
+ : nothing}
404
+ <sp-action-button
405
+ quiet
406
+ size="xs"
407
+ class="layer-delete"
408
+ title="Delete"
409
+ @click=${(e: MouseEvent) => {
410
+ e.stopPropagation();
411
+ transactDoc(activeTab.value!, (t) => mutateRemoveNode(t, path));
412
+ }}
413
+ >
414
+ <sp-icon-close slot="icon"></sp-icon-close>
415
+ </sp-action-button>
416
+ </span>
300
417
  `
301
- : nothing}</span
302
- >
303
- <span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
304
- <span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
305
- >${labelText}</span
306
- >
307
- ${isElement && !isRoot
308
- ? html`
309
- <span class="layer-actions">
310
- <span class="layer-drag-handle" title="Drag to reorder">⠿</span>
311
- ${canMoveUp
312
- ? html`<sp-action-button
313
- quiet
314
- size="xs"
315
- title="Move up"
316
- @click=${(e: MouseEvent) => {
317
- e.stopPropagation();
318
- (e.currentTarget as HTMLElement).blur();
319
- const pp = parentPath as JxPath;
320
- transactDoc(activeTab.value!, (t) => mutateMoveNode(t, path, pp, idx - 1));
321
- }}
322
- >
323
- <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
324
- </sp-action-button>`
325
- : nothing}
326
- ${canMoveDown
327
- ? html`<sp-action-button
328
- quiet
329
- size="xs"
330
- title="Move down"
331
- @click=${(e: MouseEvent) => {
332
- e.stopPropagation();
333
- (e.currentTarget as HTMLElement).blur();
334
- const pp = parentPath as JxPath;
335
- transactDoc(activeTab.value!, (t) => mutateMoveNode(t, path, pp, idx + 2));
336
- }}
337
- >
338
- <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
339
- </sp-action-button>`
340
- : nothing}
341
- ${canMoveIn
342
- ? html`<sp-action-button
343
- quiet
344
- size="xs"
345
- title="Move into previous sibling"
346
- @click=${(e: MouseEvent) => {
347
- e.stopPropagation();
348
- (e.currentTarget as HTMLElement).blur();
349
- const pp = parentPath as JxPath;
350
- const prevPath = [...pp, idx - 1];
351
- const prev = getNodeAtPath(activeTab.value!.doc.document, prevPath);
352
- const len = childList(prev).length;
353
- transactDoc(activeTab.value!, (t) =>
354
- mutateMoveNode(t, path, prevPath, len),
355
- );
356
- }}
357
- >
358
- <sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
359
- </sp-action-button>`
360
- : nothing}
361
- ${canMoveOut
362
- ? html`<sp-action-button
363
- quiet
364
- size="xs"
365
- title="Move out of parent"
366
- @click=${(e: MouseEvent) => {
367
- e.stopPropagation();
368
- (e.currentTarget as HTMLElement).blur();
369
- const gp = grandparentPath as JxPath;
370
- const parentIdx = childIndex(parentPath!) as number;
371
- transactDoc(activeTab.value!, (t) =>
372
- mutateMoveNode(t, path, gp, parentIdx + 1),
373
- );
374
- }}
375
- >
376
- <sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
377
- </sp-action-button>`
378
- : nothing}
379
- <sp-action-button
380
- quiet
381
- size="xs"
382
- class="layer-delete"
383
- title="Delete"
384
- @click=${(e: MouseEvent) => {
385
- e.stopPropagation();
386
- transactDoc(activeTab.value!, (t) => mutateRemoveNode(t, path));
387
- }}
388
- >
389
- <sp-icon-close slot="icon"></sp-icon-close>
390
- </sp-action-button>
391
- </span>
392
- `
393
- : nothing}
394
- </div>
395
- `);
418
+ : nothing}
419
+ </div>
420
+ `,
421
+ });
396
422
  }
397
423
 
398
424
  return html`
@@ -421,7 +447,11 @@ export function renderLayersTemplate(ctx: {
421
447
  ctx.rerender();
422
448
  }}
423
449
  >
424
- ${layerRows}
450
+ ${repeat(
451
+ layerRows,
452
+ (r) => r.key,
453
+ (r) => r.tpl,
454
+ )}
425
455
  </div>
426
456
  </div>
427
457
  `;
@@ -37,6 +37,21 @@ export function renderCanvasNode(
37
37
  return;
38
38
  }
39
39
 
40
+ // Array pseudo-element: render as a repeater perimeter at the array's own path, with a single
41
+ // Template instance inside (matches the edit-mode visual).
42
+ if (isMappedArray(node)) {
43
+ const wrapper = document.createElement("div");
44
+ wrapper.className = "repeater-perimeter";
45
+ elToPath.set(wrapper, path);
46
+ const template = node.map;
47
+ if (template && typeof template === "object") {
48
+ renderCanvasNode(template, [...path, "map"], wrapper, activeBreakpoints, featureToggles);
49
+ }
50
+ wrapper.style.pointerEvents = "none";
51
+ parent.append(wrapper);
52
+ return wrapper;
53
+ }
54
+
40
55
  const tag = node.tagName || "div";
41
56
  const el = document.createElement(tag);
42
57
 
@@ -55,6 +55,7 @@ interface SignalOption {
55
55
  interface HtmlMetaEntry {
56
56
  $section: string;
57
57
  $order: number;
58
+ $attr?: string;
58
59
  $elements?: string[];
59
60
  $label?: string;
60
61
  $input?: string;
@@ -77,12 +78,9 @@ function isInsideMapTemplate(path: JxPath | null) {
77
78
  if (!path) {
78
79
  return false;
79
80
  }
80
- for (let i = 0; i < path.length - 1; i++) {
81
- if (path[i] === "children" && path[i + 1] === "map") {
82
- return true;
83
- }
84
- }
85
- return false;
81
+ // A "map" segment addresses a repeater template (`[…, "children", i, "map", …]`, or the legacy
82
+ // `[…, "children", "map", …]`), so anything at or below it is inside a map template.
83
+ return path.includes("map");
86
84
  }
87
85
 
88
86
  /**
@@ -173,9 +171,9 @@ function bindableFieldRow(
173
171
  const staticDefault = defaultAsString(defs[defName]);
174
172
  onChange(staticDefault || undefined);
175
173
  } else if (signalDefs.length > 0) {
176
- onChange({ $ref: `#/state/${signalDefs[0][0]}` });
174
+ onChange({ $ref: `#/state/${signalDefs[0]![0]}` });
177
175
  } else if (extraSignals && extraSignals.length > 0) {
178
- onChange({ $ref: extraSignals[0].value });
176
+ onChange({ $ref: extraSignals[0]!.value });
179
177
  }
180
178
  };
181
179
 
@@ -446,9 +444,9 @@ function renderComponentPropsFieldsTemplate(
446
444
  const staticDefault = defaultAsString(defs[defName]);
447
445
  onChange(staticDefault || undefined);
448
446
  } else if (signalDefs.length > 0) {
449
- onChange({ $ref: `#/state/${signalDefs[0][0]}` });
447
+ onChange({ $ref: `#/state/${signalDefs[0]![0]}` });
450
448
  } else if (extraSignals && extraSignals.length > 0) {
451
- onChange({ $ref: extraSignals[0].value });
449
+ onChange({ $ref: extraSignals[0]!.value });
452
450
  }
453
451
  };
454
452
 
@@ -565,7 +563,7 @@ function renderComponentPropsFieldsTemplate(
565
563
 
566
564
  /** Custom attrs fields template */
567
565
  function renderCustomAttrsFieldsTemplate(
568
- node: JxMutableNode,
566
+ _node: JxMutableNode,
569
567
  path: JxPath,
570
568
  attrs: Record<string, unknown>,
571
569
  knownAttrNames: Set<string>,
@@ -809,7 +807,7 @@ function renderPageSection(node: JxMutableNode) {
809
807
  }
810
808
 
811
809
  if (layoutEntries === null) {
812
- loadLayoutEntries();
810
+ void loadLayoutEntries();
813
811
  return nothing;
814
812
  }
815
813
 
@@ -949,10 +947,6 @@ export function renderPropertiesPanelTemplate(ctx: {
949
947
 
950
948
  const path = tab.session.selection;
951
949
  const isMapNode = node.$prototype === "Array";
952
- const isMapParent =
953
- node.children &&
954
- typeof node.children === "object" &&
955
- (node.children as unknown as { $prototype?: string }).$prototype === "Array";
956
950
  const isSwitchNode = Boolean(node.$switch);
957
951
  const isCustomInstance = (node.tagName || "").includes("-");
958
952
  const isRoot = path.length === 0;
@@ -1010,7 +1004,9 @@ export function renderPropertiesPanelTemplate(ctx: {
1010
1004
  const applicableAttrs = {} as Record<string, HtmlMetaEntry>;
1011
1005
  for (const [attr, entry] of Object.entries(htmlMeta.$defs) as [string, HtmlMetaEntry][]) {
1012
1006
  if (!entry.$elements || entry.$elements.includes(tagName)) {
1013
- applicableAttrs[attr] = entry;
1007
+ // The $attr field aliases a $defs key to a different attribute name.
1008
+ // This lets the same attribute (e.g. "name") carry per-element metadata.
1009
+ applicableAttrs[entry.$attr ?? attr] = entry;
1014
1010
  }
1015
1011
  }
1016
1012
 
@@ -1025,7 +1021,7 @@ export function renderPropertiesPanelTemplate(ctx: {
1025
1021
  }
1026
1022
  }
1027
1023
  for (const sec of htmlMeta.$sections) {
1028
- attrSections[sec.key].sort(
1024
+ attrSections[sec.key]!.sort(
1029
1025
  (a: { name: string; entry: HtmlMetaEntry }, b: { name: string; entry: HtmlMetaEntry }) =>
1030
1026
  a.entry.$order - b.entry.$order,
1031
1027
  );
@@ -1196,13 +1192,6 @@ export function renderPropertiesPanelTemplate(ctx: {
1196
1192
  >
1197
1193
  </sp-checkbox>
1198
1194
  </div>
1199
- ${isMapParent
1200
- ? html`
1201
- <div style="font-size:10px;color:var(--fg-dim);padding:4px 0;font-style:italic">
1202
- Children: Repeater (select in layers to configure)
1203
- </div>
1204
- `
1205
- : nothing}
1206
1195
  </div>
1207
1196
  </sp-accordion-item>
1208
1197
  `;
@@ -1283,9 +1272,9 @@ export function renderPropertiesPanelTemplate(ctx: {
1283
1272
  : nothing;
1284
1273
 
1285
1274
  const attrSectionTemplates = htmlMeta.$sections
1286
- .filter((sec) => attrSections[sec.key].length > 0)
1275
+ .filter((sec) => attrSections[sec.key]!.length > 0)
1287
1276
  .map((sec) => {
1288
- const sectionAttrs = attrSections[sec.key];
1277
+ const sectionAttrs = attrSections[sec.key]!;
1289
1278
  const hasAnySet = sectionAttrs.some(
1290
1279
  (a: { name: string; entry: HtmlMetaEntry }) => attrs[a.name] !== undefined,
1291
1280
  );
@@ -82,7 +82,7 @@ function onKeydown(e: KeyboardEvent) {
82
82
  case "Enter": {
83
83
  e.preventDefault();
84
84
  if (items[_selectedIndex]) {
85
- selectItem(items[_selectedIndex]);
85
+ selectItem(items[_selectedIndex]!);
86
86
  }
87
87
  break;
88
88
  }
@@ -101,7 +101,7 @@ function selectItem(item: { path: string; name?: string }) {
101
101
  closeQuickSearch();
102
102
  const { path } = item;
103
103
  trackRecentFile({ name: path.split("/").pop() || "", path });
104
- openFileInTab(path);
104
+ void openFileInTab(path);
105
105
  }
106
106
 
107
107
  function fileIcon(name: string) {
@@ -89,7 +89,6 @@ export function unmount() {
89
89
  _eventsContainer = null;
90
90
  _styleContainer = null;
91
91
  _assistantContainer = null;
92
- _lastTab = null;
93
92
  }
94
93
 
95
94
  /**
@@ -104,7 +103,6 @@ let _propsContainer: HTMLElement | null = null;
104
103
  let _eventsContainer: HTMLElement | null = null;
105
104
  let _styleContainer: HTMLElement | null = null;
106
105
  let _assistantContainer: HTMLElement | null = null;
107
- let _lastTab: string | null = null;
108
106
 
109
107
  function _ensureContainers() {
110
108
  if (_propsContainer) {
@@ -183,9 +181,9 @@ function _doRender() {
183
181
  // Show/hide containers
184
182
  for (let i = 0; i < containers.length; i++) {
185
183
  if (tabKeys[i] === tab) {
186
- containers[i].style.display = tabKeys[i] === "assistant" ? "flex" : "";
184
+ containers[i]!.style.display = tabKeys[i] === "assistant" ? "flex" : "";
187
185
  } else {
188
- containers[i].style.display = "none";
186
+ containers[i]!.style.display = "none";
189
187
  }
190
188
  }
191
189
 
@@ -221,8 +219,6 @@ function _doRender() {
221
219
  } else if (tab === "assistant") {
222
220
  litRender(renderAiPanelTemplate(), _assistantContainer!);
223
221
  }
224
-
225
- _lastTab = tab;
226
222
  } catch (error) {
227
223
  console.error("right-panel render error:", error);
228
224
  }
@@ -82,6 +82,9 @@ export function defaultDef(tag: string): JxMutableNode {
82
82
  def.attributes = { alt: "Image" };
83
83
  } else if (tag === "iframe") {
84
84
  def.attributes = { src: "" };
85
+ } else if (tag === "slot") {
86
+ // Slot children are the fallback content shown when no slotted content is provided.
87
+ def.children = [{ tagName: "p", textContent: "Slot content" }];
85
88
  } else if (tag === "select") {
86
89
  def.children = [{ tagName: "option", textContent: "Option 1" }];
87
90
  } else if (tag === "ul" || tag === "ol") {