@jxsuite/studio 0.23.2 → 0.25.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 (106) hide show
  1. package/dist/studio.js +2311 -718
  2. package/dist/studio.js.map +121 -104
  3. package/package.json +8 -7
  4. package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
  5. package/src/browse/{browse.js → browse.ts} +109 -84
  6. package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
  7. package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
  8. package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
  9. package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
  10. package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
  11. package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
  12. package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
  13. package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
  14. package/src/editor/{context-menu.js → context-menu.ts} +38 -36
  15. package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
  16. package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
  17. package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
  18. package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
  19. package/src/editor/{inline-format.js → inline-format.ts} +46 -51
  20. package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
  21. package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
  22. package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
  23. package/src/files/{components.js → components.ts} +12 -15
  24. package/src/files/{file-ops.js → file-ops.ts} +36 -29
  25. package/src/files/{files.js → files.ts} +102 -84
  26. package/src/github/{github-auth.js → github-auth.ts} +3 -3
  27. package/src/github/{github-publish.js → github-publish.ts} +20 -18
  28. package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
  29. package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
  30. package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
  31. package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
  32. package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
  33. package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
  34. package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
  35. package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
  36. package/src/panels/dnd.ts +417 -0
  37. package/src/panels/{editors.js → editors.ts} +43 -50
  38. package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
  39. package/src/panels/{events-panel.js → events-panel.ts} +33 -25
  40. package/src/panels/{git-panel.js → git-panel.ts} +97 -83
  41. package/src/panels/{head-panel.js → head-panel.ts} +117 -139
  42. package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
  43. package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
  44. package/src/panels/{left-panel.js → left-panel.ts} +56 -63
  45. package/src/panels/{overlays.js → overlays.ts} +28 -36
  46. package/src/panels/{panel-events.js → panel-events.ts} +53 -51
  47. package/src/panels/{preview-render.js → preview-render.ts} +20 -15
  48. package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
  49. package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
  50. package/src/panels/{quick-search.js → quick-search.ts} +16 -17
  51. package/src/panels/{right-panel.js → right-panel.ts} +41 -59
  52. package/src/panels/{shared.js → shared.ts} +8 -6
  53. package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
  54. package/src/panels/{statusbar.js → statusbar.ts} +14 -14
  55. package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
  56. package/src/panels/{style-panel.js → style-panel.ts} +235 -290
  57. package/src/panels/{style-utils.js → style-utils.ts} +32 -31
  58. package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
  59. package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
  60. package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
  61. package/src/panels/{toolbar.js → toolbar.ts} +68 -70
  62. package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
  63. package/src/{platform.js → platform.ts} +5 -3
  64. package/src/platforms/{devserver.js → devserver.ts} +53 -44
  65. package/src/{recent-projects.js → recent-projects.ts} +17 -12
  66. package/src/{resize-edges.js → resize-edges.ts} +18 -13
  67. package/src/services/{cem-export.js → cem-export.ts} +14 -7
  68. package/src/services/{code-services.js → code-services.ts} +29 -20
  69. package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
  70. package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
  71. package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
  72. package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
  73. package/src/settings/{general-settings.js → general-settings.ts} +29 -28
  74. package/src/settings/{head-editor.js → head-editor.ts} +37 -35
  75. package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
  76. package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
  77. package/src/{site-context.js → site-context.ts} +35 -32
  78. package/src/{state.js → state.ts} +95 -78
  79. package/src/{store.js → store.ts} +60 -63
  80. package/src/{studio.js → studio.ts} +117 -119
  81. package/src/tabs/tab.ts +196 -0
  82. package/src/tabs/{transact.js → transact.ts} +107 -60
  83. package/src/types.ts +185 -0
  84. package/src/ui/{button-group.js → button-group.ts} +21 -21
  85. package/src/ui/{color-selector.js → color-selector.ts} +44 -37
  86. package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
  87. package/src/ui/{field-row.js → field-row.ts} +19 -2
  88. package/src/ui/{icons.js → icons.ts} +2 -2
  89. package/src/ui/{layers.js → layers.ts} +33 -24
  90. package/src/ui/{media-picker.js → media-picker.ts} +13 -9
  91. package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
  92. package/src/ui/{spectrum.js → spectrum.ts} +4 -4
  93. package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
  94. package/src/ui/{value-selector.js → value-selector.ts} +28 -28
  95. package/src/ui/{widgets.js → widgets.ts} +62 -15
  96. package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
  97. package/src/utils/edit-display.ts +298 -0
  98. package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
  99. package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
  100. package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
  101. package/src/{view.js → view.ts} +53 -55
  102. package/src/workspace/{workspace.js → workspace.ts} +44 -33
  103. package/src/panels/dnd.js +0 -426
  104. package/src/tabs/tab.js +0 -192
  105. package/src/utils/edit-display.js +0 -203
  106. /package/src/{reactivity.js → reactivity.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Component inline edit — extracted from studio.js (Phase 4j). Manages plaintext-only editing on
3
4
  * canvas elements in design mode, with slash menu delegation for block insertion.
@@ -11,28 +12,43 @@ import {
11
12
  childIndex,
12
13
  canvasPanels,
13
14
  elToPath,
14
- } from "../store.js";
15
- import { activeTab } from "../workspace/workspace.js";
15
+ } from "../store";
16
+ import { activeTab } from "../workspace/workspace";
17
+ import type { JxPath } from "../state";
16
18
  import {
17
19
  transactDoc,
18
20
  mutateRemoveNode,
19
21
  mutateInsertNode,
20
22
  mutateUpdateProperty,
21
- } from "../tabs/transact.js";
22
- import { view } from "../view.js";
23
- import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu.js";
24
- import { renderBlockActionBar } from "../panels/block-action-bar.js";
25
- import { defaultDef } from "../panels/shared.js";
23
+ } from "../tabs/transact";
24
+ import { view } from "../view";
25
+ import { isSlashMenuOpen, showSlashMenu, dismissSlashMenu } from "./slash-menu";
26
+ import { renderBlockActionBar } from "../panels/block-action-bar";
27
+ import { defaultDef } from "../panels/shared";
26
28
 
27
- /** @type {{ findCanvasElement: Function } | null} */
29
+ /**
30
+ * @type {{
31
+ * findCanvasElement: (
32
+ * path: import("../state").JxPath,
33
+ * canvasEl: HTMLElement,
34
+ * ) => HTMLElement | null;
35
+ * } | null}
36
+ */
28
37
  let _ctx = null;
29
38
 
30
39
  /**
31
40
  * Initialize the component inline edit module.
32
41
  *
33
- * @param {{ findCanvasElement: Function }} ctx
42
+ * @param {{
43
+ * findCanvasElement: (
44
+ * path: import("../state").JxPath,
45
+ * canvasEl: HTMLElement,
46
+ * ) => HTMLElement | null;
47
+ * }} ctx
34
48
  */
35
- export function initComponentInlineEdit(ctx) {
49
+ export function initComponentInlineEdit(ctx: {
50
+ findCanvasElement: (path: JxPath, canvasEl: HTMLElement) => HTMLElement | null;
51
+ }) {
36
52
  _ctx = ctx;
37
53
  }
38
54
 
@@ -42,12 +58,12 @@ export function initComponentInlineEdit(ctx) {
42
58
  * @param {HTMLElement} el
43
59
  * @param {JxPath} path
44
60
  */
45
- export function enterComponentInlineEdit(el, path) {
61
+ export function enterComponentInlineEdit(el: HTMLElement, path: JxPath) {
46
62
  if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
47
63
  return;
48
64
  }
49
65
 
50
- const node = getNodeAtPath(activeTab.value?.doc.document, path);
66
+ const node = getNodeAtPath(activeTab.value!.doc.document, path);
51
67
  if (!node) return;
52
68
 
53
69
  const tc = node.textContent;
@@ -59,9 +75,7 @@ export function enterComponentInlineEdit(el, path) {
59
75
  if (voids.has(node.tagName || "")) return;
60
76
 
61
77
  for (const p of canvasPanels) {
62
- const boxes = /** @type {NodeListOf<HTMLElement>} */ (
63
- p.overlay.querySelectorAll(".overlay-box")
64
- );
78
+ const boxes = p.overlay.querySelectorAll(".overlay-box") as NodeListOf<HTMLElement>;
65
79
  for (const box of boxes) {
66
80
  box.style.border = "none";
67
81
  }
@@ -96,22 +110,20 @@ export function enterComponentInlineEdit(el, path) {
96
110
  el.addEventListener("keydown", componentInlineKeydown);
97
111
  el.addEventListener("input", componentInlineInput);
98
112
 
99
- const outsideHandler = (/** @type {MouseEvent} */ evt) => {
113
+ const outsideHandler = (evt: MouseEvent) => {
100
114
  if (!view.componentInlineEdit) {
101
115
  document.removeEventListener("mousedown", outsideHandler, true);
102
116
  return;
103
117
  }
104
- if (view.componentInlineEdit.el.contains(/** @type {Node} */ (evt.target))) return;
118
+ if (view.componentInlineEdit.el.contains(evt.target as Node)) return;
105
119
  if (isSlashMenuOpen()) return;
106
- if (view.blockActionBarEl && view.blockActionBarEl.contains(/** @type {Node} */ (evt.target)))
107
- return;
120
+ if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target as Node)) return;
108
121
  document.removeEventListener("mousedown", outsideHandler, true);
109
122
 
110
- /** @type {(string | number)[] | null} */
111
- let hitPath = null,
123
+ let hitPath: (string | number)[] | null = null,
112
124
  hitMedia = null;
113
125
  for (const p of canvasPanels) {
114
- const els = /** @type {NodeListOf<HTMLElement>} */ (p.canvas.querySelectorAll("*"));
126
+ const els = p.canvas.querySelectorAll("*") as NodeListOf<HTMLElement>;
115
127
  for (const el of els) el.style.pointerEvents = "auto";
116
128
  p.overlayClk.style.display = "none";
117
129
  const found = document.elementsFromPoint(evt.clientX, evt.clientY);
@@ -141,12 +153,12 @@ export function enterComponentInlineEdit(el, path) {
141
153
  let hp = hitPath;
142
154
  const media = hitMedia === "base" ? null : (hitMedia ?? null);
143
155
  updateUi("pendingInlineEdit", { path: hp, mediaName: hitMedia });
144
- activeTab.value.session.ui.activeMedia = media;
156
+ activeTab.value!.session.ui.activeMedia = media;
145
157
  if (isEmpty && pPath) {
146
158
  transactDoc(activeTab.value, (t) => {
147
159
  mutateRemoveNode(t, editPath);
148
- const removedIdx = /** @type {number} */ (childIndex(editPath));
149
- const hitIdx = /** @type {number} */ (childIndex(hp));
160
+ const removedIdx = childIndex(editPath) as number;
161
+ const hitIdx = childIndex(hp) as number;
150
162
  const hitParent = parentElementPath(hp);
151
163
  if (
152
164
  hitParent &&
@@ -165,7 +177,7 @@ export function enterComponentInlineEdit(el, path) {
165
177
  t.session.selection = hp;
166
178
  });
167
179
  } else {
168
- activeTab.value.session.selection = hp;
180
+ activeTab.value!.session.selection = hp;
169
181
  }
170
182
  } else {
171
183
  if (isEmpty && pPath) {
@@ -187,7 +199,7 @@ export function enterComponentInlineEdit(el, path) {
187
199
  }
188
200
 
189
201
  /** @param {KeyboardEvent} e */
190
- function componentInlineKeydown(e) {
202
+ function componentInlineKeydown(e: KeyboardEvent) {
191
203
  if (isSlashMenuOpen()) {
192
204
  if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
193
205
  }
@@ -206,7 +218,7 @@ function splitParagraph() {
206
218
  if (!view.componentInlineEdit) return;
207
219
  const { el, path, mediaName } = view.componentInlineEdit;
208
220
 
209
- const sel = /** @type {Selection | null} */ (el.ownerDocument.defaultView?.getSelection());
221
+ const sel = el.ownerDocument.defaultView?.getSelection() as Selection | null;
210
222
  const fullText = el.textContent || "";
211
223
  let offset = fullText.length;
212
224
  if (sel && sel.rangeCount) {
@@ -221,8 +233,8 @@ function splitParagraph() {
221
233
  const textAfter = fullText.slice(offset);
222
234
 
223
235
  const tag = "p";
224
- const pPath = /** @type {JxPath} */ (parentElementPath(path));
225
- const idx = /** @type {number} */ (childIndex(path));
236
+ const pPath = parentElementPath(path) as JxPath;
237
+ const idx = childIndex(path) as number;
226
238
  if (!pPath) return;
227
239
 
228
240
  const newDef = { tagName: tag, textContent: textAfter };
@@ -268,7 +280,7 @@ function cancelComponentInlineEdit() {
268
280
  }
269
281
 
270
282
  /** @param {HTMLElement} el */
271
- function cleanupComponentInlineEdit(el) {
283
+ function cleanupComponentInlineEdit(el: HTMLElement) {
272
284
  el.removeEventListener("keydown", componentInlineKeydown);
273
285
  el.removeEventListener("input", componentInlineInput);
274
286
  dismissSlashMenu();
@@ -306,11 +318,11 @@ function componentInlineInput() {
306
318
  }
307
319
 
308
320
  /** @param {{ tag: string; label: string; description: string }} cmd */
309
- function handleComponentSlashSelect(cmd) {
321
+ function handleComponentSlashSelect(cmd: { tag: string; label: string; description: string }) {
310
322
  if (!view.componentInlineEdit) return;
311
323
  const { el, path, mediaName } = view.componentInlineEdit;
312
324
  const pPath = parentElementPath(path);
313
- const idx = /** @type {number} */ (childIndex(path));
325
+ const idx = childIndex(path) as number;
314
326
  if (!pPath) return;
315
327
 
316
328
  cleanupComponentInlineEdit(el);
@@ -1,27 +1,18 @@
1
- /**
2
- * Content inline edit bridge extracted from studio.js (Phase 4k). Rich-text editing entry point
3
- * for edit/content mode. Bridges startEditing() with Jx document state mutations.
4
- *
5
- * @typedef {import("./inline-edit.js").JxContentResult} JxContentResult
6
- *
7
- * @typedef {import("./inline-edit.js").SlashCommand} SlashCommand
8
- */
9
-
10
- import {
11
- renderOnly,
12
- getNodeAtPath,
13
- parentElementPath,
14
- childIndex,
15
- canvasPanels,
16
- } from "../store.js";
17
- import { activeTab } from "../workspace/workspace.js";
18
- import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact.js";
19
- import { view } from "../view.js";
20
- import { startEditing, isEditableBlock } from "./inline-edit.js";
21
- import { restoreTemplateExpressions } from "../utils/edit-display.js";
22
- import { renderBlockActionBar } from "../panels/block-action-bar.js";
23
- import { defaultDef } from "../panels/shared.js";
24
- import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
1
+ /// <reference lib="dom" />
2
+ import { renderOnly, getNodeAtPath, parentElementPath, childIndex, canvasPanels } from "../store";
3
+ import { activeTab } from "../workspace/workspace";
4
+ import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact";
5
+ import { view } from "../view";
6
+ import { startEditing, isEditableBlock } from "./inline-edit";
7
+ import { restoreTemplateExpressions } from "../utils/edit-display";
8
+ import { renderBlockActionBar } from "../panels/block-action-bar";
9
+ import { defaultDef } from "../panels/shared";
10
+ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
11
+
12
+ import type { JxContentResult } from "./inline-edit";
13
+ import type { SlashCommand } from "./inline-edit";
14
+ import type { JxPath } from "../state";
15
+ import type { JxMutableNode } from "@jxsuite/schema/types";
25
16
 
26
17
  /**
27
18
  * Enter rich-text inline editing on a canvas element (edit/content mode).
@@ -29,7 +20,7 @@ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
29
20
  * @param {HTMLElement} el
30
21
  * @param {JxPath} path
31
22
  */
32
- export function enterInlineEdit(el, path) {
23
+ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
33
24
  // Restore raw template expressions before editing.
34
25
  // prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
35
26
  // revert so the user edits the real syntax and commits it back intact.
@@ -43,11 +34,11 @@ export function enterInlineEdit(el, path) {
43
34
 
44
35
  startEditing(el, path, {
45
36
  onCommit(
46
- /** @type {JxPath} */ commitPath,
47
- /** @type {(JxMutableNode | string)[] | null} */ children,
48
- /** @type {string | null} */ textContent,
37
+ commitPath: JxPath,
38
+ children: (JxMutableNode | string)[] | null,
39
+ textContent: string | null,
49
40
  ) {
50
- const node = getNodeAtPath(activeTab.value?.doc.document, commitPath);
41
+ const node = getNodeAtPath(activeTab.value!.doc.document, commitPath);
51
42
  if (children) {
52
43
  if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
53
44
  transactDoc(activeTab.value, (t) => {
@@ -63,18 +54,13 @@ export function enterInlineEdit(el, path) {
63
54
  }
64
55
  },
65
56
 
66
- onSplit(
67
- /** @type {JxPath} */ splitPath,
68
- /** @type {JxContentResult} */ before,
69
- /** @type {JxContentResult} */ after,
70
- ) {
57
+ onSplit(splitPath: JxPath, before: JxContentResult, after: JxContentResult) {
71
58
  const tag = "p";
72
59
 
73
60
  // Insert new element after with "after" content
74
- const parentPath = /** @type {JxPath} */ (parentElementPath(splitPath));
75
- const idx = /** @type {number} */ (childIndex(splitPath));
76
- /** @type {JxMutableNode} */
77
- const newNode = { tagName: tag };
61
+ const parentPath = parentElementPath(splitPath) as JxPath;
62
+ const idx = childIndex(splitPath) as number;
63
+ const newNode: JxMutableNode = { tagName: tag };
78
64
  if (after.textContent != null) {
79
65
  newNode.textContent = after.textContent;
80
66
  } else if (after.children) {
@@ -116,11 +102,7 @@ export function enterInlineEdit(el, path) {
116
102
  });
117
103
  },
118
104
 
119
- onInsert(
120
- /** @type {JxPath} */ afterPath,
121
- /** @type {SlashCommand} */ cmd,
122
- /** @type {JxContentResult | undefined} */ commitData,
123
- ) {
105
+ onInsert(afterPath: JxPath, cmd: SlashCommand, commitData: JxContentResult | undefined) {
124
106
  const isEmpty =
125
107
  !commitData ||
126
108
  (commitData.textContent != null && commitData.textContent.trim() === "") ||
@@ -160,8 +142,8 @@ export function enterInlineEdit(el, path) {
160
142
  }
161
143
 
162
144
  const elementDef = defaultDef(cmd.tag);
163
- const parentPath = /** @type {JxPath} */ (parentElementPath(afterPath));
164
- const idx = /** @type {number} */ (childIndex(afterPath));
145
+ const parentPath = parentElementPath(afterPath) as JxPath;
146
+ const idx = childIndex(afterPath) as number;
165
147
  const newPath = [...parentPath, "children", idx + 1];
166
148
 
167
149
  // Apply pending commit from inline edit first (batched to avoid double render)
@@ -1,9 +1,10 @@
1
+ /// <reference lib="dom" />
1
2
  // ─── Clipboard & Context Menu ─────────────────────────────────────────────────
2
3
  import { html } from "lit-html";
3
4
  import { ref } from "lit-html/directives/ref.js";
4
5
  import { htmlToJx } from "@jxsuite/parser/html-to-jx";
5
- import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
6
- import { activeTab, workspace } from "../workspace/workspace.js";
6
+ import { getNodeAtPath, parentElementPath, childIndex } from "../store";
7
+ import { activeTab, workspace } from "../workspace/workspace";
7
8
  import {
8
9
  transactDoc,
9
10
  mutateInsertNode,
@@ -11,28 +12,25 @@ import {
11
12
  mutateDuplicateNode,
12
13
  mutateWrapNode,
13
14
  mutateReplaceStyle,
14
- } from "../tabs/transact.js";
15
- import { statusMessage } from "../panels/statusbar.js";
16
- import { convertToComponent } from "./convert-to-component.js";
17
- import { convertToRepeater } from "./convert-to-repeater.js";
18
- import { componentRegistry } from "../files/components.js";
19
- import { renderPopover } from "../ui/layers.js";
20
- import { startLayerTitleEdit } from "../panels/layers-panel.js";
15
+ } from "../tabs/transact";
16
+ import { statusMessage } from "../panels/statusbar";
17
+ import { convertToComponent } from "./convert-to-component";
18
+ import { convertToRepeater } from "./convert-to-repeater";
19
+ import { componentRegistry } from "../files/components";
20
+ import { renderPopover } from "../ui/layers";
21
+ import { startLayerTitleEdit } from "../panels/layers-panel";
21
22
 
22
- /**
23
- * @typedef {import("../state.js").StudioState} StudioState
24
- *
25
- * @typedef {import("../state.js").JxPath} JxPath
26
- *
27
- * @typedef {JxMutableNode} JxNode
28
- */
23
+ import type { JxPath } from "../state";
24
+ import type { JxMutableNode } from "@jxsuite/schema/types";
25
+
26
+ type JxNode = JxMutableNode;
29
27
 
30
28
  // ─── Clipboard helpers ───────────────────────────────────────────────────────
31
29
 
32
30
  const JX_MIME = "web application/jx+json";
33
31
 
34
32
  /** @param {JxNode | string} node */
35
- function nodeToHtml(node) {
33
+ function nodeToHtml(node: JxNode | string) {
36
34
  if (typeof node === "string") return node;
37
35
  const tag = node.tagName || "div";
38
36
  let attrs = "";
@@ -61,7 +59,7 @@ function nodeToHtml(node) {
61
59
  *
62
60
  * @param {object} json
63
61
  */
64
- async function writeToClipboard(json) {
62
+ async function writeToClipboard(json: Record<string, unknown>) {
65
63
  workspace.clipboard = json;
66
64
  try {
67
65
  await navigator.clipboard.write([
@@ -98,9 +96,9 @@ async function readFromClipboard() {
98
96
  const blob = await item.getType("text/html");
99
97
  const htmlStr = await blob.text();
100
98
  const nodes = htmlToJx(htmlStr);
101
- const jxNodes = /** @type {JxNode[]} */ (
102
- nodes.map((n) => (typeof n === "string" ? { tagName: "p", textContent: n } : n))
103
- );
99
+ const jxNodes = nodes.map((n) =>
100
+ typeof n === "string" ? { tagName: "p", textContent: n } : n,
101
+ ) as JxNode[];
104
102
  if (jxNodes.length > 0) return jxNodes;
105
103
  }
106
104
  if (item.types.includes("text/plain")) {
@@ -161,8 +159,8 @@ export async function pasteNode() {
161
159
  if (!parent) return;
162
160
 
163
161
  if (tab.session.selection && tab.session.selection.length >= 2) {
164
- const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
165
- const idx = /** @type {number} */ (childIndex(tab.session.selection));
162
+ const pp = parentElementPath(tab.session.selection) as JxPath;
163
+ const idx = childIndex(tab.session.selection) as number;
166
164
  transactDoc(tab, (t) => {
167
165
  for (let i = 0; i < nodes.length; i++) {
168
166
  mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
@@ -193,15 +191,14 @@ export function pasteStyles() {
193
191
  const tab = activeTab.value;
194
192
  if (!tab?.session.selection) return;
195
193
  const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
196
- const sel = /** @type {JxPath} */ (tab.session.selection);
194
+ const sel = tab.session.selection as JxPath;
197
195
  transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
198
196
  statusMessage("Styles pasted");
199
197
  }
200
198
 
201
199
  // ─── Context menu ─────────────────────────────────────────────────────────────
202
200
 
203
- /** @type {ReturnType<typeof renderPopover> | null} */
204
- let _ctxHandle = null;
201
+ let _ctxHandle: ReturnType<typeof renderPopover> | null = null;
205
202
 
206
203
  /** Dismiss the context menu if open. */
207
204
  export function dismissContextMenu() {
@@ -216,12 +213,17 @@ export function dismissContextMenu() {
216
213
  * @param {JxPath} path
217
214
  * @param {{ onEditComponent?: (path: string) => void; rerender?: () => void }} [opts]
218
215
  */
219
- export function showContextMenu(e, path, opts = {}) {
216
+ export function showContextMenu(
217
+ e: MouseEvent,
218
+ path: JxPath,
219
+ opts: { onEditComponent?: (path: string) => void; rerender?: () => void } = {},
220
+ ) {
220
221
  e.preventDefault();
221
222
  dismissContextMenu();
222
223
 
223
224
  const tab = activeTab.value;
224
- const node = getNodeAtPath(tab?.doc.document, path);
225
+ if (!tab) return;
226
+ const node = getNodeAtPath(tab.doc.document, path);
225
227
  if (!node) return;
226
228
 
227
229
  // Select the node
@@ -261,8 +263,8 @@ export function showContextMenu(e, path, opts = {}) {
261
263
  items.push({
262
264
  label: "Insert before",
263
265
  action: () => {
264
- const pp = /** @type {JxPath} */ (parentElementPath(path));
265
- const idx = /** @type {number} */ (childIndex(path));
266
+ const pp = parentElementPath(path) as JxPath;
267
+ const idx = childIndex(path) as number;
266
268
  transactDoc(activeTab.value, (t) =>
267
269
  mutateInsertNode(t, pp, idx, { tagName: "p", children: [] }),
268
270
  );
@@ -271,8 +273,8 @@ export function showContextMenu(e, path, opts = {}) {
271
273
  items.push({
272
274
  label: "Insert after",
273
275
  action: () => {
274
- const pp = /** @type {JxPath} */ (parentElementPath(path));
275
- const idx = /** @type {number} */ (childIndex(path));
276
+ const pp = parentElementPath(path) as JxPath;
277
+ const idx = childIndex(path) as number;
276
278
  transactDoc(activeTab.value, (t) =>
277
279
  mutateInsertNode(t, pp, idx + 1, { tagName: "p", children: [] }),
278
280
  );
@@ -309,7 +311,7 @@ export function showContextMenu(e, path, opts = {}) {
309
311
  );
310
312
  items.push({
311
313
  label: "Edit Component",
312
- action: () => opts.onEditComponent?.(/** @type {string} */ (comp?.path)),
314
+ action: () => opts.onEditComponent?.(comp?.path as string),
313
315
  });
314
316
  } else if (!isComponent) {
315
317
  items.push({
@@ -346,8 +348,8 @@ export function showContextMenu(e, path, opts = {}) {
346
348
  action: async () => {
347
349
  const nodes = await readFromClipboard();
348
350
  if (!nodes || nodes.length === 0) return;
349
- const pp = /** @type {JxPath} */ (parentElementPath(path));
350
- const idx = /** @type {number} */ (childIndex(path));
351
+ const pp = parentElementPath(path) as JxPath;
352
+ const idx = childIndex(path) as number;
351
353
  transactDoc(activeTab.value, (t) => {
352
354
  for (let i = 0; i < nodes.length; i++) {
353
355
  mutateInsertNode(t, pp, idx + 1 + i, nodes[i]);
@@ -368,7 +370,7 @@ export function showContextMenu(e, path, opts = {}) {
368
370
  ${ref((el) => {
369
371
  if (!el) return;
370
372
  requestAnimationFrame(() => {
371
- const popover = /** @type {HTMLElement} */ (el);
373
+ const popover = el as HTMLElement;
372
374
  const menuRect = popover.getBoundingClientRect();
373
375
  if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
374
376
  if (y + menuRect.height > window.innerHeight)
@@ -1,8 +1,12 @@
1
1
  import elementsMeta from "../../data/elements-meta.json";
2
2
 
3
- /** @typedef {{ label: string; tag: string; description: string }} SlashCommand */
3
+ export interface SlashCommand {
4
+ label: string;
5
+ tag: string;
6
+ description: string;
7
+ }
4
8
 
5
- const TAG_LABELS = /** @type {Record<string, string>} */ ({
9
+ const TAG_LABELS = {
6
10
  p: "Paragraph",
7
11
  h1: "Heading 1",
8
12
  h2: "Heading 2",
@@ -22,9 +26,9 @@ const TAG_LABELS = /** @type {Record<string, string>} */ ({
22
26
  search: "Search",
23
27
  ul: "Bulleted List",
24
28
  ol: "Numbered List",
25
- });
29
+ } as Record<string, string>;
26
30
 
27
- const groups = /** @type {Record<string, string[]>} */ (elementsMeta.$convertGroups || {});
31
+ const groups = (elementsMeta.$convertGroups || {}) as Record<string, string[]>;
28
32
 
29
33
  /**
30
34
  * Get the list of tags the current element can be converted to.
@@ -33,19 +37,18 @@ const groups = /** @type {Record<string, string[]>} */ (elementsMeta.$convertGro
33
37
  * @param {boolean} isEmpty
34
38
  * @returns {SlashCommand[]}
35
39
  */
36
- export function getConvertTargets(currentTag, isEmpty) {
37
- const def = /** @type {Record<string, unknown> | undefined} */ (
38
- /** @type {Record<string, Record<string, unknown>>} */ (elementsMeta.$defs)?.[currentTag]
39
- );
40
+ export function getConvertTargets(currentTag: string, isEmpty: boolean) {
41
+ const def = (elementsMeta.$defs as Record<string, Record<string, unknown>>)?.[currentTag] as
42
+ | Record<string, unknown>
43
+ | undefined;
40
44
  if (!def?.$convertTo) return [];
41
45
 
42
46
  const groupNames =
43
47
  isEmpty && def.$convertToWhenEmpty
44
- ? /** @type {string[]} */ (def.$convertToWhenEmpty)
45
- : [/** @type {string} */ (def.$convertTo)];
48
+ ? (def.$convertToWhenEmpty as string[])
49
+ : [def.$convertTo as string];
46
50
 
47
- /** @type {Set<string>} */
48
- const tags = new Set();
51
+ const tags: Set<string> = new Set();
49
52
  for (const name of groupNames) {
50
53
  for (const tag of groups[name] || []) {
51
54
  if (tag !== currentTag) tags.add(tag);
@@ -1,17 +1,16 @@
1
+ /// <reference lib="dom" />
1
2
  // ─── Convert to Component ─────────────────────────────────────────────────────
2
3
  import { html, render as litRender } from "lit-html";
3
4
  import { ref } from "lit-html/directives/ref.js";
4
- import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
5
- import { activeTab } from "../workspace/workspace.js";
6
- import { transact } from "../tabs/transact.js";
7
- import {
8
- computeRelativePath,
9
- loadComponentRegistry,
10
- componentRegistry,
11
- } from "../files/components.js";
12
- import { getPlatform } from "../platform.js";
13
- import { statusMessage } from "../panels/statusbar.js";
14
- import { showDialog } from "../ui/layers.js";
5
+ import { getNodeAtPath, parentElementPath, childIndex } from "../store";
6
+ import { activeTab } from "../workspace/workspace";
7
+ import { transact } from "../tabs/transact";
8
+ import { computeRelativePath, loadComponentRegistry, componentRegistry } from "../files/components";
9
+ import { getPlatform } from "../platform";
10
+ import { statusMessage } from "../panels/statusbar";
11
+ import { showDialog } from "../ui/layers";
12
+
13
+ import type { JxMutableNode } from "@jxsuite/schema/types";
15
14
 
16
15
  const VALID_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
17
16
 
@@ -40,7 +39,7 @@ export async function convertToComponent() {
40
39
  transact(tab, (doc) => {
41
40
  // Navigate to parent's children array and replace the node
42
41
  const pp = parentElementPath(selectionPath) ?? [];
43
- const idx = /** @type {number} */ (childIndex(selectionPath));
42
+ const idx = childIndex(selectionPath) as number;
44
43
  let parent = doc;
45
44
  for (const seg of pp) parent = parent[seg];
46
45
  if (!parent.children) parent.children = [];
@@ -63,8 +62,8 @@ export async function convertToComponent() {
63
62
  await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
64
63
  await loadComponentRegistry();
65
64
  statusMessage(`Converted to <${name}>`);
66
- } catch (/** @type {unknown} */ err) {
67
- statusMessage(`Error saving component: ${/** @type {Error} */ (err).message}`);
65
+ } catch (err) {
66
+ statusMessage(`Error saving component: ${(err as Error).message}`);
68
67
  }
69
68
  }
70
69
 
@@ -74,7 +73,7 @@ export async function convertToComponent() {
74
73
  * @param {JxMutableNode} node
75
74
  * @returns {string}
76
75
  */
77
- function deriveDefaultName(node) {
76
+ function deriveDefaultName(node: JxMutableNode) {
78
77
  if (node.$id && node.$id.includes("-")) return node.$id.toLowerCase();
79
78
  const tag = (node.tagName ?? "div").toLowerCase();
80
79
  return tag.includes("-") ? tag : "jx-" + tag;
@@ -86,7 +85,7 @@ function deriveDefaultName(node) {
86
85
  * @param {JxMutableNode} node
87
86
  * @returns {JxMutableNode}
88
87
  */
89
- function extractComponentDef(node) {
88
+ function extractComponentDef(node: JxMutableNode) {
90
89
  const clone = structuredClone(node);
91
90
  delete clone.$id;
92
91
  delete clone.$layout;
@@ -100,7 +99,7 @@ function extractComponentDef(node) {
100
99
  * @param {string} val
101
100
  * @returns {{ valid: boolean; error: string }}
102
101
  */
103
- function validateName(val) {
102
+ function validateName(val: string) {
104
103
  val = val.trim().toLowerCase();
105
104
  if (!val.includes("-")) {
106
105
  return { valid: false, error: "Name must contain a hyphen (e.g. my-component)" };
@@ -108,7 +107,7 @@ function validateName(val) {
108
107
  if (!VALID_NAME.test(val)) {
109
108
  return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
110
109
  }
111
- const exists = componentRegistry.some((/** @type {JxMutableNode} */ c) => c.tagName === val);
110
+ const exists = componentRegistry.some((c: JxMutableNode) => c.tagName === val);
112
111
  if (exists) {
113
112
  return { valid: false, error: `Component <${val}> already exists` };
114
113
  }
@@ -121,11 +120,11 @@ function validateName(val) {
121
120
  * @param {string} defaultName
122
121
  * @returns {Promise<string | null>}
123
122
  */
124
- function promptComponentName(defaultName) {
123
+ function promptComponentName(defaultName: string) {
125
124
  let value = defaultName;
126
125
  let error = "";
127
126
 
128
- return showDialog((done) => {
127
+ return showDialog<string | null>((done) => {
129
128
  function confirm() {
130
129
  const result = validateName(value);
131
130
  if (!result.valid) {
@@ -136,21 +135,21 @@ function promptComponentName(defaultName) {
136
135
  done(value.trim().toLowerCase());
137
136
  }
138
137
 
139
- function onInput(/** @type {Event} */ e) {
140
- value = /** @type {HTMLInputElement} */ (e.target).value || "";
138
+ function onInput(e: Event) {
139
+ value = (e.target as HTMLInputElement).value || "";
141
140
  const result = validateName(value);
142
141
  error = result.valid ? "" : result.error;
143
142
  rerender();
144
143
  }
145
144
 
146
- function onKeydown(/** @type {KeyboardEvent} */ e) {
145
+ function onKeydown(e: KeyboardEvent) {
147
146
  if (e.key === "Enter") confirm();
148
147
  }
149
148
 
150
149
  function rerender() {
151
150
  const layer = document.getElementById("layer-dialog");
152
151
  const slot = layer?.lastElementChild;
153
- if (slot) litRender(buildTpl(), /** @type {HTMLElement} */ (slot));
152
+ if (slot) litRender(buildTpl(), slot as HTMLElement);
154
153
  }
155
154
 
156
155
  function buildTpl() {
@@ -176,8 +175,8 @@ function promptComponentName(defaultName) {
176
175
  ${ref((el) => {
177
176
  if (el)
178
177
  requestAnimationFrame(() => {
179
- /** @type {HTMLElement} */ (el).focus();
180
- const input = /** @type {HTMLElement} */ (el).shadowRoot?.querySelector("input");
178
+ (el as HTMLElement).focus();
179
+ const input = (el as HTMLElement).shadowRoot?.querySelector("input");
181
180
  if (input) input.select();
182
181
  });
183
182
  })}