@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
package/src/panels/dnd.ts CHANGED
@@ -16,14 +16,15 @@ import {
16
16
  } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
17
17
 
18
18
  import {
19
- leftPanel,
20
- getNodeAtPath,
21
- parentElementPath,
22
19
  childIndex,
20
+ childList,
21
+ getNodeAtPath,
23
22
  isAncestor,
23
+ leftPanel,
24
+ parentElementPath,
24
25
  renderOnly,
25
26
  } from "../store";
26
- import { transact, transactDoc, mutateMoveNode, mutateInsertNode } from "../tabs/transact";
27
+ import { mutateInsertNode, mutateMoveNode, transact, transactDoc } from "../tabs/transact";
27
28
  import { activeTab } from "../workspace/workspace";
28
29
  import { view } from "../view";
29
30
  import { componentRegistry, computeRelativePath } from "../files/components";
@@ -31,6 +32,7 @@ import { renderComponentPreview } from "./stylebook-panel";
31
32
  import { defaultDef, unsafeTags } from "./shared";
32
33
  import type { JxPath } from "../state";
33
34
  import type { JxMutableNode } from "@jxsuite/schema/types";
35
+ import type { ComponentEntry } from "../files/components.js";
34
36
 
35
37
  interface DragCanDragArgs {
36
38
  element: HTMLElement;
@@ -54,26 +56,30 @@ interface DragMonitorDropArgs {
54
56
  export function registerLayersDnD() {
55
57
  requestAnimationFrame(() => {
56
58
  const container = leftPanel?.querySelector(".layers-container") as HTMLElement | null;
57
- if (!container) return;
59
+ if (!container) {
60
+ return;
61
+ }
58
62
 
59
- (container.querySelectorAll("[data-dnd-row]") as NodeListOf<HTMLElement>).forEach((row) => {
63
+ for (const row of container.querySelectorAll("[data-dnd-row]") as NodeListOf<HTMLElement>) {
60
64
  const rowPath = (row.dataset.path as string)
61
65
  .split("/")
62
- .map((s: string) => (/^\d+$/.test(s) ? parseInt(s) : s)) as JxPath;
63
- const rowDepth = parseInt(row.dataset.dndDepth as string) || 0;
64
- const isVoid = row.hasAttribute("data-dnd-void");
65
- const isExpanded = row.hasAttribute("data-dnd-expanded");
66
+ .map((s: string) => (/^\d+$/.test(s) ? Number.parseInt(s, 10) : s)) as JxPath;
67
+ const rowDepth = Number.parseInt(row.dataset.dndDepth as string, 10) || 0;
68
+ const isVoid = Object.hasOwn(row.dataset, "dndVoid");
69
+ const isExpanded = Object.hasOwn(row.dataset, "dndExpanded");
66
70
 
67
71
  const cleanup = combine(
68
72
  draggable({
69
- element: row,
70
73
  canDrag({ element: _el, input }: DragCanDragArgs) {
71
74
  const target = document.elementFromPoint(input.clientX, input.clientY) as HTMLElement;
72
- if (target?.closest(".layer-actions")) return false;
75
+ if (target?.closest(".layer-actions")) {
76
+ return false;
77
+ }
73
78
  return true;
74
79
  },
80
+ element: row,
75
81
  getInitialData() {
76
- return { type: "tree-node", path: rowPath };
82
+ return { path: rowPath, type: "tree-node" };
77
83
  },
78
84
  onDragStart() {
79
85
  row.classList.add("dragging");
@@ -93,19 +99,27 @@ export function registerLayersDnD() {
93
99
  element: row,
94
100
  canDrop({ source }: DragDropSourceArgs) {
95
101
  const srcPath = source.data.path as JxPath | undefined;
96
- if (srcPath && isAncestor(srcPath, rowPath)) return false;
102
+ if (srcPath && isAncestor(srcPath, rowPath)) {
103
+ return false;
104
+ }
97
105
  return true;
98
106
  },
99
- getData({ input, element }: any) {
107
+ getData({
108
+ input,
109
+ element,
110
+ }: {
111
+ input: Parameters<typeof attachInstruction>[1]["input"];
112
+ element: Element;
113
+ }) {
100
114
  return attachInstruction(
101
115
  { path: rowPath },
102
116
  {
103
- input,
104
- element,
117
+ block: isVoid ? ["make-child"] : [],
105
118
  currentLevel: rowDepth,
119
+ element,
106
120
  indentPerLevel: 16,
121
+ input,
107
122
  mode: isExpanded ? "expanded" : "standard",
108
- block: isVoid ? ["make-child"] : [],
109
123
  },
110
124
  ) as Record<string | symbol, unknown>;
111
125
  },
@@ -116,35 +130,33 @@ export function registerLayersDnD() {
116
130
  showLayerDropGap(row, self.data, container);
117
131
  },
118
132
  // No onDragLeave clear — the gap persists while the pointer crosses dead space
119
- // between rows; the monitor clears it when the drag moves to a non-tree target.
133
+ // Between rows; the monitor clears it when the drag moves to a non-tree target.
120
134
  onDrop() {
121
135
  clearLayerDropGap(container);
122
136
  },
123
137
  }),
124
138
  );
125
139
  view.dndCleanups.push(cleanup);
126
- });
140
+ }
127
141
 
128
142
  // Global monitor
129
143
  const monitorCleanup = monitorForElements({
130
- onDropTargetChange({ location }: DragMonitorDropArgs) {
131
- // Clear the layer gap when the drag moves onto a non-tree target (e.g. a canvas
132
- // element). When there is no target at all, keep the gap so it persists.
133
- const inner = location.current.dropTargets[0];
134
- if (inner && !extractInstruction(inner.data)) clearLayerDropGap(container);
135
- },
136
144
  onDrop({ source, location }: DragMonitorDropArgs) {
137
145
  clearLayerDropGap(container);
138
- const target = location.current.dropTargets[0];
139
- if (!target) return;
146
+ const [target] = location.current.dropTargets;
147
+ if (!target) {
148
+ return;
149
+ }
140
150
  const instruction = extractInstruction(target.data);
141
- if (!instruction || instruction.type === "instruction-blocked") return;
151
+ if (!instruction || instruction.type === "instruction-blocked") {
152
+ return;
153
+ }
142
154
  const srcData = source.data;
143
155
  const targetPath = target.data.path as JxPath;
144
156
 
145
157
  // If the source had children, persist collapse at the new location
146
158
  const srcRow = srcData.type === "tree-node" && source.element;
147
- const wasExpanded = srcRow && srcRow.hasAttribute("data-dnd-expanded");
159
+ const wasExpanded = srcRow && Object.hasOwn(srcRow.dataset, "dndExpanded");
148
160
 
149
161
  applyDropInstruction(instruction, srcData, targetPath);
150
162
 
@@ -152,11 +164,19 @@ export function registerLayersDnD() {
152
164
  const tab = activeTab.value;
153
165
  const newPath = tab?.session.selection;
154
166
  if (newPath) {
155
- const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
167
+ const collapsed = (view._layersCollapsed ||= new Set());
156
168
  collapsed.add(newPath.join("/"));
157
169
  }
158
170
  }
159
171
  },
172
+ onDropTargetChange({ location }: DragMonitorDropArgs) {
173
+ // Clear the layer gap when the drag moves onto a non-tree target (e.g. a canvas
174
+ // Element). When there is no target at all, keep the gap so it persists.
175
+ const [inner] = location.current.dropTargets;
176
+ if (inner && !extractInstruction(inner.data)) {
177
+ clearLayerDropGap(container);
178
+ }
179
+ },
160
180
  });
161
181
  view.dndCleanups.push(monitorCleanup);
162
182
  });
@@ -166,48 +186,52 @@ export function registerLayersDnD() {
166
186
  export function registerComponentsDnD() {
167
187
  requestAnimationFrame(() => {
168
188
  const container = leftPanel?.querySelector(".components-section") as HTMLElement | null;
169
- if (!container) return;
170
-
171
- (container.querySelectorAll("[data-component-tag]") as NodeListOf<HTMLElement>).forEach(
172
- (row) => {
173
- const tagName = row.dataset.componentTag;
174
- if (!tagName) return;
175
- const comp = componentRegistry.find(
176
- (c: import("../files/components.js").ComponentEntry) => c.tagName === tagName,
177
- );
178
- if (!comp) return;
189
+ if (!container) {
190
+ return;
191
+ }
179
192
 
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((el: HTMLElement) => {
184
- preview.textContent = "";
185
- preview.appendChild(el);
186
- });
187
- }
193
+ for (const row of container.querySelectorAll(
194
+ "[data-component-tag]",
195
+ ) as NodeListOf<HTMLElement>) {
196
+ const tagName = row.dataset.componentTag;
197
+ if (!tagName) {
198
+ continue;
199
+ }
200
+ const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tagName);
201
+ if (!comp) {
202
+ continue;
203
+ }
188
204
 
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) => [
195
- p.name,
196
- p.default !== undefined ? p.default : "",
197
- ],
198
- ),
199
- )
200
- : {},
201
- };
202
- const cleanup = draggable({
203
- element: row,
204
- getInitialData() {
205
- return { type: "block", fragment: structuredClone(instanceDef) };
206
- },
205
+ // Fill preview with live rendered component
206
+ const preview = row.querySelector(".element-card-preview");
207
+ if (preview && !preview.querySelector(tagName)) {
208
+ renderComponentPreview(comp).then((el: HTMLElement) => {
209
+ preview.textContent = "";
210
+ preview.append(el);
207
211
  });
208
- view.dndCleanups.push(cleanup);
209
- },
210
- );
212
+ }
213
+
214
+ const instanceDef = {
215
+ $props: comp.props
216
+ ? Object.fromEntries(
217
+ comp.props.map(
218
+ (/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
219
+ p.name,
220
+ p.default !== undefined ? p.default : "",
221
+ ],
222
+ ),
223
+ )
224
+ : {},
225
+ tagName: comp.tagName,
226
+ };
227
+ const cleanup = draggable({
228
+ element: row,
229
+ getInitialData() {
230
+ return { fragment: structuredClone(instanceDef), type: "block" };
231
+ },
232
+ });
233
+ view.dndCleanups.push(cleanup);
234
+ }
211
235
  });
212
236
  }
213
237
 
@@ -215,24 +239,26 @@ export function registerComponentsDnD() {
215
239
  export function registerElementsDnD() {
216
240
  requestAnimationFrame(() => {
217
241
  const container = leftPanel?.querySelector(".panel-body") as HTMLElement | null;
218
- if (!container) return;
219
- (container.querySelectorAll("[data-block-tag]") as NodeListOf<HTMLElement>).forEach((row) => {
242
+ if (!container) {
243
+ return;
244
+ }
245
+ for (const row of container.querySelectorAll("[data-block-tag]") as NodeListOf<HTMLElement>) {
220
246
  const tag = row.dataset.blockTag as string;
221
247
  const preview = row.querySelector(".element-card-preview");
222
248
  if (preview && !preview.firstChild) {
223
249
  const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
224
250
  el.textContent = tag;
225
- preview.appendChild(el);
251
+ preview.append(el);
226
252
  }
227
253
  const def = defaultDef(tag);
228
254
  const cleanup = draggable({
229
255
  element: row,
230
256
  getInitialData() {
231
- return { type: "block", fragment: structuredClone(def) };
257
+ return { fragment: structuredClone(def), type: "block" };
232
258
  },
233
259
  });
234
260
  view.dndCleanups.push(cleanup);
235
- });
261
+ }
236
262
  });
237
263
  }
238
264
 
@@ -243,7 +269,7 @@ export function registerElementsDnD() {
243
269
  * @param {HTMLElement} container
244
270
  */
245
271
  function hideDescendantRows(parentRow: HTMLElement, container: HTMLElement) {
246
- const prefix = parentRow.dataset.path + "/";
272
+ const prefix = `${parentRow.dataset.path}/`;
247
273
  const rows = container.querySelectorAll(".layers-tree .layer-row");
248
274
  for (const r of rows) {
249
275
  if ((r as HTMLElement).dataset.path?.startsWith(prefix)) {
@@ -285,12 +311,14 @@ export function showLayerDropGap(
285
311
  view._currentDropTargetRow = rowEl;
286
312
 
287
313
  // Shift rows to create gap
288
- const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
314
+ const rows = [...container.querySelectorAll(".layers-tree .layer-row")];
289
315
  const targetIdx = rows.indexOf(rowEl);
290
316
  const gap = view.layerDragSourceHeight;
291
317
 
292
318
  for (let i = 0; i < rows.length; i++) {
293
- if ((rows[i] as HTMLElement).classList.contains("dragging")) continue;
319
+ if ((rows[i] as HTMLElement).classList.contains("dragging")) {
320
+ continue;
321
+ }
294
322
  if (instruction.type === "reorder-above") {
295
323
  (rows[i] as HTMLElement).style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
296
324
  } else {
@@ -306,7 +334,9 @@ export function clearLayerDropGap(container: HTMLElement) {
306
334
  view._currentDropTargetRow = null;
307
335
  }
308
336
  const rows = container.querySelectorAll(".layers-tree .layer-row");
309
- for (const r of rows) (r as HTMLElement).style.transform = "";
337
+ for (const r of rows) {
338
+ (r as HTMLElement).style.transform = "";
339
+ }
310
340
  }
311
341
 
312
342
  /**
@@ -327,34 +357,41 @@ export function applyDropInstruction(
327
357
  const targetParent = parentElementPath(targetPath) as JxPath;
328
358
  const targetIdx = childIndex(targetPath) as number;
329
359
  // Reordering requires a parent and a numeric index; root-level paths can't be
330
- // reordered around.
331
- if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
360
+ // Reordered around.
361
+ if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number")) {
332
362
  return;
363
+ }
333
364
 
334
365
  switch (instruction.type) {
335
- case "reorder-above":
366
+ case "reorder-above": {
336
367
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
337
368
  break;
338
- case "reorder-below":
369
+ }
370
+ case "reorder-below": {
339
371
  transactDoc(activeTab.value, (t) =>
340
372
  mutateMoveNode(t, fromPath, targetParent, targetIdx + 1),
341
373
  );
342
374
  break;
375
+ }
343
376
  case "make-child": {
344
377
  const target = getNodeAtPath(doc, targetPath);
345
- const len = target?.children?.length || 0;
378
+ const len = childList(target).length;
346
379
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
347
380
  break;
348
381
  }
382
+ default: {
383
+ break;
384
+ }
349
385
  }
350
386
  } else if (srcData.type === "block") {
351
387
  const targetParent = parentElementPath(targetPath) as JxPath;
352
388
  const targetIdx = childIndex(targetPath) as number;
353
- if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number"))
389
+ if (instruction.type !== "make-child" && (!targetParent || typeof targetIdx !== "number")) {
354
390
  return;
391
+ }
355
392
 
356
393
  switch (instruction.type) {
357
- case "reorder-above":
394
+ case "reorder-above": {
358
395
  transactDoc(activeTab.value, (t) =>
359
396
  mutateInsertNode(
360
397
  t,
@@ -364,7 +401,8 @@ export function applyDropInstruction(
364
401
  ),
365
402
  );
366
403
  break;
367
- case "reorder-below":
404
+ }
405
+ case "reorder-below": {
368
406
  transactDoc(activeTab.value, (t) =>
369
407
  mutateInsertNode(
370
408
  t,
@@ -374,51 +412,61 @@ export function applyDropInstruction(
374
412
  ),
375
413
  );
376
414
  break;
415
+ }
377
416
  case "make-child": {
378
417
  const target = getNodeAtPath(doc, targetPath);
379
- const len = target?.children?.length || 0;
418
+ const len = childList(target).length;
380
419
  transactDoc(activeTab.value, (t) =>
381
420
  mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment as JxMutableNode)),
382
421
  );
383
422
  break;
384
423
  }
424
+ default: {
425
+ break;
426
+ }
385
427
  }
386
428
 
387
429
  // Auto-import to $elements if the dropped block is a custom component
388
430
  const fragment = srcData.fragment as JxMutableNode | undefined;
389
431
  const tag = fragment?.tagName;
390
432
  if (tag && tag.includes("-")) {
391
- const comp = componentRegistry.find(
392
- (c: import("../files/components.js").ComponentEntry) => c.tagName === tag,
393
- );
433
+ const comp = componentRegistry.find((c: ComponentEntry) => c.tagName === tag);
394
434
  if (comp) {
395
435
  const tab = activeTab.value;
396
436
  const elements = tab?.doc.document?.$elements || [];
397
437
  if (comp.source === "npm") {
398
438
  const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
399
- if (!specifier) return;
439
+ if (!specifier) {
440
+ return;
441
+ }
400
442
  const alreadyImported = elements.some(
401
443
  (e: JxMutableNode | string | { $ref: string }) => e === specifier || e === comp.package,
402
444
  );
403
445
  if (!alreadyImported) {
404
- transact(activeTab.value, (doc: JxMutableNode) => {
405
- if (!doc.$elements) doc.$elements = [];
406
- doc.$elements.push(specifier);
446
+ transact(activeTab.value, (d: JxMutableNode) => {
447
+ if (!d.$elements) {
448
+ d.$elements = [];
449
+ }
450
+ d.$elements.push(specifier);
407
451
  });
408
452
  }
409
453
  } else {
410
454
  const alreadyImported = elements.some((e: JxMutableNode | string | { $ref: string }) => {
411
455
  const ref = typeof e === "object" && e !== null ? e.$ref : undefined;
456
+ const compPath = comp.path;
412
457
  return (
413
458
  ref &&
414
- (ref === `./${comp.path}` || ref.endsWith(comp.path.split("/").pop() as string))
459
+ compPath &&
460
+ (ref === `./${compPath}` || ref.endsWith(compPath.split("/").pop() as string))
415
461
  );
416
462
  });
417
- if (!alreadyImported) {
463
+ if (!alreadyImported && comp.path) {
418
464
  const relPath = computeRelativePath(tab?.documentPath ?? null, comp.path);
419
- transact(activeTab.value, (doc: JxMutableNode) => {
420
- if (!doc.$elements) doc.$elements = [];
421
- doc.$elements.push({ $ref: relPath });
465
+ transact(activeTab.value, (d: JxMutableNode) => {
466
+ if (!d.$elements) {
467
+ d.$elements = [];
468
+ }
469
+ d.$elements.push({ $ref: relPath });
422
470
  });
423
471
  }
424
472
  }
@@ -5,17 +5,18 @@
5
5
  */
6
6
 
7
7
  import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
8
+ import { isJsonObject } from "@jxsuite/schema/guards";
8
9
  import { html, render as litRender, nothing } from "lit-html";
9
10
  import { ref } from "lit-html/directives/ref.js";
10
11
 
11
- import { renderOnly, canvasWrap, canvasPanels, getNodeAtPath } from "../store";
12
+ import { canvasPanels, canvasWrap, getNodeAtPath, renderOnly } from "../store";
12
13
  import { activeTab } from "../workspace/workspace";
13
- import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "../tabs/transact";
14
+ import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
14
15
  import { view } from "../view";
15
- import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services";
16
+ import { codeService, getFunctionArgs, setLintMarkers } from "../services/code-services";
16
17
 
17
18
  import type { OxLintDiagnostic } from "../services/code-services";
18
- import type { JxPrototypeDef, JxMutableNode } from "@jxsuite/schema/types";
19
+ import type { JxMutableNode, JxPrototypeDef } from "@jxsuite/schema/types";
19
20
  import type { JxPath } from "../state";
20
21
 
21
22
  type EditingTarget =
@@ -25,11 +26,14 @@ type EditingTarget =
25
26
  /** @param {EditingTarget | null | undefined} editing */
26
27
  function getFunctionBody(editing: EditingTarget | null | undefined) {
27
28
  const document = activeTab.value?.doc.document;
29
+ // Read body off any object-shaped def (covers legacy entries without $prototype).
30
+ const bodyOf = (def: unknown) =>
31
+ isJsonObject(def) && typeof def.body === "string" ? def.body : "";
28
32
  if (editing?.type === "def") {
29
- return document?.state?.[editing.defName]?.body || "";
33
+ return bodyOf(document?.state?.[editing.defName]);
30
34
  } else if (editing?.type === "event") {
31
35
  const node = getNodeAtPath(document!, editing.path);
32
- return node?.[editing.eventKey]?.body || "";
36
+ return node ? bodyOf(node[editing.eventKey]) : "";
33
37
  }
34
38
  return "";
35
39
  }
@@ -60,9 +64,13 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
60
64
  }
61
65
 
62
66
  // Clean up canvas DnD and event handlers
63
- for (const fn of view.canvasDndCleanups) fn();
67
+ for (const fn of view.canvasDndCleanups) {
68
+ fn();
69
+ }
64
70
  view.canvasDndCleanups = [];
65
- for (const fn of view.canvasEventCleanups) fn();
71
+ for (const fn of view.canvasEventCleanups) {
72
+ fn();
73
+ }
66
74
  view.canvasEventCleanups = [];
67
75
  canvasPanels.length = 0;
68
76
 
@@ -95,7 +103,9 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
95
103
  <div
96
104
  class="source-editor"
97
105
  ${ref((el) => {
98
- if (el) editorContainer = el as HTMLDivElement;
106
+ if (el) {
107
+ editorContainer = el as HTMLDivElement;
108
+ }
99
109
  })}
100
110
  ></div>
101
111
  </div>`,
@@ -109,31 +119,32 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
109
119
  );
110
120
 
111
121
  view.functionEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
112
- value: body,
113
- language: "javascript",
114
- theme: "vs-dark",
115
122
  automaticLayout: true,
116
- minimap: { enabled: false },
117
- fontSize: 12,
118
123
  fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
124
+ fontSize: 12,
125
+ language: "javascript",
119
126
  lineNumbers: "on",
127
+ minimap: { enabled: false },
120
128
  scrollBeyondLastLine: false,
121
- wordWrap: "on",
122
129
  tabSize: 2,
130
+ theme: "vs-dark",
131
+ value: body,
132
+ wordWrap: "on",
123
133
  });
124
134
  view.functionEditor._editingTarget = JSON.stringify(editing);
125
135
  const editor = view.functionEditor;
126
136
 
127
137
  // Format on open — show pretty-printed code, then run initial lint
128
- codeService("format", { code: body, args }).then((result) => {
138
+ codeService("format", { args, code: body }).then((result) => {
129
139
  if (result?.code != null && view.functionEditor) {
130
140
  view.functionEditor._ignoreNextChange = true;
131
141
  view.functionEditor.setValue(result.code);
132
142
  }
133
143
  });
134
- codeService("lint", { code: body, args }).then((result) => {
135
- if (result?.diagnostics && view.functionEditor)
144
+ codeService("lint", { args, code: body }).then((result) => {
145
+ if (result?.diagnostics && view.functionEditor) {
136
146
  setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
147
+ }
137
148
  });
138
149
 
139
150
  // Debounced sync back to state + lint on edit
@@ -151,14 +162,14 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
151
162
  clearTimeout(syncDebounce);
152
163
  syncDebounce = setTimeout(() => {
153
164
  const newBody = editor.getValue();
154
- const ed = editing as EditingTarget;
155
- if (ed.type === "def") {
156
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, ed.defName, { body: newBody }));
157
- } else if (ed.type === "event") {
158
- const node = getNodeAtPath(activeTab.value?.doc.document as JxMutableNode, ed.path);
159
- const current = node?.[ed.eventKey] || {};
165
+ const target = editing as EditingTarget;
166
+ if (target.type === "def") {
167
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, target.defName, { body: newBody }));
168
+ } else if (target.type === "event") {
169
+ const node = getNodeAtPath(activeTab.value?.doc.document as JxMutableNode, target.path);
170
+ const current = node?.[target.eventKey] || {};
160
171
  transactDoc(activeTab.value, (t) =>
161
- mutateUpdateProperty(t, ed.path, ed.eventKey, {
172
+ mutateUpdateProperty(t, target.path, target.eventKey, {
162
173
  ...current,
163
174
  $prototype: "Function",
164
175
  body: newBody,
@@ -170,12 +181,15 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
170
181
 
171
182
  clearTimeout(lintDebounce);
172
183
  lintDebounce = setTimeout(() => {
173
- const gen = ++lintGen;
184
+ const gen = (lintGen += 1);
174
185
  const currentCode = editor.getValue();
175
- codeService("lint", { code: currentCode, args }).then((result) => {
176
- if (gen !== lintGen) return;
177
- if (result?.diagnostics && view.functionEditor)
186
+ codeService("lint", { args, code: currentCode }).then((result) => {
187
+ if (gen !== lintGen) {
188
+ return;
189
+ }
190
+ if (result?.diagnostics && view.functionEditor) {
178
191
  setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
192
+ }
179
193
  });
180
194
  }, 750);
181
195
  });
@@ -183,18 +197,19 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
183
197
 
184
198
  // Register Monaco JS completion provider for state scope variables (once)
185
199
  export function registerFunctionCompletions() {
186
- if (view._completionRegistered) return;
200
+ if (view._completionRegistered) {
201
+ return;
202
+ }
187
203
  view._completionRegistered = true;
188
204
  monaco.languages.registerCompletionItemProvider("javascript", {
189
- triggerCharacters: ["."],
190
205
  provideCompletionItems(model, position) {
191
206
  const defs = activeTab.value?.doc.document?.state || {};
192
207
  const word = model.getWordUntilPosition(position);
193
208
  const range = {
194
- startLineNumber: position.lineNumber,
209
+ endColumn: word.endColumn,
195
210
  endLineNumber: position.lineNumber,
196
211
  startColumn: word.startColumn,
197
- endColumn: word.endColumn,
212
+ startLineNumber: position.lineNumber,
198
213
  };
199
214
 
200
215
  const suggestions = Object.entries(defs).map(([key, def]) => {
@@ -202,18 +217,20 @@ export function registerFunctionCompletions() {
202
217
  if (
203
218
  (def as JxPrototypeDef)?.$prototype === "Function" ||
204
219
  (def as Record<string, unknown>)?.$handler
205
- )
220
+ ) {
206
221
  kind = monaco.languages.CompletionItemKind.Function;
207
- else if ((def as JxPrototypeDef)?.$prototype)
222
+ } else if ((def as JxPrototypeDef)?.$prototype) {
208
223
  kind = monaco.languages.CompletionItemKind.Property;
224
+ }
209
225
  return {
210
- label: `state.${key}`,
211
- kind,
212
226
  insertText: `state.${key}`,
227
+ kind,
228
+ label: `state.${key}`,
213
229
  range,
214
230
  };
215
231
  });
216
232
  return { suggestions };
217
233
  },
234
+ triggerCharacters: ["."],
218
235
  });
219
236
  }