@jxsuite/studio 0.23.1 → 0.24.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 (105) hide show
  1. package/dist/studio.js +2231 -715
  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} +110 -85
  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} +7 -5
  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.js → edit-display.ts} +23 -20
  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} +38 -26
  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/{reactivity.js → reactivity.ts} +0 -0
@@ -1,20 +1,22 @@
1
+ /// <reference lib="dom" />
1
2
  // ─── Convert to Repeater ──────────────────────────────────────────────────────
2
3
  import { html, render as litRender, nothing } 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 { transactDoc } from "../tabs/transact.js";
7
- import { showDialog } from "../ui/layers.js";
8
- import { defCategory } from "../panels/signals-panel.js";
5
+ import { getNodeAtPath, parentElementPath, childIndex } from "../store";
6
+ import { activeTab } from "../workspace/workspace";
7
+ import { transactDoc } from "../tabs/transact";
8
+ import { showDialog } from "../ui/layers";
9
+ import { defCategory } from "../panels/signals-panel";
10
+ import { fetchPluginSchema } from "../services/code-services";
9
11
 
10
- /**
11
- * @typedef {{
12
- * items: { $ref: string } | unknown[];
13
- * filter?: { $ref: string };
14
- * sort?: { $ref: string };
15
- * newDef?: { name: string };
16
- * }} RepeaterConfig
17
- */
12
+ import type { JxMutableNode } from "@jxsuite/schema/types";
13
+
14
+ interface RepeaterConfig {
15
+ items: { $ref: string } | unknown[];
16
+ filter?: { $ref: string };
17
+ sort?: { $ref: string };
18
+ newDef?: { name: string };
19
+ }
18
20
 
19
21
  /** Convert the currently selected element into a repeater template. */
20
22
  export async function convertToRepeater() {
@@ -37,13 +39,12 @@ export async function convertToRepeater() {
37
39
  }
38
40
  const pp = parentElementPath(path);
39
41
  if (!pp) return;
40
- const idx = /** @type {number} */ (childIndex(path));
42
+ const idx = childIndex(path) as number;
41
43
  const parent = getNodeAtPath(doc, pp);
42
44
  if (!parent?.children) return;
43
45
  const element = parent.children[idx];
44
46
 
45
- /** @type {Record<string, unknown>} */
46
- const repeater = {
47
+ const repeater: Record<string, unknown> = {
47
48
  $prototype: "Array",
48
49
  items: config.items,
49
50
  map: element,
@@ -51,9 +52,9 @@ export async function convertToRepeater() {
51
52
  if (config.filter) repeater.filter = config.filter;
52
53
  if (config.sort) repeater.sort = config.sort;
53
54
 
54
- /** @type {(string | JxMutableNode)[]} */ (parent.children)[idx] = {
55
+ (parent.children as (string | JxMutableNode)[])[idx] = {
55
56
  tagName: "div",
56
- children: /** @type {any} */ (repeater),
57
+ children: repeater as unknown as (string | JxMutableNode)[],
57
58
  };
58
59
  });
59
60
  }
@@ -62,13 +63,32 @@ export async function convertToRepeater() {
62
63
  * @param {Record<string, unknown>} defs
63
64
  * @returns {Promise<RepeaterConfig | null>}
64
65
  */
65
- function promptRepeaterConfig(defs) {
66
+ async function promptRepeaterConfig(defs: Record<string, unknown>) {
66
67
  const arrayDefs = Object.entries(defs).filter(
67
68
  ([, d]) =>
68
- /** @type {any} */ (d)?.type === "array" ||
69
- Array.isArray(/** @type {any} */ (d)?.default) ||
70
- /** @type {any} */ (d)?.$prototype === "Array",
69
+ (d as Record<string, unknown> | null)?.type === "array" ||
70
+ Array.isArray((d as Record<string, unknown> | null)?.default) ||
71
+ (d as Record<string, unknown> | null)?.$prototype === "Array",
71
72
  );
73
+
74
+ const tab = activeTab.value;
75
+ const docPath = tab?.documentPath;
76
+ for (const [name, d] of Object.entries(defs)) {
77
+ const def = d as Record<string, unknown> | null;
78
+ if (!def?.$prototype || def.$prototype === "Function" || def.$prototype === "Array") continue;
79
+ if (arrayDefs.some(([n]) => n === name)) continue;
80
+ const schema = await fetchPluginSchema(
81
+ {
82
+ ...(def.$src != null && { $src: def.$src as string }),
83
+ $prototype: def.$prototype as string,
84
+ },
85
+ { ...(docPath != null && { documentPath: docPath }) },
86
+ );
87
+ if (schema?.returns?.type === "array") {
88
+ arrayDefs.push([name, d]);
89
+ }
90
+ }
91
+
72
92
  const fnDefs = Object.entries(defs).filter(([, d]) => defCategory(d) === "function");
73
93
 
74
94
  let source = arrayDefs.length > 0 ? arrayDefs[0][0] : "__new__";
@@ -77,7 +97,7 @@ function promptRepeaterConfig(defs) {
77
97
  let sortDef = "";
78
98
  let error = "";
79
99
 
80
- return showDialog((done) => {
100
+ return showDialog<RepeaterConfig | null>((done) => {
81
101
  function confirm() {
82
102
  if (source === "__new__") {
83
103
  const name = newDefName.trim();
@@ -97,16 +117,16 @@ function promptRepeaterConfig(defs) {
97
117
  return;
98
118
  }
99
119
  done({
100
- items: { $ref: `#/$defs/${name}` },
101
- filter: filterDef ? { $ref: `#/$defs/${filterDef}` } : undefined,
102
- sort: sortDef ? { $ref: `#/$defs/${sortDef}` } : undefined,
120
+ items: { $ref: `#/state/${name}` },
121
+ ...(filterDef && { filter: { $ref: `#/state/${filterDef}` } }),
122
+ ...(sortDef && { sort: { $ref: `#/state/${sortDef}` } }),
103
123
  newDef: { name },
104
124
  });
105
125
  } else {
106
126
  done({
107
- items: { $ref: `#/$defs/${source}` },
108
- filter: filterDef ? { $ref: `#/$defs/${filterDef}` } : undefined,
109
- sort: sortDef ? { $ref: `#/$defs/${sortDef}` } : undefined,
127
+ items: { $ref: `#/state/${source}` },
128
+ ...(filterDef && { filter: { $ref: `#/state/${filterDef}` } }),
129
+ ...(sortDef && { sort: { $ref: `#/state/${sortDef}` } }),
110
130
  });
111
131
  }
112
132
  }
@@ -114,7 +134,7 @@ function promptRepeaterConfig(defs) {
114
134
  function rerender() {
115
135
  const layer = document.getElementById("layer-dialog");
116
136
  const slot = layer?.lastElementChild;
117
- if (slot) litRender(buildTpl(), /** @type {HTMLElement} */ (slot));
137
+ if (slot) litRender(buildTpl(), slot as HTMLElement);
118
138
  }
119
139
 
120
140
  function buildTpl() {
@@ -137,8 +157,8 @@ function promptRepeaterConfig(defs) {
137
157
  label="Items source"
138
158
  size="s"
139
159
  .value=${source}
140
- @change=${(/** @type {Event} */ e) => {
141
- source = /** @type {HTMLInputElement} */ (e.target).value;
160
+ @change=${(e: Event) => {
161
+ source = (e.target as HTMLInputElement).value;
142
162
  error = "";
143
163
  rerender();
144
164
  }}
@@ -160,17 +180,17 @@ function promptRepeaterConfig(defs) {
160
180
  placeholder="myItems"
161
181
  .value=${newDefName}
162
182
  ?negative=${!!error}
163
- @input=${(/** @type {Event} */ e) => {
164
- newDefName = /** @type {HTMLInputElement} */ (e.target).value || "";
183
+ @input=${(e: Event) => {
184
+ newDefName = (e.target as HTMLInputElement).value || "";
165
185
  error = "";
166
186
  rerender();
167
187
  }}
168
- @keydown=${(/** @type {KeyboardEvent} */ e) => {
188
+ @keydown=${(e: KeyboardEvent) => {
169
189
  if (e.key === "Enter") confirm();
170
190
  }}
171
191
  ${ref((el) => {
172
192
  if (el && source === "__new__")
173
- requestAnimationFrame(() => /** @type {HTMLElement} */ (el).focus());
193
+ requestAnimationFrame(() => (el as HTMLElement).focus());
174
194
  })}
175
195
  >
176
196
  <sp-help-text slot="negative-help-text">${error}</sp-help-text>
@@ -186,8 +206,8 @@ function promptRepeaterConfig(defs) {
186
206
  label="Filter"
187
207
  size="s"
188
208
  .value=${filterDef}
189
- @change=${(/** @type {Event} */ e) => {
190
- filterDef = /** @type {HTMLInputElement} */ (e.target).value;
209
+ @change=${(e: Event) => {
210
+ filterDef = (e.target as HTMLInputElement).value;
191
211
  rerender();
192
212
  }}
193
213
  >
@@ -203,8 +223,8 @@ function promptRepeaterConfig(defs) {
203
223
  label="Sort"
204
224
  size="s"
205
225
  .value=${sortDef}
206
- @change=${(/** @type {Event} */ e) => {
207
- sortDef = /** @type {HTMLInputElement} */ (e.target).value;
226
+ @change=${(e: Event) => {
227
+ sortDef = (e.target as HTMLInputElement).value;
208
228
  rerender();
209
229
  }}
210
230
  >
@@ -1,3 +1,5 @@
1
+ /// <reference lib="dom" />
2
+ /// <reference lib="dom.iterable" />
1
3
  /**
2
4
  * Inline-edit.js — Contenteditable inline editing for content mode
3
5
  *
@@ -5,23 +7,39 @@
5
7
  * text formatting, Enter for new paragraphs, and slash commands for inserting elements.
6
8
  */
7
9
 
8
- /**
9
- * @typedef {{ tag: string; label: string; icon?: string; command?: string; shortcut?: string }} InlineAction
10
- *
11
- * @typedef {{ $inlineChildren?: string[]; $inlineActions?: InlineAction[] | string }} ElementDef
12
- *
13
- * @typedef {{ label: string; tag: string; description: string }} SlashCommand
14
- *
15
- * @typedef {{ textContent?: string | null; children?: (JxMutableNode | string)[] }} JxContentResult
16
- */
17
-
18
10
  import elementsMeta from "../../data/elements-meta.json";
19
- import { toggleInlineFormat, normalizeInlineContent } from "./inline-format.js";
11
+ import { toggleInlineFormat, normalizeInlineContent } from "./inline-format";
12
+ import type { JxPath } from "../state";
13
+ import type { JxMutableNode } from "@jxsuite/schema/types";
14
+ export interface InlineAction {
15
+ tag: string;
16
+ label: string;
17
+ icon?: string;
18
+ command?: string;
19
+ shortcut?: string;
20
+ }
21
+
22
+ interface ElementDef {
23
+ $inlineChildren?: string[];
24
+ $inlineActions?: InlineAction[] | string;
25
+ }
26
+
27
+ export interface SlashCommand {
28
+ label: string;
29
+ tag: string;
30
+ description: string;
31
+ }
32
+
33
+ export interface JxContentResult {
34
+ textContent?: string | null;
35
+ children?: (JxMutableNode | string)[];
36
+ }
37
+
20
38
  import {
21
39
  showSlashMenu as sharedShowSlashMenu,
22
40
  dismissSlashMenu as sharedDismissSlashMenu,
23
41
  isSlashMenuOpen,
24
- } from "./slash-menu.js";
42
+ } from "./slash-menu";
25
43
 
26
44
  // ─── Inline tag set (tags that represent rich text formatting) ─────────────
27
45
 
@@ -71,9 +89,9 @@ const EDITABLE_BLOCKS = new Set([
71
89
  * @param {string} parentTag
72
90
  * @returns {boolean}
73
91
  */
74
- export function isInlineInContext(childTag, parentTag) {
92
+ export function isInlineInContext(childTag: string, parentTag: string) {
75
93
  if (!parentTag) return INLINE_TAGS.has(childTag);
76
- const parentDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[parentTag];
94
+ const parentDef = (elementsMeta.$defs as Record<string, ElementDef>)[parentTag];
77
95
  if (!parentDef || !parentDef.$inlineChildren) return false;
78
96
  return parentDef.$inlineChildren.includes(childTag);
79
97
  }
@@ -85,12 +103,12 @@ export function isInlineInContext(childTag, parentTag) {
85
103
  * @param {string} tag
86
104
  * @returns {InlineAction[] | null}
87
105
  */
88
- export function getInlineActions(tag) {
89
- const def = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[tag];
106
+ export function getInlineActions(tag: string) {
107
+ const def = (elementsMeta.$defs as Record<string, ElementDef>)[tag];
90
108
  if (!def) return null;
91
109
  let actions = def.$inlineActions;
92
110
  if (typeof actions === "string") {
93
- const refDef = /** @type {Record<string, ElementDef>} */ (elementsMeta.$defs)[actions];
111
+ const refDef = (elementsMeta.$defs as Record<string, ElementDef>)[actions];
94
112
  actions = refDef?.$inlineActions ?? undefined;
95
113
  }
96
114
  if (!Array.isArray(actions)) return null;
@@ -99,10 +117,8 @@ export function getInlineActions(tag) {
99
117
 
100
118
  // ─── Editing state ─────────────────────────────────────────────────────────
101
119
 
102
- /** @type {HTMLElement | null} */
103
- let activeEl = null; // currently contenteditable element
104
- /** @type {JxPath | null} */
105
- let activePath = null; // JSON path to the active element
120
+ let activeEl: HTMLElement | null = null; // currently contenteditable element
121
+ let activePath: JxPath | null = null; // JSON path to the active element
106
122
  /**
107
123
  * @type {((
108
124
  * path: JxPath,
@@ -111,19 +127,28 @@ let activePath = null; // JSON path to the active element
111
127
  * ) => void)
112
128
  * | null}
113
129
  */
114
- let commitFn = null; // function(path, newChildren, newTextContent) to commit changes
130
+ let commitFn:
131
+ | ((
132
+ path: JxPath,
133
+ children: (JxMutableNode | string)[] | null,
134
+ textContent: string | null,
135
+ ) => void)
136
+ | null = null; // function(path, newChildren, newTextContent) to commit changes
115
137
  /**
116
138
  * @type {((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
117
139
  * | null}
118
140
  */
119
- let splitFn = null; // function(path, beforeChildren, afterChildren) to split paragraph
141
+ let splitFn:
142
+ | ((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
143
+ | null = null; // function(path, beforeChildren, afterChildren) to split paragraph
120
144
  /**
121
145
  * @type {((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
122
146
  * | null}
123
147
  */
124
- let insertFn = null; // function(path, elementDef, commitData?) to insert after current block
125
- /** @type {(() => void) | null} */
126
- let endFn = null; // function() called when editing stops
148
+ let insertFn:
149
+ | ((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
150
+ | null = null; // function(path, elementDef, commitData?) to insert after current block
151
+ let endFn: (() => void) | null = null; // function() called when editing stops
127
152
 
128
153
  /**
129
154
  * Check if an element is a text-bearing editable block.
@@ -131,7 +156,7 @@ let endFn = null; // function() called when editing stops
131
156
  * @param {HTMLElement} el
132
157
  * @returns {boolean}
133
158
  */
134
- export function isEditableBlock(el) {
159
+ export function isEditableBlock(el: HTMLElement) {
135
160
  return EDITABLE_BLOCKS.has(el.tagName.toLowerCase());
136
161
  }
137
162
 
@@ -143,7 +168,7 @@ export function isEditableBlock(el) {
143
168
  * @param {JxMutableNode} [parentNode]
144
169
  * @returns {boolean}
145
170
  */
146
- export function isInlineElement(node, parentNode) {
171
+ export function isInlineElement(node: JxMutableNode, parentNode?: JxMutableNode) {
147
172
  if (!node || typeof node !== "object") return false;
148
173
  const childTag = (node.tagName ?? "div").toLowerCase();
149
174
  if (parentNode) {
@@ -173,7 +198,24 @@ export function isInlineElement(node, parentNode) {
173
198
  * onEnd: () => void;
174
199
  * }} callbacks
175
200
  */
176
- export function startEditing(el, path, callbacks) {
201
+ export function startEditing(
202
+ el: HTMLElement,
203
+ path: JxPath,
204
+ callbacks: {
205
+ onCommit: (
206
+ path: JxPath,
207
+ children: (JxMutableNode | string)[] | null,
208
+ textContent: string | null,
209
+ ) => void;
210
+ onSplit: (
211
+ path: JxPath,
212
+ beforeChildren: JxContentResult,
213
+ afterChildren: JxContentResult,
214
+ ) => void;
215
+ onInsert: (path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void;
216
+ onEnd: () => void;
217
+ },
218
+ ) {
177
219
  if (activeEl) stopEditing();
178
220
 
179
221
  activeEl = el;
@@ -259,7 +301,7 @@ export function getActiveElement() {
259
301
  // ─── Event handlers ────────────────────────────────────────────────────────
260
302
 
261
303
  /** @param {KeyboardEvent} e */
262
- function handleKeydown(e) {
304
+ function handleKeydown(e: KeyboardEvent) {
263
305
  if (e.key === "Escape") {
264
306
  e.preventDefault();
265
307
  e.stopPropagation();
@@ -325,7 +367,7 @@ function handleInput() {
325
367
  }
326
368
 
327
369
  /** @param {FocusEvent} _e */
328
- function handleBlur(_e) {
370
+ function handleBlur(_e: FocusEvent) {
329
371
  // Don't close if focus moved to slash menu
330
372
  if (isSlashMenuOpen()) return;
331
373
 
@@ -338,7 +380,7 @@ function handleBlur(_e) {
338
380
  }
339
381
 
340
382
  /** @param {ClipboardEvent} e */
341
- function handlePaste(e) {
383
+ function handlePaste(e: ClipboardEvent) {
342
384
  e.preventDefault();
343
385
  // Paste as plain text to avoid foreign HTML
344
386
  const text = e.clipboardData?.getData("text/plain") ?? "";
@@ -412,7 +454,7 @@ function commitChanges() {
412
454
  * @param {{ children?: (JxMutableNode | string)[] }} node
413
455
  * @returns {JxContentResult}
414
456
  */
415
- export function normalizeChildren(node) {
457
+ export function normalizeChildren(node: { children?: (JxMutableNode | string)[] }) {
416
458
  if (!Array.isArray(node.children) || node.children.length === 0) return { textContent: "" };
417
459
 
418
460
  // Step 1: Merge adjacent text nodes
@@ -430,7 +472,7 @@ export function normalizeChildren(node) {
430
472
  }
431
473
 
432
474
  // Step 2: If all children are text, fold into textContent
433
- if (merged.every((/** @type {JxMutableNode | string} */ c) => typeof c === "string")) {
475
+ if (merged.every((c: JxMutableNode | string) => typeof c === "string")) {
434
476
  return { textContent: merged.join("") };
435
477
  }
436
478
 
@@ -444,7 +486,7 @@ export function normalizeChildren(node) {
444
486
  * @param {HTMLElement} el
445
487
  * @returns {JxContentResult}
446
488
  */
447
- function elementToJx(el) {
489
+ function elementToJx(el: HTMLElement): JxContentResult {
448
490
  const nodes = el.childNodes;
449
491
 
450
492
  // If just a single text node, use textContent
@@ -454,8 +496,7 @@ function elementToJx(el) {
454
496
  }
455
497
 
456
498
  // Mixed content → children array
457
- /** @type {(JxMutableNode | string)[]} */
458
- const children = [];
499
+ const children: (JxMutableNode | string)[] = [];
459
500
  for (const child of nodes) {
460
501
  const jsx = domNodeToJx(child);
461
502
  if (jsx !== null && jsx !== undefined) children.push(jsx);
@@ -471,7 +512,7 @@ function elementToJx(el) {
471
512
  * @param {Node} node
472
513
  * @returns {JxMutableNode | string | null}
473
514
  */
474
- function domNodeToJx(node) {
515
+ function domNodeToJx(node: Node) {
475
516
  if (node.nodeType === Node.TEXT_NODE) {
476
517
  const text = node.textContent;
477
518
  if (!text) return null;
@@ -480,21 +521,18 @@ function domNodeToJx(node) {
480
521
 
481
522
  if (node.nodeType !== Node.ELEMENT_NODE) return null;
482
523
 
483
- const el = /** @type {HTMLElement} */ (node);
524
+ const el = node as HTMLElement;
484
525
  const tag = el.tagName.toLowerCase();
485
- /** @type {JxMutableNode} */
486
- const result = { tagName: tag };
526
+ const result: JxMutableNode = { tagName: tag };
487
527
 
488
528
  // Map browser execCommand output to our tag conventions
489
- /** @type {Record<string, string>} */
490
- const tagMap = { b: "strong", i: "em", s: "del", strike: "del" };
529
+ const tagMap: Record<string, string> = { b: "strong", i: "em", s: "del", strike: "del" };
491
530
  if (tagMap[tag]) result.tagName = tagMap[tag];
492
531
 
493
532
  // Attributes
494
- if (tag === "a" && /** @type {HTMLAnchorElement} */ (el).href) {
533
+ if (tag === "a" && (el as HTMLAnchorElement).href) {
495
534
  result.attributes = { href: el.getAttribute("href") };
496
- if (/** @type {HTMLAnchorElement} */ (el).title)
497
- result.attributes.title = /** @type {HTMLAnchorElement} */ (el).title;
535
+ if ((el as HTMLAnchorElement).title) result.attributes.title = (el as HTMLAnchorElement).title;
498
536
  }
499
537
  if (tag === "code") {
500
538
  result.textContent = el.textContent;
@@ -524,15 +562,14 @@ function domNodeToJx(node) {
524
562
  * @param {DocumentFragment} frag
525
563
  * @returns {JxContentResult}
526
564
  */
527
- function fragmentToJx(frag) {
565
+ function fragmentToJx(frag: DocumentFragment) {
528
566
  const nodes = frag.childNodes;
529
567
  if (nodes.length === 0) return { textContent: "" };
530
568
  if (nodes.length === 1 && nodes[0].nodeType === Node.TEXT_NODE) {
531
569
  return { textContent: nodes[0].textContent };
532
570
  }
533
571
 
534
- /** @type {(JxMutableNode | string)[]} */
535
- const children = [];
572
+ const children: (JxMutableNode | string)[] = [];
536
573
  for (const child of nodes) {
537
574
  const jsx = domNodeToJx(child);
538
575
  if (jsx) children.push(jsx);
@@ -556,9 +593,9 @@ function fragmentToJx(frag) {
556
593
  * @param {Range} range
557
594
  * @returns {string}
558
595
  */
559
- function getTextBeforeCursor(range) {
596
+ function getTextBeforeCursor(range: Range) {
560
597
  const preRange = document.createRange();
561
- preRange.setStart(/** @type {Node} */ (activeEl), 0);
598
+ preRange.setStart(activeEl!, 0);
562
599
  preRange.setEnd(range.startContainer, range.startOffset);
563
600
  return preRange.toString();
564
601
  }
@@ -602,7 +639,7 @@ function updateSlashMenu() {
602
639
  }
603
640
 
604
641
  /** @param {SlashCommand} cmd */
605
- function handleSlashSelect(cmd) {
642
+ function handleSlashSelect(cmd: SlashCommand) {
606
643
  if (!activeEl || !insertFn || !activePath) return;
607
644
 
608
645
  // Remove the /command text from the element
@@ -614,11 +651,10 @@ function handleSlashSelect(cmd) {
614
651
  if (slashIdx >= 0) {
615
652
  const walker = document.createTreeWalker(activeEl, NodeFilter.SHOW_TEXT);
616
653
  let charCount = 0;
617
- /** @type {Text | null} */
618
- let slashNode = null;
654
+ let slashNode: Text | null = null;
619
655
  let slashOffset = 0;
620
656
  while (walker.nextNode()) {
621
- const node = /** @type {Text} */ (walker.currentNode);
657
+ const node = walker.currentNode as Text;
622
658
  if (charCount + node.length > slashIdx) {
623
659
  slashNode = node;
624
660
  slashOffset = slashIdx - charCount;