@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,19 +22,24 @@
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
- "test": "bun test",
27
+ "test": "bun test --isolate",
28
+ "test:coverage": "bun test --isolate --coverage",
28
29
  "upgrade": "bunx npm-check-updates -u && bun install"
29
30
  },
30
31
  "dependencies": {
31
32
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
33
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.18.0",
34
+ "@jxsuite/parser": "^0.21.0",
35
+ "@jxsuite/runtime": "^0.21.0",
36
+ "@jxsuite/schema": "^0.21.0",
34
37
  "@spectrum-web-components/accordion": "^1.12.1",
35
38
  "@spectrum-web-components/action-bar": "1.12.1",
36
39
  "@spectrum-web-components/action-button": "^1.12.1",
37
40
  "@spectrum-web-components/action-group": "^1.12.1",
41
+ "@spectrum-web-components/base": "^1.12.1",
42
+ "@spectrum-web-components/button": "^1.12.1",
38
43
  "@spectrum-web-components/checkbox": "^1.12.1",
39
44
  "@spectrum-web-components/color-area": "^1.12.1",
40
45
  "@spectrum-web-components/color-handle": "^1.12.1",
@@ -42,7 +47,10 @@
42
47
  "@spectrum-web-components/combobox": "^1.12.1",
43
48
  "@spectrum-web-components/dialog": "^1.12.1",
44
49
  "@spectrum-web-components/divider": "^1.12.1",
50
+ "@spectrum-web-components/field-label": "^1.12.1",
51
+ "@spectrum-web-components/help-text": "^1.12.1",
45
52
  "@spectrum-web-components/icon": "^1.12.1",
53
+ "@spectrum-web-components/icons-ui": "^1.12.1",
46
54
  "@spectrum-web-components/icons-workflow": "^1.12.1",
47
55
  "@spectrum-web-components/menu": "^1.12.1",
48
56
  "@spectrum-web-components/number-field": "^1.12.1",
@@ -60,9 +68,12 @@
60
68
  "@spectrum-web-components/theme": "^1.12.1",
61
69
  "@spectrum-web-components/toast": "^1.12.1",
62
70
  "@spectrum-web-components/tooltip": "^1.12.1",
63
- "@vue/reactivity": "3.5.34",
71
+ "@spectrum-web-components/underlay": "^1.12.1",
72
+ "@vue/reactivity": "3.5.35",
73
+ "lit": "^3.3.3",
64
74
  "lit-html": "^3.3.3",
65
75
  "monaco-editor": "^0.55.1",
76
+ "quikchat": "^1.2.7",
66
77
  "remark-directive": "^4.0.0",
67
78
  "remark-frontmatter": "^5.0.0",
68
79
  "remark-gfm": "^4.0.1",
@@ -73,9 +84,10 @@
73
84
  },
74
85
  "devDependencies": {
75
86
  "@happy-dom/global-registrator": "^20.9.0",
76
- "@webref/css": "^8.5.6",
87
+ "@types/mdast": "^4.0.4",
88
+ "@webref/css": "^8.5.7",
77
89
  "@webref/elements": "^2.7.1",
78
- "@webref/idl": "^3.78.0"
90
+ "@webref/idl": "^3.79.0"
79
91
  },
80
92
  "//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
81
93
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { html } from "lit-html";
7
+ import { ref } from "lit-html/directives/ref.js";
7
8
  import { renderBrowse } from "./browse.js";
8
9
  import { openFileInTab } from "../files/files.js";
9
10
  import { openModal } from "../ui/layers.js";
@@ -31,23 +32,25 @@ export function openBrowseModal() {
31
32
  <sp-icon-close slot="icon"></sp-icon-close>
32
33
  </sp-action-button>
33
34
  </div>
34
- <div class="browse-modal-content"></div>
35
+ <div
36
+ class="browse-modal-content"
37
+ ${ref((el) => {
38
+ if (el) {
39
+ requestAnimationFrame(() => {
40
+ renderBrowse(/** @type {HTMLElement} */ (el), {
41
+ openFile: (/** @type {string} */ path) => {
42
+ closeBrowseModal();
43
+ openFileInTab(path);
44
+ },
45
+ });
46
+ });
47
+ }
48
+ })}
49
+ ></div>
35
50
  </div>
36
51
  `;
37
52
 
38
53
  _handle = openModal(tpl);
39
-
40
- requestAnimationFrame(() => {
41
- const container = _handle?.host.querySelector(".browse-modal-content");
42
- if (container) {
43
- renderBrowse(/** @type {HTMLElement} */ (container), {
44
- openFile: (/** @type {string} */ path) => {
45
- closeBrowseModal();
46
- openFileInTab(path);
47
- },
48
- });
49
- }
50
- });
51
54
  }
52
55
 
53
56
  export function closeBrowseModal() {
@@ -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,9 +152,9 @@ 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
- const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
157
+ const prefix = d.source.replace(/^\.\//, "").replace(/\/$/, "");
156
158
  if (filePath.startsWith(prefix + "/") || filePath === prefix) {
157
159
  return name.charAt(0).toUpperCase() + name.slice(1);
158
160
  }
@@ -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,13 +232,13 @@ 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);
234
- const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
235
+ const d = /** @type {ContentTypeDef} */ (def);
236
+ const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
235
237
  return {
236
238
  key: `contentType:${name}`,
237
239
  label: name.charAt(0).toUpperCase() + name.slice(1),
238
240
  dir,
239
- ext: ".md",
241
+ ext: `.${d.format || "md"}`,
240
242
  contentTypeName: name,
241
243
  };
242
244
  });
@@ -360,7 +362,22 @@ function showBrowseContextMenu(e, file, container, ctx) {
360
362
  y = e.clientY;
361
363
 
362
364
  _browseCtxHandle = renderPopover(
363
- html`<sp-popover open style="position:fixed;left:${x}px;top:${y}px">
365
+ html`<sp-popover
366
+ open
367
+ style="position:fixed;left:${x}px;top:${y}px"
368
+ ${ref((el) => {
369
+ if (!el) return;
370
+ requestAnimationFrame(() => {
371
+ const popover = /** @type {HTMLElement} */ (el);
372
+ const menuRect = popover.getBoundingClientRect();
373
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
374
+ if (y + menuRect.height > window.innerHeight)
375
+ y = window.innerHeight - menuRect.height - 4;
376
+ popover.style.left = `${x}px`;
377
+ popover.style.top = `${y}px`;
378
+ });
379
+ })}
380
+ >
364
381
  <sp-menu>
365
382
  ${items.map((item) =>
366
383
  item.label === "\u2014"
@@ -384,18 +401,6 @@ function showBrowseContextMenu(e, file, container, ctx) {
384
401
  layer: "dialog",
385
402
  },
386
403
  );
387
-
388
- requestAnimationFrame(() => {
389
- const popover = /** @type {HTMLElement | null} */ (
390
- _browseCtxHandle?.host.querySelector("sp-popover")
391
- );
392
- if (!popover) return;
393
- const menuRect = popover.getBoundingClientRect();
394
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
395
- if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
396
- popover.style.left = `${x}px`;
397
- popover.style.top = `${y}px`;
398
- });
399
404
  }
400
405
 
401
406
  /**
@@ -415,8 +420,8 @@ async function browseRenameFile(file, container, ctx) {
415
420
  invalidateBrowseCache();
416
421
  renderBrowse(container, ctx);
417
422
  statusMessage(`Renamed to ${newName}`);
418
- } catch (/** @type {any} */ e) {
419
- statusMessage(`Error: ${e.message}`);
423
+ } catch (/** @type {unknown} */ e) {
424
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
420
425
  }
421
426
  }
422
427
 
@@ -439,8 +444,8 @@ async function browseDuplicateFile(file, container, ctx) {
439
444
  invalidateBrowseCache();
440
445
  renderBrowse(container, ctx);
441
446
  statusMessage(`Duplicated as ${copyName}`);
442
- } catch (/** @type {any} */ e) {
443
- statusMessage(`Error: ${e.message}`);
447
+ } catch (/** @type {unknown} */ e) {
448
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
444
449
  }
445
450
  }
446
451
 
@@ -458,8 +463,8 @@ async function browseDeleteFile(file, container, ctx) {
458
463
  invalidateBrowseCache();
459
464
  renderBrowse(container, ctx);
460
465
  statusMessage(`Deleted ${file.name}`);
461
- } catch (/** @type {any} */ e) {
462
- statusMessage(`Error: ${e.message}`);
466
+ } catch (/** @type {unknown} */ e) {
467
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
463
468
  }
464
469
  }
465
470
 
@@ -495,7 +500,7 @@ function showRenameDialog(currentName) {
495
500
  style="width:100%"
496
501
  value=${value}
497
502
  @input=${(/** @type {Event} */ e) => {
498
- value = /** @type {any} */ (e.target).value || "";
503
+ value = /** @type {HTMLInputElement} */ (e.target).value || "";
499
504
  }}
500
505
  @keydown=${(/** @type {KeyboardEvent} */ e) => {
501
506
  if (e.key === "Enter") confirm();
@@ -506,7 +511,7 @@ function showRenameDialog(currentName) {
506
511
 
507
512
  requestAnimationFrame(() => {
508
513
  const layer = document.getElementById("layer-dialog");
509
- const tf = /** @type {any} */ (layer?.querySelector("sp-textfield"));
514
+ const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
510
515
  if (tf) {
511
516
  tf.focus();
512
517
  const input = tf.shadowRoot?.querySelector("input");
@@ -587,7 +592,9 @@ async function loadPreview(el, file) {
587
592
  let preview = _previewCache.get(file.path);
588
593
  if (!preview) {
589
594
  try {
590
- const comp = componentRegistry.find((/** @type {any} */ c) => c.path === file.path);
595
+ const comp = componentRegistry.find(
596
+ (/** @type {ComponentEntry} */ c) => c.path === file.path,
597
+ );
591
598
  if (comp) {
592
599
  preview = /** @type {HTMLElement | undefined} */ (await renderComponentPreview(comp));
593
600
  } else {
@@ -686,8 +693,8 @@ export async function renderBrowse(container, ctx) {
686
693
  size="s"
687
694
  placeholder="Filter files..."
688
695
  .value=${searchQuery}
689
- @input=${(/** @type {any} */ e) => {
690
- searchQuery = e.target.value;
696
+ @input=${(/** @type {Event} */ e) => {
697
+ searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
691
698
  renderBrowse(container, ctx);
692
699
  }}
693
700
  @submit=${(/** @type {Event} */ e) => e.preventDefault()}
@@ -698,7 +705,8 @@ export async function renderBrowse(container, ctx) {
698
705
  </sp-action-button>
699
706
  <sp-popover slot="click-content" tip>
700
707
  <sp-menu
701
- @change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
708
+ @change=${(/** @type {Event} */ e) =>
709
+ handleNewEntity(/** @type {HTMLSelectElement} */ (e.target).value, container, ctx)}
702
710
  >
703
711
  ${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
704
712
  ${contentTypeTypes.length
@@ -726,9 +734,10 @@ export async function renderBrowse(container, ctx) {
726
734
  accept=${UPLOAD_ACCEPT}
727
735
  class="browse-upload-input"
728
736
  style="display:none"
729
- @change=${(/** @type {any} */ e) => {
730
- if (e.target.files?.length) handleUpload(e.target.files, container, ctx);
731
- e.target.value = "";
737
+ @change=${(/** @type {Event} */ e) => {
738
+ const input = /** @type {HTMLInputElement} */ (e.target);
739
+ if (input.files?.length) handleUpload(input.files, container, ctx);
740
+ input.value = "";
732
741
  }}
733
742
  />
734
743
  <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
  };