@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
@@ -7,13 +7,18 @@ import { classMap } from "lit-html/directives/class-map.js";
7
7
  const expandedDataKeys = new Set();
8
8
 
9
9
  /** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
10
- function unwrapSignal(/** @type {any} */ value) {
11
- if (value && typeof value === "object" && value.__v_isRef) return value.value;
10
+ function unwrapSignal(/** @type {unknown} */ value) {
11
+ if (
12
+ value &&
13
+ typeof value === "object" &&
14
+ /** @type {Record<string, unknown>} */ (value).__v_isRef
15
+ )
16
+ return /** @type {Record<string, unknown>} */ (value).value;
12
17
  return value;
13
18
  }
14
19
 
15
20
  /** Type label for a signal value in the data explorer. */
16
- function dataTypeLabel(/** @type {any} */ value) {
21
+ function dataTypeLabel(/** @type {unknown} */ value) {
17
22
  const v = unwrapSignal(value);
18
23
  if (v === null) return "null";
19
24
  if (v === undefined) return "pending";
@@ -25,15 +30,15 @@ function dataTypeLabel(/** @type {any} */ value) {
25
30
  /**
26
31
  * Render the data explorer tab showing live resolved values.
27
32
  *
28
- * @param {Record<string, any>} state - S.document.state (the $defs definitions)
29
- * @param {Record<string, any> | null} liveScope - Cached live scope from runtime rendering
33
+ * @param {Record<string, unknown>} state - S.document.state (the $defs definitions)
34
+ * @param {Record<string, unknown> | null} liveScope - Cached live scope from runtime rendering
30
35
  * @param {{
31
36
  * renderCanvas: () => void;
32
37
  * renderLeftPanel: () => void;
33
- * defCategory: (def: any) => string;
34
- * defBadgeLabel: (def: any) => string;
38
+ * defCategory: (def: unknown) => string;
39
+ * defBadgeLabel: (def: unknown) => string;
35
40
  * }} callbacks
36
- * @returns {any}
41
+ * @returns {import("lit-html").TemplateResult}
37
42
  */
38
43
  export function renderDataExplorerTemplate(state, liveScope, callbacks) {
39
44
  const { renderCanvas, renderLeftPanel, defCategory, defBadgeLabel } = callbacks;
@@ -91,11 +96,11 @@ export function renderDataExplorerTemplate(state, liveScope, callbacks) {
91
96
  /**
92
97
  * Recursively render a JSON value as a tree view (Lit template).
93
98
  *
94
- * @returns {any}
99
+ * @returns {import("lit-html").TemplateResult}
95
100
  */
96
101
  export function renderDataTreeTemplate(
97
- /** @type {any} */ value,
98
- /** @type {any} */ depth,
102
+ /** @type {unknown} */ value,
103
+ /** @type {number} */ depth,
99
104
  maxDepth = 5,
100
105
  ) {
101
106
  const indent = `${(depth + 1) * 12}px`;
@@ -150,10 +155,11 @@ export function renderDataTreeTemplate(
150
155
  }
151
156
 
152
157
  // Object
153
- const keys = Object.keys(value);
158
+ const obj = /** @type {Record<string, unknown>} */ (value);
159
+ const keys = Object.keys(obj);
154
160
  const cap = 30;
155
161
  const items = keys.slice(0, cap).map((key) => {
156
- const v = value[key];
162
+ const v = obj[key];
157
163
  if (v === null || v === undefined || typeof v !== "object") {
158
164
  const valText =
159
165
  typeof v === "string" && v.length > 80 ? `"${v.slice(0, 80)}\u2026"` : JSON.stringify(v);
package/src/panels/dnd.js CHANGED
@@ -3,6 +3,19 @@
3
3
  * on layer rows, component cards, and element cards.
4
4
  */
5
5
 
6
+ /**
7
+ * @typedef {{ element: HTMLElement; input: { clientX: number; clientY: number } }} DragCanDragArgs
8
+ *
9
+ * @typedef {{ source: { data: Record<string, unknown>; element: HTMLElement } }} DragDropSourceArgs
10
+ *
11
+ * @typedef {{ self: { data: Record<string, unknown> } }} DragSelfArgs
12
+ *
13
+ * @typedef {{
14
+ * source: { data: Record<string, unknown>; element: HTMLElement };
15
+ * location: { current: { dropTargets: { data: Record<string, unknown> }[] } };
16
+ * }} DragMonitorDropArgs
17
+ */
18
+
6
19
  import {
7
20
  draggable,
8
21
  dropTargetForElements,
@@ -32,14 +45,18 @@ import { defaultDef, unsafeTags } from "./shared.js";
32
45
  /** Register DnD on layer rows — called from left-panel.js after render */
33
46
  export function registerLayersDnD() {
34
47
  requestAnimationFrame(() => {
35
- const container = /** @type {any} */ (leftPanel)?.querySelector(".layers-container");
48
+ const container = /** @type {HTMLElement | null} */ (
49
+ leftPanel?.querySelector(".layers-container")
50
+ );
36
51
  if (!container) return;
37
52
 
38
- container.querySelectorAll("[data-dnd-row]").forEach(
39
- /** @param {any} row */ (row) => {
40
- const rowPath = /** @type {string} */ (row.dataset.path)
41
- .split("/")
42
- .map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
53
+ /** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll("[data-dnd-row]")).forEach(
54
+ (row) => {
55
+ const rowPath = /** @type {JxPath} */ (
56
+ /** @type {string} */ (row.dataset.path)
57
+ .split("/")
58
+ .map((/** @type {string} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s))
59
+ );
43
60
  const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
44
61
  const isVoid = row.hasAttribute("data-dnd-void");
45
62
  const isExpanded = row.hasAttribute("data-dnd-expanded");
@@ -47,7 +64,7 @@ export function registerLayersDnD() {
47
64
  const cleanup = combine(
48
65
  draggable({
49
66
  element: row,
50
- canDrag(/** @type {any} */ { element: _el, input }) {
67
+ canDrag(/** @type {DragCanDragArgs} */ { element: _el, input }) {
51
68
  const target = /** @type {HTMLElement} */ (
52
69
  document.elementFromPoint(input.clientX, input.clientY)
53
70
  );
@@ -73,28 +90,30 @@ export function registerLayersDnD() {
73
90
  }),
74
91
  dropTargetForElements({
75
92
  element: row,
76
- canDrop(/** @type {any} */ { source }) {
77
- const srcPath = source.data.path;
93
+ canDrop(/** @type {DragDropSourceArgs} */ { source }) {
94
+ const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
78
95
  if (srcPath && isAncestor(srcPath, rowPath)) return false;
79
96
  return true;
80
97
  },
81
98
  getData(/** @type {any} */ { input, element }) {
82
- return attachInstruction(
83
- { path: rowPath },
84
- /** @type {any} */ ({
85
- input,
86
- element,
87
- currentLevel: rowDepth,
88
- indentPerLevel: 16,
89
- mode: isExpanded ? "expanded" : "standard",
90
- block: isVoid ? ["make-child"] : [],
91
- }),
99
+ return /** @type {Record<string | symbol, unknown>} */ (
100
+ attachInstruction(
101
+ { path: rowPath },
102
+ {
103
+ input,
104
+ element,
105
+ currentLevel: rowDepth,
106
+ indentPerLevel: 16,
107
+ mode: isExpanded ? "expanded" : "standard",
108
+ block: isVoid ? ["make-child"] : [],
109
+ },
110
+ )
92
111
  );
93
112
  },
94
- onDragEnter(/** @type {any} */ { self }) {
113
+ onDragEnter(/** @type {DragSelfArgs} */ { self }) {
95
114
  showLayerDropGap(row, self.data, container);
96
115
  },
97
- onDrag(/** @type {any} */ { self }) {
116
+ onDrag(/** @type {DragSelfArgs} */ { self }) {
98
117
  showLayerDropGap(row, self.data, container);
99
118
  },
100
119
  onDragLeave() {
@@ -111,14 +130,14 @@ export function registerLayersDnD() {
111
130
 
112
131
  // Global monitor
113
132
  const monitorCleanup = monitorForElements({
114
- onDrop(/** @type {any} */ { source, location }) {
133
+ onDrop(/** @type {DragMonitorDropArgs} */ { source, location }) {
115
134
  clearLayerDropGap(container);
116
135
  const target = location.current.dropTargets[0];
117
136
  if (!target) return;
118
137
  const instruction = extractInstruction(target.data);
119
138
  if (!instruction || instruction.type === "instruction-blocked") return;
120
139
  const srcData = source.data;
121
- const targetPath = target.data.path;
140
+ const targetPath = /** @type {JxPath} */ (target.data.path);
122
141
 
123
142
  // If the source had children, persist collapse at the new location
124
143
  const srcRow = srcData.type === "tree-node" && source.element;
@@ -143,56 +162,62 @@ export function registerLayersDnD() {
143
162
  /** Register DnD on component rows — called from renderLeftPanel when tab=components */
144
163
  export function registerComponentsDnD() {
145
164
  requestAnimationFrame(() => {
146
- const container = /** @type {any} */ (leftPanel)?.querySelector(".components-section");
165
+ const container = /** @type {HTMLElement | null} */ (
166
+ leftPanel?.querySelector(".components-section")
167
+ );
147
168
  if (!container) return;
148
169
 
149
- container.querySelectorAll("[data-component-tag]").forEach(
150
- /** @param {any} row */ (row) => {
151
- const tagName = row.dataset.componentTag;
152
- if (!tagName) return;
153
- const comp = componentRegistry.find(/** @param {any} c */ (c) => c.tagName === tagName);
154
- if (!comp) return;
170
+ /** @type {NodeListOf<HTMLElement>} */ (
171
+ container.querySelectorAll("[data-component-tag]")
172
+ ).forEach((row) => {
173
+ const tagName = row.dataset.componentTag;
174
+ if (!tagName) return;
175
+ const comp = componentRegistry.find(
176
+ (/** @type {import("../files/components.js").ComponentEntry} */ c) => c.tagName === tagName,
177
+ );
178
+ if (!comp) return;
155
179
 
156
- // Fill preview with live rendered component
157
- const preview = row.querySelector(".element-card-preview");
158
- if (preview && !preview.querySelector(tagName)) {
159
- renderComponentPreview(comp).then((/** @type {any} */ el) => {
160
- preview.textContent = "";
161
- preview.appendChild(el);
162
- });
163
- }
180
+ // Fill preview with live rendered component
181
+ const preview = row.querySelector(".element-card-preview");
182
+ if (preview && !preview.querySelector(tagName)) {
183
+ renderComponentPreview(comp).then((/** @type {HTMLElement} */ el) => {
184
+ preview.textContent = "";
185
+ preview.appendChild(el);
186
+ });
187
+ }
164
188
 
165
- const instanceDef = {
166
- tagName: comp.tagName,
167
- $props: comp.props
168
- ? Object.fromEntries(
169
- comp.props.map((/** @type {any} */ p) => [
189
+ const instanceDef = {
190
+ tagName: comp.tagName,
191
+ $props: comp.props
192
+ ? Object.fromEntries(
193
+ comp.props.map(
194
+ (/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
170
195
  p.name,
171
196
  p.default !== undefined ? p.default : "",
172
- ]),
173
- )
174
- : {},
175
- };
176
- const cleanup = draggable({
177
- element: row,
178
- getInitialData() {
179
- return { type: "block", fragment: structuredClone(instanceDef) };
180
- },
181
- });
182
- view.dndCleanups.push(cleanup);
183
- },
184
- );
197
+ ],
198
+ ),
199
+ )
200
+ : {},
201
+ };
202
+ const cleanup = draggable({
203
+ element: row,
204
+ getInitialData() {
205
+ return { type: "block", fragment: structuredClone(instanceDef) };
206
+ },
207
+ });
208
+ view.dndCleanups.push(cleanup);
209
+ });
185
210
  });
186
211
  }
187
212
 
188
213
  /** Register DnD on element (HTML block) rows */
189
214
  export function registerElementsDnD() {
190
215
  requestAnimationFrame(() => {
191
- const container = /** @type {any} */ (leftPanel)?.querySelector(".panel-body");
216
+ const container = /** @type {HTMLElement | null} */ (leftPanel?.querySelector(".panel-body"));
192
217
  if (!container) return;
193
- container.querySelectorAll("[data-block-tag]").forEach(
194
- /** @param {any} row */ (row) => {
195
- const tag = row.dataset.blockTag;
218
+ /** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll("[data-block-tag]")).forEach(
219
+ (row) => {
220
+ const tag = /** @type {string} */ (row.dataset.blockTag);
196
221
  const preview = row.querySelector(".element-card-preview");
197
222
  if (preview && !preview.firstChild) {
198
223
  const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
@@ -215,23 +240,23 @@ export function registerElementsDnD() {
215
240
  /**
216
241
  * Hide descendant rows of the dragged item so it appears collapsed during drag.
217
242
  *
218
- * @param {any} parentRow
219
- * @param {any} container
243
+ * @param {HTMLElement} parentRow
244
+ * @param {HTMLElement} container
220
245
  */
221
246
  function hideDescendantRows(parentRow, container) {
222
247
  const prefix = parentRow.dataset.path + "/";
223
248
  const rows = container.querySelectorAll(".layers-tree .layer-row");
224
249
  for (const r of rows) {
225
- if (/** @type {any} */ (r).dataset.path?.startsWith(prefix)) {
226
- /** @type {any} */ (r).style.display = "none";
250
+ if (/** @type {HTMLElement} */ (r).dataset.path?.startsWith(prefix)) {
251
+ /** @type {HTMLElement} */ (r).style.display = "none";
227
252
  }
228
253
  }
229
254
  }
230
255
 
231
256
  /**
232
- * @param {any} rowEl
233
- * @param {any} data
234
- * @param {any} container
257
+ * @param {HTMLElement} rowEl
258
+ * @param {Record<string, unknown>} data
259
+ * @param {HTMLElement} container
235
260
  */
236
261
  export function showLayerDropGap(rowEl, data, container) {
237
262
  const instruction = extractInstruction(data);
@@ -262,37 +287,39 @@ export function showLayerDropGap(rowEl, data, container) {
262
287
  const gap = view.layerDragSourceHeight;
263
288
 
264
289
  for (let i = 0; i < rows.length; i++) {
265
- if (/** @type {any} */ (rows[i]).classList.contains("dragging")) continue;
290
+ if (/** @type {HTMLElement} */ (rows[i]).classList.contains("dragging")) continue;
266
291
  if (instruction.type === "reorder-above") {
267
- /** @type {any} */ (rows[i]).style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
292
+ /** @type {HTMLElement} */ (rows[i]).style.transform =
293
+ i >= targetIdx ? `translateY(${gap}px)` : "";
268
294
  } else {
269
- /** @type {any} */ (rows[i]).style.transform = i > targetIdx ? `translateY(${gap}px)` : "";
295
+ /** @type {HTMLElement} */ (rows[i]).style.transform =
296
+ i > targetIdx ? `translateY(${gap}px)` : "";
270
297
  }
271
298
  }
272
299
  }
273
300
 
274
- /** @param {any} container */
301
+ /** @param {HTMLElement} container */
275
302
  export function clearLayerDropGap(container) {
276
303
  if (view._currentDropTargetRow) {
277
304
  view._currentDropTargetRow.classList.remove("drop-target");
278
305
  view._currentDropTargetRow = null;
279
306
  }
280
307
  const rows = container.querySelectorAll(".layers-tree .layer-row");
281
- for (const r of rows) /** @type {any} */ (r).style.transform = "";
308
+ for (const r of rows) /** @type {HTMLElement} */ (r).style.transform = "";
282
309
  }
283
310
 
284
311
  /**
285
312
  * Apply a DnD instruction to the state
286
313
  *
287
- * @param {any} instruction
288
- * @param {any} srcData
289
- * @param {any} targetPath
314
+ * @param {{ type: string }} instruction
315
+ * @param {Record<string, unknown>} srcData
316
+ * @param {JxPath} targetPath
290
317
  */
291
318
  export function applyDropInstruction(instruction, srcData, targetPath) {
292
- const document = activeTab.value?.doc.document;
319
+ const doc = /** @type {JxMutableNode} */ (activeTab.value?.doc.document);
293
320
  if (srcData.type === "tree-node") {
294
- const fromPath = srcData.path;
295
- const targetParent = /** @type {any} */ (parentElementPath(targetPath));
321
+ const fromPath = /** @type {JxPath} */ (srcData.path);
322
+ const targetParent = /** @type {JxPath} */ (parentElementPath(targetPath));
296
323
  const targetIdx = /** @type {number} */ (childIndex(targetPath));
297
324
 
298
325
  switch (instruction.type) {
@@ -305,64 +332,89 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
305
332
  );
306
333
  break;
307
334
  case "make-child": {
308
- const target = getNodeAtPath(document, targetPath);
335
+ const target = getNodeAtPath(doc, targetPath);
309
336
  const len = target?.children?.length || 0;
310
337
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
311
338
  break;
312
339
  }
313
340
  }
314
341
  } else if (srcData.type === "block") {
315
- const targetParent = /** @type {any} */ (parentElementPath(targetPath));
342
+ const targetParent = /** @type {JxPath} */ (parentElementPath(targetPath));
316
343
  const targetIdx = /** @type {number} */ (childIndex(targetPath));
317
344
 
318
345
  switch (instruction.type) {
319
346
  case "reorder-above":
320
347
  transactDoc(activeTab.value, (t) =>
321
- mutateInsertNode(t, targetParent, targetIdx, structuredClone(srcData.fragment)),
348
+ mutateInsertNode(
349
+ t,
350
+ targetParent,
351
+ targetIdx,
352
+ structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
353
+ ),
322
354
  );
323
355
  break;
324
356
  case "reorder-below":
325
357
  transactDoc(activeTab.value, (t) =>
326
- mutateInsertNode(t, targetParent, targetIdx + 1, structuredClone(srcData.fragment)),
358
+ mutateInsertNode(
359
+ t,
360
+ targetParent,
361
+ targetIdx + 1,
362
+ structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
363
+ ),
327
364
  );
328
365
  break;
329
366
  case "make-child": {
330
- const target = getNodeAtPath(document, targetPath);
367
+ const target = getNodeAtPath(doc, targetPath);
331
368
  const len = target?.children?.length || 0;
332
369
  transactDoc(activeTab.value, (t) =>
333
- mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment)),
370
+ mutateInsertNode(
371
+ t,
372
+ targetPath,
373
+ len,
374
+ structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
375
+ ),
334
376
  );
335
377
  break;
336
378
  }
337
379
  }
338
380
 
339
381
  // Auto-import to $elements if the dropped block is a custom component
340
- const tag = srcData.fragment?.tagName;
382
+ const fragment = /** @type {JxMutableNode | undefined} */ (srcData.fragment);
383
+ const tag = fragment?.tagName;
341
384
  if (tag && tag.includes("-")) {
342
- const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
385
+ const comp = componentRegistry.find(
386
+ (/** @type {import("../files/components.js").ComponentEntry} */ c) => c.tagName === tag,
387
+ );
343
388
  if (comp) {
344
389
  const tab = activeTab.value;
345
390
  const elements = tab?.doc.document?.$elements || [];
346
391
  if (comp.source === "npm") {
347
392
  const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
393
+ if (!specifier) return;
348
394
  const alreadyImported = elements.some(
349
- (/** @type {any} */ e) => e === specifier || e === comp.package,
395
+ (/** @type {JxMutableNode | string | { $ref: string }} */ e) =>
396
+ e === specifier || e === comp.package,
350
397
  );
351
398
  if (!alreadyImported) {
352
- transact(activeTab.value, (/** @type {any} */ doc) => {
399
+ transact(activeTab.value, (/** @type {JxMutableNode} */ doc) => {
353
400
  if (!doc.$elements) doc.$elements = [];
354
401
  doc.$elements.push(specifier);
355
402
  });
356
403
  }
357
404
  } else {
358
405
  const alreadyImported = elements.some(
359
- (/** @type {any} */ e) =>
360
- e.$ref &&
361
- (e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
406
+ (/** @type {JxMutableNode | string | { $ref: string }} */ e) => {
407
+ const ref = typeof e === "object" && e !== null ? e.$ref : undefined;
408
+ return (
409
+ ref &&
410
+ (ref === `./${comp.path}` ||
411
+ ref.endsWith(/** @type {string} */ (comp.path.split("/").pop())))
412
+ );
413
+ },
362
414
  );
363
415
  if (!alreadyImported) {
364
416
  const relPath = computeRelativePath(tab?.documentPath, comp.path);
365
- transact(activeTab.value, (/** @type {any} */ doc) => {
417
+ transact(activeTab.value, (/** @type {JxMutableNode} */ doc) => {
366
418
  if (!doc.$elements) doc.$elements = [];
367
419
  doc.$elements.push({ $ref: relPath });
368
420
  });
@@ -3,6 +3,8 @@
3
3
  * completion provider for state scope variables.
4
4
  */
5
5
 
6
+ /** @typedef {import("../services/code-services.js").OxLintDiagnostic} OxLintDiagnostic */
7
+
6
8
  import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
7
9
  import { html, render as litRender, nothing } from "lit-html";
8
10
  import { ref } from "lit-html/directives/ref.js";
@@ -13,12 +15,15 @@ import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "../tabs/tran
13
15
  import { view } from "../view.js";
14
16
  import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
15
17
 
16
- function getFunctionBody(/** @type {any} */ editing) {
18
+ /** @typedef {{ type: "def"; defName: string } | { type: "event"; path: JxPath; eventKey: string }} EditingTarget */
19
+
20
+ /** @param {EditingTarget | null | undefined} editing */
21
+ function getFunctionBody(editing) {
17
22
  const document = activeTab.value?.doc.document;
18
- if (editing.type === "def") {
23
+ if (editing?.type === "def") {
19
24
  return document?.state?.[editing.defName]?.body || "";
20
- } else if (editing.type === "event") {
21
- const node = getNodeAtPath(document, editing.path);
25
+ } else if (editing?.type === "event") {
26
+ const node = getNodeAtPath(/** @type {JxMutableNode} */ (document), editing.path);
22
27
  return node?.[editing.eventKey]?.body || "";
23
28
  }
24
29
  return "";
@@ -26,7 +31,9 @@ function getFunctionBody(/** @type {any} */ editing) {
26
31
 
27
32
  /** @param {() => void} closeFunctionEditor */
28
33
  export function renderFunctionEditor(closeFunctionEditor) {
29
- const editing = activeTab.value?.session.ui.editingFunction;
34
+ const editing = /** @type {EditingTarget | null | undefined} */ (
35
+ activeTab.value?.session.ui.editingFunction
36
+ );
30
37
 
31
38
  // If editor already exists and matches current target, just sync value
32
39
  if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
@@ -66,7 +73,7 @@ export function renderFunctionEditor(closeFunctionEditor) {
66
73
 
67
74
  const tab = activeTab.value;
68
75
  const docName = tab?.documentPath?.split("/").pop() || tab?.doc.document?.tagName || "document";
69
- const ed = /** @type {any} */ (editing);
76
+ const ed = /** @type {EditingTarget} */ (editing);
70
77
  const funcLabel = ed.type === "def" ? `ƒ ${ed.defName}` : `ƒ ${ed.eventKey}`;
71
78
 
72
79
  // Editor container
@@ -94,22 +101,29 @@ export function renderFunctionEditor(closeFunctionEditor) {
94
101
  );
95
102
 
96
103
  const body = getFunctionBody(editing);
97
- const args = getFunctionArgs(editing, activeTab.value?.doc.document);
98
-
99
- view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
100
- value: body,
101
- language: "javascript",
102
- theme: "vs-dark",
103
- automaticLayout: true,
104
- minimap: { enabled: false },
105
- fontSize: 12,
106
- fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
107
- lineNumbers: "on",
108
- scrollBeyondLastLine: false,
109
- wordWrap: "on",
110
- tabSize: 2,
111
- });
104
+ const args = getFunctionArgs(
105
+ /** @type {EditingTarget} */ (editing),
106
+ activeTab.value?.doc.document,
107
+ );
108
+
109
+ view.functionEditor = monaco.editor.create(
110
+ /** @type {HTMLElement} */ (/** @type {unknown} */ (editorContainer)),
111
+ {
112
+ value: body,
113
+ language: "javascript",
114
+ theme: "vs-dark",
115
+ automaticLayout: true,
116
+ minimap: { enabled: false },
117
+ fontSize: 12,
118
+ fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
119
+ lineNumbers: "on",
120
+ scrollBeyondLastLine: false,
121
+ wordWrap: "on",
122
+ tabSize: 2,
123
+ },
124
+ );
112
125
  view.functionEditor._editingTarget = JSON.stringify(editing);
126
+ const editor = view.functionEditor;
113
127
 
114
128
  // Format on open — show pretty-printed code, then run initial lint
115
129
  codeService("format", { code: body, args }).then((result) => {
@@ -120,29 +134,32 @@ export function renderFunctionEditor(closeFunctionEditor) {
120
134
  });
121
135
  codeService("lint", { code: body, args }).then((result) => {
122
136
  if (result?.diagnostics && view.functionEditor)
123
- setLintMarkers(view.functionEditor, result.diagnostics);
137
+ setLintMarkers(view.functionEditor, /** @type {OxLintDiagnostic[]} */ (result.diagnostics));
124
138
  });
125
139
 
126
140
  // Debounced sync back to state + lint on edit
127
- /** @type {any} */
141
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
128
142
  let syncDebounce;
129
- /** @type {any} */
143
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
130
144
  let lintDebounce;
131
145
  let lintGen = 0;
132
- view.functionEditor.onDidChangeModelContent(() => {
133
- if (view.functionEditor._ignoreNextChange) {
134
- view.functionEditor._ignoreNextChange = false;
146
+ editor.onDidChangeModelContent(() => {
147
+ if (editor._ignoreNextChange) {
148
+ editor._ignoreNextChange = false;
135
149
  return;
136
150
  }
137
151
 
138
152
  clearTimeout(syncDebounce);
139
153
  syncDebounce = setTimeout(() => {
140
- const newBody = view.functionEditor.getValue();
141
- const ed = /** @type {any} */ (editing);
154
+ const newBody = editor.getValue();
155
+ const ed = /** @type {EditingTarget} */ (editing);
142
156
  if (ed.type === "def") {
143
157
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, ed.defName, { body: newBody }));
144
158
  } else if (ed.type === "event") {
145
- const node = getNodeAtPath(activeTab.value?.doc.document, ed.path);
159
+ const node = getNodeAtPath(
160
+ /** @type {JxMutableNode} */ (activeTab.value?.doc.document),
161
+ ed.path,
162
+ );
146
163
  const current = node?.[ed.eventKey] || {};
147
164
  transactDoc(activeTab.value, (t) =>
148
165
  mutateUpdateProperty(t, ed.path, ed.eventKey, {
@@ -158,11 +175,14 @@ export function renderFunctionEditor(closeFunctionEditor) {
158
175
  clearTimeout(lintDebounce);
159
176
  lintDebounce = setTimeout(() => {
160
177
  const gen = ++lintGen;
161
- const currentCode = view.functionEditor.getValue();
178
+ const currentCode = editor.getValue();
162
179
  codeService("lint", { code: currentCode, args }).then((result) => {
163
180
  if (gen !== lintGen) return;
164
181
  if (result?.diagnostics && view.functionEditor)
165
- setLintMarkers(view.functionEditor, result.diagnostics);
182
+ setLintMarkers(
183
+ view.functionEditor,
184
+ /** @type {OxLintDiagnostic[]} */ (result.diagnostics),
185
+ );
166
186
  });
167
187
  }, 750);
168
188
  });
@@ -187,11 +207,11 @@ export function registerFunctionCompletions() {
187
207
  const suggestions = Object.entries(defs).map(([key, def]) => {
188
208
  let kind = monaco.languages.CompletionItemKind.Variable;
189
209
  if (
190
- /** @type {any} */ (def)?.$prototype === "Function" ||
191
- /** @type {any} */ (def)?.$handler
210
+ /** @type {JxPrototypeDef} */ (def)?.$prototype === "Function" ||
211
+ /** @type {{ $handler?: unknown }} */ (def)?.$handler
192
212
  )
193
213
  kind = monaco.languages.CompletionItemKind.Function;
194
- else if (/** @type {any} */ (def)?.$prototype)
214
+ else if (/** @type {JxPrototypeDef} */ (def)?.$prototype)
195
215
  kind = monaco.languages.CompletionItemKind.Property;
196
216
  return {
197
217
  label: `state.${key}`,