@jxsuite/studio 0.19.0 → 0.20.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 (93) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +12577 -12183
  3. package/dist/studio.js.map +146 -144
  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 +17 -6
  8. package/src/browse/browse.js +25 -19
  9. package/src/canvas/canvas-diff.js +24 -16
  10. package/src/canvas/canvas-helpers.js +24 -19
  11. package/src/canvas/canvas-live-render.js +94 -39
  12. package/src/canvas/canvas-render.js +119 -73
  13. package/src/canvas/canvas-utils.js +38 -15
  14. package/src/canvas/nested-site-style.js +50 -0
  15. package/src/editor/component-inline-edit.js +14 -13
  16. package/src/editor/content-inline-edit.js +22 -10
  17. package/src/editor/context-menu.js +1 -1
  18. package/src/editor/convert-to-component.js +12 -10
  19. package/src/editor/inline-edit.js +66 -28
  20. package/src/editor/insertion-helper.js +16 -12
  21. package/src/editor/shortcuts.js +4 -2
  22. package/src/editor/slash-menu.js +6 -4
  23. package/src/files/components.js +1 -1
  24. package/src/files/file-ops.js +37 -16
  25. package/src/files/files.js +78 -52
  26. package/src/github/github-auth.js +122 -0
  27. package/src/github/github-publish.js +115 -0
  28. package/src/markdown/md-convert.js +172 -107
  29. package/src/new-project/new-project-modal.js +204 -0
  30. package/src/panels/activity-bar.js +22 -20
  31. package/src/panels/ai-panel.js +399 -0
  32. package/src/panels/block-action-bar.js +5 -5
  33. package/src/panels/canvas-dnd.js +8 -2
  34. package/src/panels/data-explorer.js +19 -13
  35. package/src/panels/dnd.js +144 -92
  36. package/src/panels/editors.js +55 -35
  37. package/src/panels/elements-panel.js +33 -19
  38. package/src/panels/events-panel.js +15 -10
  39. package/src/panels/git-panel.js +469 -98
  40. package/src/panels/head-panel.js +121 -81
  41. package/src/panels/imports-panel.js +88 -64
  42. package/src/panels/layers-panel.js +64 -43
  43. package/src/panels/left-panel.js +85 -47
  44. package/src/panels/overlays.js +1 -0
  45. package/src/panels/panel-events.js +45 -30
  46. package/src/panels/preview-render.js +23 -18
  47. package/src/panels/properties-panel.js +365 -249
  48. package/src/panels/pseudo-preview.js +2 -2
  49. package/src/panels/quick-search.js +7 -5
  50. package/src/panels/right-panel.js +26 -2
  51. package/src/panels/shared.js +4 -4
  52. package/src/panels/signals-panel.js +295 -166
  53. package/src/panels/statusbar.js +4 -4
  54. package/src/panels/style-inputs.js +67 -35
  55. package/src/panels/style-panel.js +233 -132
  56. package/src/panels/style-utils.js +34 -16
  57. package/src/panels/stylebook-layers-panel.js +12 -10
  58. package/src/panels/stylebook-panel.js +134 -66
  59. package/src/panels/toolbar.js +333 -109
  60. package/src/panels/welcome-screen.js +121 -0
  61. package/src/platform.js +2 -4
  62. package/src/platforms/devserver.js +113 -7
  63. package/src/resize-edges.js +98 -0
  64. package/src/services/cem-export.js +12 -7
  65. package/src/services/code-services.js +30 -12
  66. package/src/settings/content-types-editor.js +7 -7
  67. package/src/settings/css-vars-editor.js +30 -24
  68. package/src/settings/defs-editor.js +12 -7
  69. package/src/settings/general-settings.js +85 -3
  70. package/src/settings/head-editor.js +30 -24
  71. package/src/settings/schema-field-ui.js +58 -39
  72. package/src/site-context.js +41 -31
  73. package/src/state.js +70 -29
  74. package/src/store.js +21 -26
  75. package/src/studio.js +103 -81
  76. package/src/tabs/tab.js +67 -43
  77. package/src/tabs/transact.js +23 -13
  78. package/src/ui/button-group.js +24 -24
  79. package/src/ui/color-selector.js +28 -23
  80. package/src/ui/field-row.js +3 -3
  81. package/src/ui/icons.js +2 -2
  82. package/src/ui/media-picker.js +7 -7
  83. package/src/ui/spectrum.js +5 -1
  84. package/src/ui/unit-selector.js +18 -16
  85. package/src/ui/value-selector.js +21 -15
  86. package/src/ui/widgets.js +22 -19
  87. package/src/utils/canvas-media.js +5 -4
  88. package/src/utils/edit-display.js +31 -20
  89. package/src/utils/google-fonts.js +11 -11
  90. package/src/utils/inherited-style.js +9 -8
  91. package/src/utils/studio-utils.js +7 -7
  92. package/src/view.js +58 -1
  93. package/src/workspace/workspace.js +10 -7
@@ -13,10 +13,23 @@ import { projectState } from "../store.js";
13
13
  import { updateSiteConfig } from "../site-context.js";
14
14
  import { getPlatform } from "../platform.js";
15
15
 
16
+ /** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
17
+
18
+ /** @typedef {string | JxMutableNode | { $ref: string }} ElementsEntry */
19
+
20
+ /**
21
+ * @typedef {{
22
+ * renderLeftPanel: () => void;
23
+ * documentPath: string | null;
24
+ * documentElements: ElementsEntry[];
25
+ * applyMutation: (fn: (doc: JxMutableNode) => void) => void;
26
+ * }} ImportsContext
27
+ */
28
+
16
29
  /**
17
30
  * Build the subpath specifier for a component: `<package>/<modulePath>`
18
31
  *
19
- * @param {any} comp
32
+ * @param {ComponentEntry} comp
20
33
  * @returns {string}
21
34
  */
22
35
  function componentSpecifier(comp) {
@@ -27,8 +40,8 @@ function componentSpecifier(comp) {
27
40
  * Check if a component is enabled (present in $elements array). Supports both cherry-picked subpath
28
41
  * specifiers and legacy full-package imports.
29
42
  *
30
- * @param {any} comp
31
- * @param {any[]} elements
43
+ * @param {ComponentEntry} comp
44
+ * @param {ElementsEntry[]} elements
32
45
  * @returns {boolean}
33
46
  */
34
47
  function isComponentEnabled(comp, elements) {
@@ -47,10 +60,10 @@ function isComponentEnabled(comp, elements) {
47
60
  /**
48
61
  * Group npm components by package name.
49
62
  *
50
- * @returns {Map<string, any[]>}
63
+ * @returns {Map<string, ComponentEntry[]>}
51
64
  */
52
65
  function groupByPackage() {
53
- /** @type {Map<string, any[]>} */
66
+ /** @type {Map<string, ComponentEntry[]>} */
54
67
  const groups = new Map();
55
68
  for (const comp of componentRegistry) {
56
69
  if (comp.source !== "npm" || !comp.package || !comp.modulePath) continue;
@@ -61,13 +74,8 @@ function groupByPackage() {
61
74
  }
62
75
 
63
76
  /**
64
- * @param {{
65
- * renderLeftPanel: () => void;
66
- * documentPath: string | null;
67
- * documentElements: any[];
68
- * applyMutation: (fn: (doc: any) => void) => void;
69
- * }} ctx
70
- * @returns {any}
77
+ * @param {ImportsContext} ctx
78
+ * @returns {import("lit-html").TemplateResult}
71
79
  */
72
80
  export function renderImportsTemplate({
73
81
  renderLeftPanel,
@@ -95,7 +103,9 @@ export function renderImportsTemplate({
95
103
  function renderSiteLevelImports(renderLeftPanel) {
96
104
  const siteImports = projectState?.projectConfig?.imports || {};
97
105
  const entries = Object.entries(siteImports);
98
- const siteElements = projectState?.projectConfig?.$elements || [];
106
+ const siteElements = /** @type {ElementsEntry[]} */ (
107
+ projectState?.projectConfig?.$elements || []
108
+ );
99
109
 
100
110
  const packageGroups = groupByPackage();
101
111
 
@@ -141,10 +151,14 @@ function renderSiteLevelImports(renderLeftPanel) {
141
151
  quiet
142
152
  size="xs"
143
153
  title="Add import"
144
- @click=${async (/** @type {any} */ e) => {
145
- const form = e.target.closest(".import-add-form");
146
- const nameField = form?.querySelector(".import-add-name");
147
- const pathField = form?.querySelector(".import-add-path");
154
+ @click=${async (/** @type {Event} */ e) => {
155
+ const form = /** @type {HTMLElement} */ (e.target).closest(".import-add-form");
156
+ const nameField = /** @type {HTMLInputElement} */ (
157
+ form?.querySelector(".import-add-name")
158
+ );
159
+ const pathField = /** @type {HTMLInputElement} */ (
160
+ form?.querySelector(".import-add-path")
161
+ );
148
162
  const name = nameField?.value?.trim();
149
163
  const path = pathField?.value?.trim();
150
164
  if (!name || !path) return;
@@ -177,13 +191,16 @@ function renderSiteLevelImports(renderLeftPanel) {
177
191
  await platform.removePackage(pkg);
178
192
  // Also remove all cherry-picked elements for this package
179
193
  const updatedElements = siteElements.filter(
180
- (/** @type {any} */ e) => typeof e !== "string" || !e.startsWith(pkg + "/"),
194
+ (/** @type {ElementsEntry} */ e) =>
195
+ typeof e !== "string" || !e.startsWith(pkg + "/"),
181
196
  );
182
197
  const { loadComponentRegistry } = await import("../files/components.js");
183
198
  await loadComponentRegistry();
184
- await updateSiteConfig({ $elements: updatedElements });
199
+ await updateSiteConfig({
200
+ $elements: /** @type {(string | JxElement)[]} */ (updatedElements),
201
+ });
185
202
  renderLeftPanel();
186
- } catch (/** @type {any} */ e) {
203
+ } catch (/** @type {unknown} */ e) {
187
204
  console.error("Failed to remove package:", e);
188
205
  }
189
206
  }}
@@ -192,7 +209,7 @@ function renderSiteLevelImports(renderLeftPanel) {
192
209
  </sp-action-button>
193
210
  </div>
194
211
  <div class="imports-list imports-component-list">
195
- ${comps.map((/** @type {any} */ comp) => {
212
+ ${comps.map((/** @type {ComponentEntry} */ comp) => {
196
213
  const enabled = isComponentEnabled(comp, siteElements);
197
214
  const specifier = componentSpecifier(comp);
198
215
  return html`
@@ -200,16 +217,20 @@ function renderSiteLevelImports(renderLeftPanel) {
200
217
  <sp-checkbox
201
218
  size="s"
202
219
  .checked=${enabled}
203
- @change=${async (/** @type {any} */ e) => {
220
+ @change=${async (/** @type {Event} */ e) => {
204
221
  let updated = [...siteElements];
205
222
  // Remove legacy full-package import if present
206
- updated = updated.filter((/** @type {any} */ el) => el !== pkg);
207
- if (e.target.checked) {
223
+ updated = updated.filter((/** @type {ElementsEntry} */ el) => el !== pkg);
224
+ if (/** @type {HTMLInputElement} */ (e.target).checked) {
208
225
  if (!updated.includes(specifier)) updated.push(specifier);
209
226
  } else {
210
- updated = updated.filter((/** @type {any} */ el) => el !== specifier);
227
+ updated = updated.filter(
228
+ (/** @type {ElementsEntry} */ el) => el !== specifier,
229
+ );
211
230
  }
212
- await updateSiteConfig({ $elements: updated });
231
+ await updateSiteConfig({
232
+ $elements: /** @type {(string | JxElement)[]} */ (updated),
233
+ });
213
234
  renderLeftPanel();
214
235
  }}
215
236
  >
@@ -233,18 +254,18 @@ function renderSiteLevelImports(renderLeftPanel) {
233
254
  placeholder="Package name…"
234
255
  size="s"
235
256
  style="flex:1"
236
- @keydown=${async (/** @type {any} */ e) => {
257
+ @keydown=${async (/** @type {KeyboardEvent} */ e) => {
237
258
  if (e.key !== "Enter") return;
238
- const name = e.target.value?.trim();
259
+ const name = /** @type {HTMLInputElement} */ (e.target).value?.trim();
239
260
  if (!name) return;
240
- e.target.value = "";
261
+ /** @type {HTMLInputElement} */ (e.target).value = "";
241
262
  try {
242
263
  const platform = getPlatform();
243
264
  await platform.addPackage(name);
244
265
  const { loadComponentRegistry } = await import("../files/components.js");
245
266
  await loadComponentRegistry();
246
267
  renderLeftPanel();
247
- } catch (/** @type {any} */ err) {
268
+ } catch (/** @type {unknown} */ err) {
248
269
  console.error("Failed to add package:", err);
249
270
  }
250
271
  }}
@@ -253,18 +274,20 @@ function renderSiteLevelImports(renderLeftPanel) {
253
274
  quiet
254
275
  size="xs"
255
276
  title="Add package"
256
- @click=${async (/** @type {any} */ e) => {
257
- const input = e.target.closest(".import-add-form")?.querySelector("sp-textfield");
258
- const name = input?.value?.trim();
277
+ @click=${async (/** @type {Event} */ e) => {
278
+ const input = /** @type {HTMLElement} */ (e.target)
279
+ .closest(".import-add-form")
280
+ ?.querySelector("sp-textfield");
281
+ const name = /** @type {HTMLInputElement | null} */ (input)?.value?.trim();
259
282
  if (!name) return;
260
- input.value = "";
283
+ /** @type {HTMLInputElement} */ (input).value = "";
261
284
  try {
262
285
  const platform = getPlatform();
263
286
  await platform.addPackage(name);
264
287
  const { loadComponentRegistry } = await import("../files/components.js");
265
288
  await loadComponentRegistry();
266
289
  renderLeftPanel();
267
- } catch (/** @type {any} */ err) {
290
+ } catch (/** @type {unknown} */ err) {
268
291
  console.error("Failed to add package:", err);
269
292
  }
270
293
  }}
@@ -279,14 +302,7 @@ function renderSiteLevelImports(renderLeftPanel) {
279
302
 
280
303
  // ─── Document-level: Component Imports + npm Component Cherry-pick ───────────
281
304
 
282
- /**
283
- * @param {{
284
- * renderLeftPanel: () => void;
285
- * documentPath: string | null;
286
- * documentElements: any[];
287
- * applyMutation: (fn: (doc: any) => void) => void;
288
- * }} ctx
289
- */
305
+ /** @param {ImportsContext} ctx */
290
306
  function renderDocumentLevelImports({
291
307
  renderLeftPanel,
292
308
  documentPath,
@@ -294,14 +310,18 @@ function renderDocumentLevelImports({
294
310
  applyMutation,
295
311
  }) {
296
312
  const refEntries = documentElements.filter(
297
- (/** @type {any} */ e) => e && typeof e === "object" && e.$ref,
313
+ (/** @type {ElementsEntry} */ e) => e && typeof e === "object" && e.$ref,
314
+ );
315
+ const npmEntries = documentElements.filter(
316
+ (/** @type {ElementsEntry} */ e) => typeof e === "string",
298
317
  );
299
- const npmEntries = documentElements.filter((/** @type {any} */ e) => typeof e === "string");
300
318
 
301
319
  // Available JX components not yet imported
302
- const importedRefs = new Set(refEntries.map((/** @type {any} */ e) => e.$ref));
320
+ const importedRefs = new Set(
321
+ refEntries.map((/** @type {ElementsEntry} */ e) => /** @type {{ $ref: string }} */ (e).$ref),
322
+ );
303
323
  const availableComponents = componentRegistry.filter(
304
- (/** @type {any} */ c) =>
324
+ (/** @type {ComponentEntry} */ c) =>
305
325
  c.source !== "npm" && !importedRefs.has(`./${c.path}`) && !importedRefs.has(c.path),
306
326
  );
307
327
 
@@ -309,9 +329,9 @@ function renderDocumentLevelImports({
309
329
 
310
330
  /** @param {string} ref */
311
331
  const removeRef = (ref) => {
312
- applyMutation((/** @type {any} */ doc) => {
332
+ applyMutation((/** @type {JxMutableNode} */ doc) => {
313
333
  doc.$elements = (doc.$elements || []).filter(
314
- (/** @type {any} */ e) => !(e && typeof e === "object" && e.$ref === ref),
334
+ (/** @type {ElementsEntry} */ e) => !(e && typeof e === "object" && e.$ref === ref),
315
335
  );
316
336
  });
317
337
  renderLeftPanel();
@@ -329,14 +349,18 @@ function renderDocumentLevelImports({
329
349
  ? html`
330
350
  <div class="imports-list">
331
351
  ${refEntries.map(
332
- (/** @type {any} */ entry) => html`
352
+ (/** @type {ElementsEntry} */ entry) => html`
333
353
  <div class="import-row">
334
- <span class="import-path" title=${entry.$ref}>${entry.$ref}</span>
354
+ <span
355
+ class="import-path"
356
+ title=${/** @type {{ $ref: string }} */ (entry).$ref}
357
+ >${/** @type {{ $ref: string }} */ (entry).$ref}</span
358
+ >
335
359
  <sp-action-button
336
360
  quiet
337
361
  size="xs"
338
362
  title="Remove"
339
- @click=${() => removeRef(entry.$ref)}
363
+ @click=${() => removeRef(/** @type {{ $ref: string }} */ (entry).$ref)}
340
364
  >
341
365
  <sp-icon-close slot="icon" size="xs"></sp-icon-close>
342
366
  </sp-action-button>
@@ -353,16 +377,16 @@ function renderDocumentLevelImports({
353
377
  size="s"
354
378
  label="Add component…"
355
379
  class="import-picker"
356
- @change=${(/** @type {any} */ e) => {
357
- const tag = e.target.value;
380
+ @change=${(/** @type {Event} */ e) => {
381
+ const tag = /** @type {HTMLInputElement} */ (e.target).value;
358
382
  if (!tag) return;
359
- e.target.value = "";
383
+ /** @type {HTMLInputElement} */ (e.target).value = "";
360
384
  const comp = componentRegistry.find(
361
- (/** @type {any} */ c) => c.tagName === tag,
385
+ (/** @type {ComponentEntry} */ c) => c.tagName === tag,
362
386
  );
363
387
  if (!comp) return;
364
388
  const relPath = computeRelativePath(documentPath, comp.path);
365
- applyMutation((/** @type {any} */ doc) => {
389
+ applyMutation((/** @type {JxMutableNode} */ doc) => {
366
390
  if (!doc.$elements) doc.$elements = [];
367
391
  doc.$elements.push({ $ref: relPath });
368
392
  });
@@ -370,7 +394,7 @@ function renderDocumentLevelImports({
370
394
  }}
371
395
  >
372
396
  ${availableComponents.map(
373
- (/** @type {any} */ c) =>
397
+ (/** @type {ComponentEntry} */ c) =>
374
398
  html`<sp-menu-item value=${c.tagName}>&lt;${c.tagName}&gt;</sp-menu-item>`,
375
399
  )}
376
400
  </sp-picker>
@@ -387,7 +411,7 @@ function renderDocumentLevelImports({
387
411
  <span class="imports-section-title import-mono">${pkg}</span>
388
412
  </div>
389
413
  <div class="imports-list imports-component-list">
390
- ${comps.map((/** @type {any} */ comp) => {
414
+ ${comps.map((/** @type {ComponentEntry} */ comp) => {
391
415
  const enabled = isComponentEnabled(comp, npmEntries);
392
416
  const specifier = componentSpecifier(comp);
393
417
  return html`
@@ -395,18 +419,18 @@ function renderDocumentLevelImports({
395
419
  <sp-checkbox
396
420
  size="s"
397
421
  .checked=${enabled}
398
- @change=${(/** @type {any} */ e) => {
399
- applyMutation((/** @type {any} */ doc) => {
422
+ @change=${(/** @type {Event} */ e) => {
423
+ applyMutation((/** @type {JxMutableNode} */ doc) => {
400
424
  if (!doc.$elements) doc.$elements = [];
401
425
  // Remove legacy full-package import if present
402
426
  doc.$elements = doc.$elements.filter(
403
- (/** @type {any} */ el) => el !== pkg,
427
+ (/** @type {ElementsEntry} */ el) => el !== pkg,
404
428
  );
405
- if (e.target.checked) {
429
+ if (/** @type {HTMLInputElement} */ (e.target).checked) {
406
430
  if (!doc.$elements.includes(specifier)) doc.$elements.push(specifier);
407
431
  } else {
408
432
  doc.$elements = doc.$elements.filter(
409
- (/** @type {any} */ el) => el !== specifier,
433
+ (/** @type {ElementsEntry} */ el) => el !== specifier,
410
434
  );
411
435
  }
412
436
  });
@@ -31,7 +31,7 @@ import { panToElement } from "../canvas/canvas-utils.js";
31
31
  /**
32
32
  * Start inline title editing on a layer row.
33
33
  *
34
- * @param {any[]} path
34
+ * @param {JxPath} path
35
35
  * @param {() => void} rerender
36
36
  */
37
37
  export function startLayerTitleEdit(path, rerender) {
@@ -87,7 +87,7 @@ export function startLayerTitleEdit(path, rerender) {
87
87
  }
88
88
 
89
89
  /**
90
- * @param {{ navigateToComponent: any; rerender: () => void }} ctx
90
+ * @param {{ navigateToComponent: (path: string) => void; rerender: () => void }} ctx
91
91
  * @returns {import("lit-html").TemplateResult}
92
92
  */
93
93
  export function renderLayersTemplate(ctx) {
@@ -99,7 +99,7 @@ export function renderLayersTemplate(ctx) {
99
99
  const rows = flattenTree(tab?.doc.document);
100
100
  const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
101
101
 
102
- /** @type {any[]} */
102
+ /** @type {import("lit-html").TemplateResult[]} */
103
103
  const layerRows = [];
104
104
  for (const { node, path, depth, nodeType } of rows) {
105
105
  let hidden = false;
@@ -128,28 +128,40 @@ export function renderLayersTemplate(ctx) {
128
128
  continue;
129
129
  }
130
130
 
131
+ // After text-node skip, node is guaranteed to be JxMutableNode (not a primitive)
132
+ if (typeof node !== "object" || node === null) continue;
133
+ /** @type {JxMutableNode} */
134
+ const jxNode = /** @type {JxMutableNode} */ (node);
135
+
131
136
  if (path.length >= 2 && nodeType === "element") {
132
137
  const pPath = parentElementPath(path);
133
138
  const parentNode = pPath ? getNodeAtPath(tab?.doc.document, pPath) : null;
134
- if (parentNode && isInlineElement(node, parentNode)) continue;
139
+ if (parentNode && isInlineElement(jxNode, parentNode)) continue;
135
140
  }
136
141
 
137
142
  const key = pathKey(path);
138
143
  const isSelected = pathsEqual(path, tab?.session.selection);
139
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
144
+ const hasChildren = Array.isArray(jxNode.children) && jxNode.children.length > 0;
140
145
  const hasMapChildren =
141
- node.children && typeof node.children === "object" && node.children.$prototype === "Array";
146
+ jxNode.children &&
147
+ typeof jxNode.children === "object" &&
148
+ /** @type {{ $prototype?: string }} */ (/** @type {unknown} */ (jxNode.children))
149
+ .$prototype === "Array";
142
150
  const hasCases =
143
- node.$switch &&
144
- node.cases &&
145
- typeof node.cases === "object" &&
146
- Object.keys(node.cases).length > 0;
151
+ jxNode.$switch &&
152
+ jxNode.cases &&
153
+ typeof jxNode.cases === "object" &&
154
+ Object.keys(jxNode.cases).length > 0;
147
155
  const isExpandable =
148
- hasChildren || hasMapChildren || hasCases || (nodeType === "map" && node.map);
149
- const isVoidEl = VOID_ELEMENTS.has((node.tagName || "div").toLowerCase());
156
+ hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
157
+ const isVoidEl = VOID_ELEMENTS.has((jxNode.tagName || "div").toLowerCase());
150
158
 
151
- /** @type {any} */
152
- let badgeClass, badgeText, badgeTitle;
159
+ /** @type {string} */
160
+ let badgeClass;
161
+ /** @type {string | number} */
162
+ let badgeText;
163
+ /** @type {string | undefined} */
164
+ let badgeTitle;
153
165
  if (nodeType === "map") {
154
166
  badgeClass = "layer-tag map-tag";
155
167
  badgeText = "↻";
@@ -158,41 +170,48 @@ export function renderLayersTemplate(ctx) {
158
170
  badgeClass = "layer-tag case-tag";
159
171
  badgeText = path[path.length - 1];
160
172
  badgeTitle = `$switch case: ${path[path.length - 1]}`;
161
- } else if (node.$switch) {
173
+ } else if (jxNode.$switch) {
162
174
  badgeClass = "layer-tag switch-tag";
163
175
  badgeText = "⇄";
164
176
  badgeTitle = "$switch";
165
177
  } else {
166
178
  badgeClass = "layer-tag";
167
- badgeText = node.tagName || "div";
179
+ badgeText = jxNode.tagName || "div";
168
180
  badgeTitle = undefined;
169
181
  }
170
182
 
171
- /** @type {any} */
172
- let labelText, labelItalic;
183
+ /** @type {string} */
184
+ let labelText;
185
+ /** @type {boolean} */
186
+ let labelItalic;
173
187
  if (nodeType === "case-ref") {
174
- labelText = node.$ref || "external";
188
+ labelText = jxNode.$ref || "external";
175
189
  labelItalic = true;
176
190
  } else {
177
- labelText = nodeLabel(node);
191
+ labelText = nodeLabel(jxNode);
178
192
  labelItalic = false;
179
193
  }
180
194
 
181
195
  const isElement = nodeType === "element";
182
196
  const isRoot = tab?.doc.mode === "content" ? path.length === 0 : path.length < 2;
183
197
  const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
184
- const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
198
+ const parentPath =
199
+ isElement && !isRoot ? /** @type {JxPath} */ (parentElementPath(path)) : null;
185
200
  const parentNode = parentPath ? getNodeAtPath(tab?.doc.document, parentPath) : null;
186
201
  const siblingCount = parentNode?.children?.length || 0;
187
202
  const canMoveUp = isElement && !isRoot && idx > 0;
188
203
  const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
189
- const prevSibling = canMoveUp && parentNode ? parentNode.children[idx - 1] : null;
204
+ const prevSibling = canMoveUp && parentNode ? parentNode.children?.[idx - 1] : null;
190
205
  const prevIsContainer = (() => {
191
206
  if (!prevSibling || typeof prevSibling !== "object") return false;
192
207
  if (VOID_ELEMENTS.has((prevSibling.tagName || "div").toLowerCase())) return false;
193
208
  const ch = prevSibling.children;
194
209
  if (!ch) return false;
195
- if (typeof ch === "object" && ch.$prototype === "Array") return true;
210
+ if (
211
+ typeof ch === "object" &&
212
+ /** @type {{ $prototype?: string }} */ (/** @type {unknown} */ (ch)).$prototype === "Array"
213
+ )
214
+ return true;
196
215
  if (!Array.isArray(ch)) return false;
197
216
  if (ch.length === 0) return true;
198
217
  return ch.some(
@@ -202,7 +221,7 @@ export function renderLayersTemplate(ctx) {
202
221
  const canMoveIn = isElement && !isRoot && prevIsContainer;
203
222
  const grandparentPath =
204
223
  isElement && parentPath && parentPath.length >= 2
205
- ? /** @type {any} */ (parentElementPath(parentPath))
224
+ ? /** @type {JxPath} */ (parentElementPath(parentPath))
206
225
  : null;
207
226
  const canMoveOut = isElement && !isRoot && !!grandparentPath;
208
227
 
@@ -219,13 +238,13 @@ export function renderLayersTemplate(ctx) {
219
238
  panToElement(path);
220
239
  }}
221
240
  @dblclick=${isElement
222
- ? (/** @type {any} */ e) => {
241
+ ? (/** @type {MouseEvent} */ e) => {
223
242
  e.stopPropagation();
224
243
  startLayerTitleEdit(path, ctx.rerender);
225
244
  }
226
245
  : nothing}
227
246
  @contextmenu=${isElement
228
- ? (/** @type {any} */ e) =>
247
+ ? (/** @type {MouseEvent} */ e) =>
229
248
  showContextMenu(e, path, {
230
249
  onEditComponent: ctx.navigateToComponent,
231
250
  rerender: ctx.rerender,
@@ -255,12 +274,11 @@ export function renderLayersTemplate(ctx) {
255
274
  quiet
256
275
  size="xs"
257
276
  title="Move up"
258
- @click=${(/** @type {any} */ e) => {
277
+ @click=${(/** @type {MouseEvent} */ e) => {
259
278
  e.stopPropagation();
260
279
  /** @type {HTMLElement} */ (e.currentTarget).blur();
261
- transactDoc(activeTab.value, (t) =>
262
- mutateMoveNode(t, path, parentPath, idx - 1),
263
- );
280
+ const pp = /** @type {JxPath} */ (parentPath);
281
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, path, pp, idx - 1));
264
282
  }}
265
283
  >
266
284
  <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
@@ -271,12 +289,11 @@ export function renderLayersTemplate(ctx) {
271
289
  quiet
272
290
  size="xs"
273
291
  title="Move down"
274
- @click=${(/** @type {any} */ e) => {
292
+ @click=${(/** @type {MouseEvent} */ e) => {
275
293
  e.stopPropagation();
276
294
  /** @type {HTMLElement} */ (e.currentTarget).blur();
277
- transactDoc(activeTab.value, (t) =>
278
- mutateMoveNode(t, path, parentPath, idx + 2),
279
- );
295
+ const pp = /** @type {JxPath} */ (parentPath);
296
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, path, pp, idx + 2));
280
297
  }}
281
298
  >
282
299
  <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
@@ -287,10 +304,11 @@ export function renderLayersTemplate(ctx) {
287
304
  quiet
288
305
  size="xs"
289
306
  title="Move into previous sibling"
290
- @click=${(/** @type {any} */ e) => {
307
+ @click=${(/** @type {MouseEvent} */ e) => {
291
308
  e.stopPropagation();
292
309
  /** @type {HTMLElement} */ (e.currentTarget).blur();
293
- const prevPath = [...parentPath, idx - 1];
310
+ const pp = /** @type {JxPath} */ (parentPath);
311
+ const prevPath = [...pp, idx - 1];
294
312
  const prev = getNodeAtPath(activeTab.value?.doc.document, prevPath);
295
313
  const len = prev?.children?.length || 0;
296
314
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, path, prevPath, len));
@@ -304,12 +322,15 @@ export function renderLayersTemplate(ctx) {
304
322
  quiet
305
323
  size="xs"
306
324
  title="Move out of parent"
307
- @click=${(/** @type {any} */ e) => {
325
+ @click=${(/** @type {MouseEvent} */ e) => {
308
326
  e.stopPropagation();
309
327
  /** @type {HTMLElement} */ (e.currentTarget).blur();
310
- const parentIdx = /** @type {number} */ (childIndex(parentPath));
328
+ const gp = /** @type {JxPath} */ (grandparentPath);
329
+ const parentIdx = /** @type {number} */ (
330
+ childIndex(/** @type {JxPath} */ (parentPath))
331
+ );
311
332
  transactDoc(activeTab.value, (t) =>
312
- mutateMoveNode(t, path, grandparentPath, parentIdx + 1),
333
+ mutateMoveNode(t, path, gp, parentIdx + 1),
313
334
  );
314
335
  }}
315
336
  >
@@ -321,7 +342,7 @@ export function renderLayersTemplate(ctx) {
321
342
  size="xs"
322
343
  class="layer-delete"
323
344
  title="Delete"
324
- @click=${(/** @type {any} */ e) => {
345
+ @click=${(/** @type {MouseEvent} */ e) => {
325
346
  e.stopPropagation();
326
347
  transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path));
327
348
  }}
@@ -339,13 +360,13 @@ export function renderLayersTemplate(ctx) {
339
360
  <div class="layers-container" style="position:relative">
340
361
  <div
341
362
  class="layers-tree"
342
- @click=${(/** @type {any} */ e) => {
343
- const toggle = e.target.closest(".layer-toggle");
363
+ @click=${(/** @type {MouseEvent} */ e) => {
364
+ const toggle = /** @type {HTMLElement} */ (e.target).closest(".layer-toggle");
344
365
  if (!toggle) return;
345
366
  e.stopPropagation();
346
367
  const row = toggle.closest(".layer-row");
347
368
  if (!row) return;
348
- const key = row.dataset.path;
369
+ const key = /** @type {HTMLElement} */ (row).dataset.path;
349
370
  if (!key) return;
350
371
  if (collapsed.has(key)) collapsed.delete(key);
351
372
  else collapsed.add(key);