@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "provenance": true
23
23
  },
24
24
  "scripts": {
25
- "build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build ../../node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ../../node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ../../node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
25
+ "build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build ./node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
26
26
  "gen:webdata": "bun run scripts/gen-webdata.js",
27
27
  "test": "bun test",
28
28
  "upgrade": "bunx npm-check-updates -u && bun install"
@@ -30,11 +30,15 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.18.0",
33
+ "@jxsuite/parser": "^0.20.0",
34
+ "@jxsuite/runtime": "^0.20.0",
35
+ "@jxsuite/schema": "^0.20.0",
34
36
  "@spectrum-web-components/accordion": "^1.12.1",
35
37
  "@spectrum-web-components/action-bar": "1.12.1",
36
38
  "@spectrum-web-components/action-button": "^1.12.1",
37
39
  "@spectrum-web-components/action-group": "^1.12.1",
40
+ "@spectrum-web-components/base": "^1.12.1",
41
+ "@spectrum-web-components/button": "^1.12.1",
38
42
  "@spectrum-web-components/checkbox": "^1.12.1",
39
43
  "@spectrum-web-components/color-area": "^1.12.1",
40
44
  "@spectrum-web-components/color-handle": "^1.12.1",
@@ -42,7 +46,10 @@
42
46
  "@spectrum-web-components/combobox": "^1.12.1",
43
47
  "@spectrum-web-components/dialog": "^1.12.1",
44
48
  "@spectrum-web-components/divider": "^1.12.1",
49
+ "@spectrum-web-components/field-label": "^1.12.1",
50
+ "@spectrum-web-components/help-text": "^1.12.1",
45
51
  "@spectrum-web-components/icon": "^1.12.1",
52
+ "@spectrum-web-components/icons-ui": "^1.12.1",
46
53
  "@spectrum-web-components/icons-workflow": "^1.12.1",
47
54
  "@spectrum-web-components/menu": "^1.12.1",
48
55
  "@spectrum-web-components/number-field": "^1.12.1",
@@ -60,9 +67,12 @@
60
67
  "@spectrum-web-components/theme": "^1.12.1",
61
68
  "@spectrum-web-components/toast": "^1.12.1",
62
69
  "@spectrum-web-components/tooltip": "^1.12.1",
63
- "@vue/reactivity": "3.5.34",
70
+ "@spectrum-web-components/underlay": "^1.12.1",
71
+ "@vue/reactivity": "3.5.35",
72
+ "lit": "^3.3.3",
64
73
  "lit-html": "^3.3.3",
65
74
  "monaco-editor": "^0.55.1",
75
+ "quikchat": "^1.2.7",
66
76
  "remark-directive": "^4.0.0",
67
77
  "remark-frontmatter": "^5.0.0",
68
78
  "remark-gfm": "^4.0.1",
@@ -73,9 +83,10 @@
73
83
  },
74
84
  "devDependencies": {
75
85
  "@happy-dom/global-registrator": "^20.9.0",
76
- "@webref/css": "^8.5.6",
86
+ "@types/mdast": "^4.0.4",
87
+ "@webref/css": "^8.5.7",
77
88
  "@webref/elements": "^2.7.1",
78
- "@webref/idl": "^3.78.0"
89
+ "@webref/idl": "^3.79.0"
79
90
  },
80
91
  "//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
81
92
  }
@@ -15,6 +15,8 @@ import { yamlDefault } from "../settings/schema-field-ui.js";
15
15
  import { invalidateMediaCache } from "../ui/media-picker.js";
16
16
  import { statusMessage } from "../panels/statusbar.js";
17
17
  import { componentRegistry } from "../files/components.js";
18
+
19
+ /** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
18
20
  import { showDialog, renderPopover } from "../ui/layers.js";
19
21
  import { renderComponentPreview } from "../panels/stylebook-panel.js";
20
22
  import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
@@ -150,7 +152,7 @@ function contentTypeFor(filePath) {
150
152
  const config = projectState?.projectConfig;
151
153
  if (!config?.contentTypes) return null;
152
154
  for (const [name, def] of Object.entries(config.contentTypes)) {
153
- const d = /** @type {any} */ (def);
155
+ const d = /** @type {ContentTypeDef} */ (def);
154
156
  if (!d.source) continue;
155
157
  const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
156
158
  if (filePath.startsWith(prefix + "/") || filePath === prefix) {
@@ -215,8 +217,8 @@ function buildFrontmatterYaml(contentTypeName) {
215
217
 
216
218
  let yaml = "";
217
219
  for (const [field, def] of Object.entries(col.schema.properties)) {
218
- const d = /** @type {any} */ (def);
219
- yaml += `${field}: ${yamlDefault(d.type, d.format)}\n`;
220
+ const d = /** @type {{ type?: string; format?: string }} */ (def);
221
+ yaml += `${field}: ${yamlDefault(d.type || "", d.format || "")}\n`;
220
222
  }
221
223
  return yaml || "title: Untitled\n";
222
224
  }
@@ -230,7 +232,7 @@ function getContentTypeTypes() {
230
232
  const config = projectState?.projectConfig;
231
233
  if (!config?.contentTypes) return [];
232
234
  return Object.entries(config.contentTypes).map(([name, def]) => {
233
- const d = /** @type {any} */ (def);
235
+ const d = /** @type {ContentTypeDef} */ (def);
234
236
  const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
235
237
  return {
236
238
  key: `contentType:${name}`,
@@ -415,8 +417,8 @@ async function browseRenameFile(file, container, ctx) {
415
417
  invalidateBrowseCache();
416
418
  renderBrowse(container, ctx);
417
419
  statusMessage(`Renamed to ${newName}`);
418
- } catch (/** @type {any} */ e) {
419
- statusMessage(`Error: ${e.message}`);
420
+ } catch (/** @type {unknown} */ e) {
421
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
420
422
  }
421
423
  }
422
424
 
@@ -439,8 +441,8 @@ async function browseDuplicateFile(file, container, ctx) {
439
441
  invalidateBrowseCache();
440
442
  renderBrowse(container, ctx);
441
443
  statusMessage(`Duplicated as ${copyName}`);
442
- } catch (/** @type {any} */ e) {
443
- statusMessage(`Error: ${e.message}`);
444
+ } catch (/** @type {unknown} */ e) {
445
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
444
446
  }
445
447
  }
446
448
 
@@ -458,8 +460,8 @@ async function browseDeleteFile(file, container, ctx) {
458
460
  invalidateBrowseCache();
459
461
  renderBrowse(container, ctx);
460
462
  statusMessage(`Deleted ${file.name}`);
461
- } catch (/** @type {any} */ e) {
462
- statusMessage(`Error: ${e.message}`);
463
+ } catch (/** @type {unknown} */ e) {
464
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
463
465
  }
464
466
  }
465
467
 
@@ -495,7 +497,7 @@ function showRenameDialog(currentName) {
495
497
  style="width:100%"
496
498
  value=${value}
497
499
  @input=${(/** @type {Event} */ e) => {
498
- value = /** @type {any} */ (e.target).value || "";
500
+ value = /** @type {HTMLInputElement} */ (e.target).value || "";
499
501
  }}
500
502
  @keydown=${(/** @type {KeyboardEvent} */ e) => {
501
503
  if (e.key === "Enter") confirm();
@@ -506,7 +508,7 @@ function showRenameDialog(currentName) {
506
508
 
507
509
  requestAnimationFrame(() => {
508
510
  const layer = document.getElementById("layer-dialog");
509
- const tf = /** @type {any} */ (layer?.querySelector("sp-textfield"));
511
+ const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
510
512
  if (tf) {
511
513
  tf.focus();
512
514
  const input = tf.shadowRoot?.querySelector("input");
@@ -587,7 +589,9 @@ async function loadPreview(el, file) {
587
589
  let preview = _previewCache.get(file.path);
588
590
  if (!preview) {
589
591
  try {
590
- const comp = componentRegistry.find((/** @type {any} */ c) => c.path === file.path);
592
+ const comp = componentRegistry.find(
593
+ (/** @type {ComponentEntry} */ c) => c.path === file.path,
594
+ );
591
595
  if (comp) {
592
596
  preview = /** @type {HTMLElement | undefined} */ (await renderComponentPreview(comp));
593
597
  } else {
@@ -686,8 +690,8 @@ export async function renderBrowse(container, ctx) {
686
690
  size="s"
687
691
  placeholder="Filter files..."
688
692
  .value=${searchQuery}
689
- @input=${(/** @type {any} */ e) => {
690
- searchQuery = e.target.value;
693
+ @input=${(/** @type {Event} */ e) => {
694
+ searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
691
695
  renderBrowse(container, ctx);
692
696
  }}
693
697
  @submit=${(/** @type {Event} */ e) => e.preventDefault()}
@@ -698,7 +702,8 @@ export async function renderBrowse(container, ctx) {
698
702
  </sp-action-button>
699
703
  <sp-popover slot="click-content" tip>
700
704
  <sp-menu
701
- @change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
705
+ @change=${(/** @type {Event} */ e) =>
706
+ handleNewEntity(/** @type {HTMLSelectElement} */ (e.target).value, container, ctx)}
702
707
  >
703
708
  ${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
704
709
  ${contentTypeTypes.length
@@ -726,9 +731,10 @@ export async function renderBrowse(container, ctx) {
726
731
  accept=${UPLOAD_ACCEPT}
727
732
  class="browse-upload-input"
728
733
  style="display:none"
729
- @change=${(/** @type {any} */ e) => {
730
- if (e.target.files?.length) handleUpload(e.target.files, container, ctx);
731
- e.target.value = "";
734
+ @change=${(/** @type {Event} */ e) => {
735
+ const input = /** @type {HTMLInputElement} */ (e.target);
736
+ if (input.files?.length) handleUpload(input.files, container, ctx);
737
+ input.value = "";
732
738
  }}
733
739
  />
734
740
  <div class="browse-view-switcher">
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  /**
9
- * @typedef {Record<string, any>} JxNode
9
+ * @typedef {JxMutableNode} JxNode
10
10
  *
11
11
  * @typedef {"added" | "removed" | "modified"} DiffStatus
12
12
  *
@@ -16,8 +16,8 @@
16
16
  /**
17
17
  * Deep equality check for two values. Ignores function and ref properties.
18
18
  *
19
- * @param {any} a
20
- * @param {any} b
19
+ * @param {unknown} a
20
+ * @param {unknown} b
21
21
  * @returns {boolean}
22
22
  */
23
23
  function valuesEqual(a, b) {
@@ -26,38 +26,46 @@ function valuesEqual(a, b) {
26
26
  if (Array.isArray(a) !== Array.isArray(b)) return false;
27
27
 
28
28
  if (Array.isArray(a)) {
29
- if (a.length !== b.length) return false;
30
- return a.every((/** @type {any} */ v, /** @type {number} */ i) => valuesEqual(v, b[i]));
29
+ const bArr = /** @type {unknown[]} */ (b);
30
+ if (a.length !== bArr.length) return false;
31
+ return a.every((/** @type {unknown} */ v, /** @type {number} */ i) => valuesEqual(v, bArr[i]));
31
32
  }
32
33
 
33
- const keysA = Object.keys(a).filter(
34
+ const aObj = /** @type {Record<string, unknown>} */ (a);
35
+ const bObj = /** @type {Record<string, unknown>} */ (b);
36
+
37
+ const keysA = Object.keys(aObj).filter(
34
38
  (/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
35
39
  );
36
- const keysB = Object.keys(b).filter(
40
+ const keysB = Object.keys(bObj).filter(
37
41
  (/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
38
42
  );
39
43
 
40
44
  if (keysA.length !== keysB.length) return false;
41
45
  if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
42
46
 
43
- return keysA.every((/** @type {string} */ k) => valuesEqual(a[k], b[k]));
47
+ return keysA.every((/** @type {string} */ k) => valuesEqual(aObj[k], bObj[k]));
44
48
  }
45
49
 
46
50
  /**
47
51
  * Filter element children from a node's children array.
48
52
  *
49
- * @param {any} node
50
- * @returns {any[]}
53
+ * @param {JxMutableNode | undefined} node
54
+ * @returns {JxMutableNode[]}
51
55
  */
52
56
  function elementChildren(node) {
53
57
  if (!node?.children || !Array.isArray(node.children)) return [];
54
- return node.children.filter((/** @type {any} */ c) => c != null && typeof c === "object");
58
+ return /** @type {JxMutableNode[]} */ (
59
+ node.children.filter(
60
+ (/** @type {JxMutableNode | string} */ c) => c != null && typeof c === "object",
61
+ )
62
+ );
55
63
  }
56
64
 
57
65
  /**
58
66
  * Mark an entire subtree with a diff status.
59
67
  *
60
- * @param {any} node
68
+ * @param {JxMutableNode} node
61
69
  * @param {DiffStatus} status
62
70
  * @param {string} path
63
71
  * @param {Map<string, DiffStatus>} diffMap
@@ -76,8 +84,8 @@ function markRecursive(node, status, path, diffMap, allPaths) {
76
84
  /**
77
85
  * Compute structural diff between original and current documents.
78
86
  *
79
- * @param {any} originalDoc - Original document (from git)
80
- * @param {any} currentDoc - Current document (in memory/on disk)
87
+ * @param {JxMutableNode | undefined} originalDoc - Original document (from git)
88
+ * @param {JxMutableNode | undefined} currentDoc - Current document (in memory/on disk)
81
89
  * @returns {DiffResult}
82
90
  */
83
91
  export function computeDocumentDiff(originalDoc, currentDoc) {
@@ -89,8 +97,8 @@ export function computeDocumentDiff(originalDoc, currentDoc) {
89
97
  /**
90
98
  * Walk both trees in parallel and mark differences.
91
99
  *
92
- * @param {any} origNode
93
- * @param {any} currNode
100
+ * @param {JxMutableNode | undefined} origNode
101
+ * @param {JxMutableNode | undefined} currNode
94
102
  * @param {string} path
95
103
  */
96
104
  const walk = (origNode, currNode, path = "") => {
@@ -7,7 +7,7 @@ import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath }
7
7
  import { activeTab } from "../workspace/workspace.js";
8
8
  import { isInlineInContext } from "../editor/inline-edit.js";
9
9
 
10
- /** @type {any} */
10
+ /** @type {{ getCanvasMode: () => string; getZoom: () => number } | null} */
11
11
  let _ctx = null;
12
12
 
13
13
  /**
@@ -21,9 +21,10 @@ export function initCanvasHelpers(ctx) {
21
21
 
22
22
  /** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
23
23
  export function effectiveZoom() {
24
- return _ctx.getCanvasMode() === "edit"
24
+ const ctx = /** @type {{ getCanvasMode: () => string; getZoom: () => number }} */ (_ctx);
25
+ return ctx.getCanvasMode() === "edit"
25
26
  ? 1
26
- : (_ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
27
+ : (ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
27
28
  }
28
29
 
29
30
  /** Return the active canvas panel based on the current activeMedia setting. */
@@ -42,10 +43,11 @@ export function getActivePanel() {
42
43
  * Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
43
44
  * ancestor. Returns the original path if already non-inline.
44
45
  *
45
- * @param {any} doc
46
- * @param {any} path
46
+ * @param {JxMutableNode | undefined} doc
47
+ * @param {JxPath} path
47
48
  */
48
49
  export function bubbleInlinePath(doc, path) {
50
+ if (!doc) return path;
49
51
  let currentPath = path;
50
52
  while (currentPath.length >= 2) {
51
53
  const node = getNodeAtPath(doc, currentPath);
@@ -55,7 +57,7 @@ export function bubbleInlinePath(doc, path) {
55
57
  const childTag = (node.tagName ?? "div").toLowerCase();
56
58
  const parentTag = (parentNode.tagName ?? "div").toLowerCase();
57
59
  if (!isInlineInContext(childTag, parentTag)) break;
58
- currentPath = pPath;
60
+ currentPath = /** @type {JxPath} */ (pPath);
59
61
  }
60
62
  return currentPath;
61
63
  }
@@ -63,11 +65,13 @@ export function bubbleInlinePath(doc, path) {
63
65
  /**
64
66
  * Find a canvas DOM element by its document path.
65
67
  *
66
- * @param {any} path
67
- * @param {any} canvasEl
68
+ * @param {JxPath} path
69
+ * @param {HTMLElement} canvasEl
70
+ * @returns {HTMLElement | null}
68
71
  */
69
72
  export function findCanvasElement(path, canvasEl) {
70
- let el = canvasEl.firstElementChild;
73
+ /** @type {HTMLElement | null | undefined} */
74
+ let el = /** @type {HTMLElement | null} */ (canvasEl.firstElementChild);
71
75
  if (!el) return null;
72
76
  if (path.length === 0) return el;
73
77
 
@@ -75,11 +79,11 @@ export function findCanvasElement(path, canvasEl) {
75
79
  if (path[i] !== "children" && path[i] !== "cases") return null;
76
80
  const idx = path[i + 1];
77
81
  if (idx === undefined) {
78
- el = el.children[0];
82
+ el = /** @type {HTMLElement | undefined} */ (el.children[0]);
79
83
  } else if (idx === "map") {
80
- el = el.children[0]?.children[0];
84
+ el = /** @type {HTMLElement | undefined} */ (el.children[0]?.children[0]);
81
85
  } else {
82
- el = el.children[idx];
86
+ el = /** @type {HTMLElement | undefined} */ (el.children[/** @type {number} */ (idx)]);
83
87
  }
84
88
  if (!el) break;
85
89
  }
@@ -91,7 +95,7 @@ export function findCanvasElement(path, canvasEl) {
91
95
 
92
96
  for (const candidate of canvasEl.querySelectorAll("*")) {
93
97
  const p = elToPath.get(candidate);
94
- if (p && pathsEqual(p, path)) return candidate;
98
+ if (p && pathsEqual(p, path)) return /** @type {HTMLElement} */ (candidate);
95
99
  }
96
100
  return null;
97
101
  }
@@ -99,18 +103,19 @@ export function findCanvasElement(path, canvasEl) {
99
103
  /**
100
104
  * Build an overlay box descriptor (no DOM creation).
101
105
  *
102
- * @param {any} el
103
- * @param {any} type
104
- * @param {any} panel
106
+ * @param {Element} el
107
+ * @param {string} type
108
+ * @param {import("./canvas-render.js").CanvasPanel} panel
105
109
  */
106
110
  export function overlayBoxDescriptor(el, type, panel) {
107
- const vpRect = panel.viewport.getBoundingClientRect();
111
+ const viewport = /** @type {HTMLElement} */ (panel.viewport);
112
+ const vpRect = viewport.getBoundingClientRect();
108
113
  const elRect = el.getBoundingClientRect();
109
114
  const scale = effectiveZoom();
110
115
  return {
111
116
  cls: `overlay-box overlay-${type}`,
112
- top: `${(elRect.top - vpRect.top + panel.viewport.scrollTop) / scale}px`,
113
- left: `${(elRect.left - vpRect.left + panel.viewport.scrollLeft) / scale}px`,
117
+ top: `${(elRect.top - vpRect.top + viewport.scrollTop) / scale}px`,
118
+ left: `${(elRect.left - vpRect.left + viewport.scrollLeft) / scale}px`,
114
119
  width: `${elRect.width / scale}px`,
115
120
  height: `${elRect.height / scale}px`,
116
121
  };