@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
@@ -6,15 +6,15 @@
6
6
  * shortcuts on the canvas / document.
7
7
  */
8
8
 
9
- import { getNodeAtPath, parentElementPath, childIndex, canvasWrap } from "../store";
10
- import { activeTab, workspace, closeTab } from "../workspace/workspace";
9
+ import { canvasWrap, childIndex, childList, getNodeAtPath, parentElementPath } from "../store";
10
+ import { activeTab, closeTab, workspace } from "../workspace/workspace";
11
11
  import {
12
- transactDoc,
12
+ mutateDuplicateNode,
13
13
  mutateInsertNode,
14
14
  mutateRemoveNode,
15
- mutateDuplicateNode,
16
- undo as tabUndo,
17
15
  redo as tabRedo,
16
+ undo as tabUndo,
17
+ transactDoc,
18
18
  } from "../tabs/transact";
19
19
  import { isEditing, stopEditing } from "./inline-edit";
20
20
  import { copyNode, cutNode, pasteNode } from "./context-menu";
@@ -59,9 +59,13 @@ export function initShortcuts(
59
59
  (e: WheelEvent) => {
60
60
  const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
61
61
  // Edit (content) mode: let the scroll container handle scrolling natively
62
- if (canvasMode === "edit") return;
62
+ if (canvasMode === "edit") {
63
+ return;
64
+ }
63
65
  // Manage mode: browse table handles its own scrolling
64
- if (canvasMode === "manage") return;
66
+ if (canvasMode === "manage") {
67
+ return;
68
+ }
65
69
  e.preventDefault();
66
70
  if (e.ctrlKey || e.metaKey) {
67
71
  // Zoom towards cursor
@@ -70,7 +74,7 @@ export function initShortcuts(
70
74
  const cursorY = e.clientY - rect.top;
71
75
  const oldZoom = activeTab.value?.session.ui.zoom ?? 1;
72
76
  const delta = -e.deltaY * 0.005;
73
- const newZoom = Math.min(5.0, Math.max(0.05, oldZoom * (1 + delta)));
77
+ const newZoom = Math.min(5, Math.max(0.05, oldZoom * (1 + delta)));
74
78
  const ratio = newZoom / oldZoom;
75
79
  // Adjust pan so the point under cursor stays stationary
76
80
  setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
@@ -90,8 +94,12 @@ export function initShortcuts(
90
94
  // Middle-mouse drag panning
91
95
  canvasWrap.addEventListener("pointerdown", (e: PointerEvent) => {
92
96
  const ctx = getContext();
93
- if (ctx.canvasMode === "edit") return; // no panning in edit mode
94
- if (e.button !== 1) return; // middle button only
97
+ if (ctx.canvasMode === "edit") {
98
+ return;
99
+ } // No panning in edit mode
100
+ if (e.button !== 1) {
101
+ return;
102
+ } // Middle button only
95
103
  e.preventDefault();
96
104
  canvasWrap.setPointerCapture(e.pointerId);
97
105
  let lastX = e.clientX,
@@ -169,97 +177,125 @@ export function initShortcuts(
169
177
 
170
178
  if (mod) {
171
179
  switch (e.key) {
172
- case "w":
180
+ case "w": {
173
181
  e.preventDefault();
174
182
  if (workspace.activeTabId && workspace.tabOrder.length > 1) {
175
- const tab = workspace.tabs.get(workspace.activeTabId);
176
- if (tab?.doc.dirty) {
177
- const name = tab.documentPath?.split("/").pop() || "Untitled";
183
+ const tabToClose = workspace.tabs.get(workspace.activeTabId);
184
+ if (tabToClose?.doc.dirty) {
185
+ const name = tabToClose.documentPath?.split("/").pop() || "Untitled";
178
186
  showConfirmDialog(
179
187
  "Unsaved Changes",
180
188
  `"${name}" has unsaved changes. Close without saving?`,
181
189
  { confirmLabel: "Close", destructive: true },
182
190
  ).then((confirmed) => {
183
- if (confirmed && workspace.activeTabId) closeTab(workspace.activeTabId);
191
+ if (confirmed && workspace.activeTabId) {
192
+ closeTab(workspace.activeTabId);
193
+ }
184
194
  });
185
195
  } else {
186
196
  closeTab(workspace.activeTabId);
187
197
  }
188
198
  }
189
199
  break;
190
- case "o":
200
+ }
201
+ case "o": {
191
202
  e.preventDefault();
192
203
  openProject();
193
204
  break;
194
- case "p":
205
+ }
206
+ case "p": {
195
207
  e.preventDefault();
196
208
  openQuickSearch();
197
209
  break;
198
- case "s":
210
+ }
211
+ case "s": {
199
212
  e.preventDefault();
200
213
  saveFile();
201
214
  break;
202
- case "z":
215
+ }
216
+ case "z": {
203
217
  e.preventDefault();
204
- if (e.shiftKey) tabRedo(activeTab.value!);
205
- else tabUndo(activeTab.value!);
218
+ if (e.shiftKey) {
219
+ tabRedo(activeTab.value!);
220
+ } else {
221
+ tabUndo(activeTab.value!);
222
+ }
206
223
  break;
207
- case "d":
224
+ }
225
+ case "d": {
208
226
  e.preventDefault();
209
227
  if (tab?.session.selection) {
210
228
  const sel = tab.session.selection;
211
229
  transactDoc(tab, (t) => mutateDuplicateNode(t, sel));
212
230
  }
213
231
  break;
214
- case "c":
232
+ }
233
+ case "c": {
215
234
  e.preventDefault();
216
235
  copyNode();
217
236
  break;
218
- case "x":
237
+ }
238
+ case "x": {
219
239
  e.preventDefault();
220
240
  cutNode();
221
241
  break;
222
- case "v":
242
+ }
243
+ case "v": {
223
244
  e.preventDefault();
224
245
  pasteNode();
225
246
  break;
226
- case "0":
227
- if (canvasMode === "edit") break;
247
+ }
248
+ case "0": {
249
+ if (canvasMode === "edit") {
250
+ break;
251
+ }
228
252
  e.preventDefault();
229
253
  activeTab.value!.session.ui.zoom = 1;
230
254
  setPan(16, 16);
231
255
  applyTransform();
232
256
  break;
257
+ }
233
258
  case "=":
234
- case "+":
235
- if (canvasMode === "edit") break;
259
+ case "+": {
260
+ if (canvasMode === "edit") {
261
+ break;
262
+ }
236
263
  e.preventDefault();
237
- activeTab.value!.session.ui.zoom = Math.min(5.0, (tab?.session.ui.zoom ?? 1) * 1.2);
264
+ activeTab.value!.session.ui.zoom = Math.min(5, (tab?.session.ui.zoom ?? 1) * 1.2);
238
265
  applyTransform();
239
266
  break;
240
- case "-":
241
- if (canvasMode === "edit") break;
267
+ }
268
+ case "-": {
269
+ if (canvasMode === "edit") {
270
+ break;
271
+ }
242
272
  e.preventDefault();
243
273
  activeTab.value!.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
244
274
  applyTransform();
245
275
  break;
276
+ }
277
+ default: {
278
+ break;
279
+ }
246
280
  }
247
281
  return;
248
282
  }
249
283
 
250
284
  switch (e.key) {
251
285
  case "Delete":
252
- case "Backspace":
286
+ case "Backspace": {
253
287
  if (tab?.session.selection && tab.session.selection.length >= 2) {
254
288
  e.preventDefault();
255
289
  const sel = tab.session.selection;
256
290
  transactDoc(tab, (t) => mutateRemoveNode(t, sel));
257
291
  }
258
292
  break;
259
- case "Escape":
293
+ }
294
+ case "Escape": {
260
295
  activeTab.value!.session.selection = null;
261
296
  break;
262
- case "Enter":
297
+ }
298
+ case "Enter": {
263
299
  if (tab?.session.selection && tab.session.selection.length >= 2) {
264
300
  e.preventDefault();
265
301
  const pp = parentElementPath(tab.session.selection) as JxPath;
@@ -272,29 +308,37 @@ export function initShortcuts(
272
308
  enterEditOnPath(newPath);
273
309
  }
274
310
  break;
275
- case "ArrowUp":
311
+ }
312
+ case "ArrowUp": {
276
313
  e.preventDefault();
277
314
  navigateSelection(-1);
278
315
  break;
279
- case "ArrowDown":
316
+ }
317
+ case "ArrowDown": {
280
318
  e.preventDefault();
281
319
  navigateSelection(1);
282
320
  break;
283
- case "ArrowLeft":
321
+ }
322
+ case "ArrowLeft": {
284
323
  e.preventDefault();
285
324
  if (tab?.session.selection && tab.session.selection.length >= 2) {
286
325
  activeTab.value!.session.selection = parentElementPath(tab.session.selection);
287
326
  }
288
327
  break;
289
- case "ArrowRight":
328
+ }
329
+ case "ArrowRight": {
290
330
  e.preventDefault();
291
331
  if (tab?.session.selection) {
292
332
  const node = getNodeAtPath(tab.doc.document, tab.session.selection);
293
- if ((node?.children?.length ?? 0) > 0) {
333
+ if (childList(node).length > 0) {
294
334
  activeTab.value!.session.selection = [...tab.session.selection, "children", 0];
295
335
  }
296
336
  }
297
337
  break;
338
+ }
339
+ default: {
340
+ break;
341
+ }
298
342
  }
299
343
  });
300
344
 
@@ -311,13 +355,15 @@ export function initShortcuts(
311
355
  }
312
356
 
313
357
  /** @param {number} [direction] */
314
- function navigateSelection(direction: number = -1) {
358
+ function navigateSelection(direction = -1) {
315
359
  const tab = activeTab.value;
316
360
  if (!tab?.session.selection) {
317
361
  activeTab.value!.session.selection = [];
318
362
  return;
319
363
  }
320
- if (tab.session.selection.length < 2) return;
364
+ if (tab.session.selection.length < 2) {
365
+ return;
366
+ }
321
367
 
322
368
  const parent = getNodeAtPath(
323
369
  tab.doc.document,
@@ -325,7 +371,7 @@ function navigateSelection(direction: number = -1) {
325
371
  );
326
372
  const idx = childIndex(tab.session.selection) as number;
327
373
  const newIdx = idx + direction;
328
- if (parent?.children && newIdx >= 0 && newIdx < parent.children.length) {
374
+ if (newIdx >= 0 && newIdx < childList(parent).length) {
329
375
  activeTab.value!.session.selection = [
330
376
  ...(parentElementPath(tab.session.selection) as JxPath),
331
377
  "children",
@@ -21,21 +21,21 @@ interface SlashCommand {
21
21
  // ─── Commands ─────────────────────────────────────────────────────────────────
22
22
 
23
23
  const SLASH_COMMANDS = [
24
- { label: "Heading 1", tag: "h1", description: "Large heading" },
25
- { label: "Heading 2", tag: "h2", description: "Medium heading" },
26
- { label: "Heading 3", tag: "h3", description: "Small heading" },
27
- { label: "Paragraph", tag: "p", description: "Plain text" },
28
- { label: "Bulleted List", tag: "ul", description: "Unordered list" },
29
- { label: "Numbered List", tag: "ol", description: "Numbered list" },
30
- { label: "Blockquote", tag: "blockquote", description: "Quote block" },
31
- { label: "Image", tag: "img", description: "Insert image" },
32
- { label: "Horizontal Rule", tag: "hr", description: "Divider line" },
33
- { label: "Button", tag: "button", description: "Button element" },
34
- { label: "Link", tag: "a", description: "Anchor link" },
35
- { label: "Code Block", tag: "pre", description: "Preformatted code" },
36
- { label: "Table", tag: "table", description: "Insert table" },
37
- { label: "Div", tag: "div", description: "Container" },
38
- { label: "Section", tag: "section", description: "Section container" },
24
+ { description: "Large heading", label: "Heading 1", tag: "h1" },
25
+ { description: "Medium heading", label: "Heading 2", tag: "h2" },
26
+ { description: "Small heading", label: "Heading 3", tag: "h3" },
27
+ { description: "Plain text", label: "Paragraph", tag: "p" },
28
+ { description: "Unordered list", label: "Bulleted List", tag: "ul" },
29
+ { description: "Numbered list", label: "Numbered List", tag: "ol" },
30
+ { description: "Quote block", label: "Blockquote", tag: "blockquote" },
31
+ { description: "Insert image", label: "Image", tag: "img" },
32
+ { description: "Divider line", label: "Horizontal Rule", tag: "hr" },
33
+ { description: "Button element", label: "Button", tag: "button" },
34
+ { description: "Anchor link", label: "Link", tag: "a" },
35
+ { description: "Preformatted code", label: "Code Block", tag: "pre" },
36
+ { description: "Insert table", label: "Table", tag: "table" },
37
+ { description: "Container", label: "Div", tag: "div" },
38
+ { description: "Section container", label: "Section", tag: "section" },
39
39
  ];
40
40
 
41
41
  // ─── State ────────────────────────────────────────────────────────────────────
@@ -96,7 +96,7 @@ export function showSlashMenu(
96
96
  )
97
97
  : source;
98
98
 
99
- if (!filteredItems.length && !cbs.showFilter) {
99
+ if (filteredItems.length === 0 && !cbs.showFilter) {
100
100
  dismissSlashMenu();
101
101
  return;
102
102
  }
@@ -107,7 +107,7 @@ export function showSlashMenu(
107
107
 
108
108
  if (!open) {
109
109
  open = true;
110
- document.addEventListener("keydown", onKeydown, true); // capture phase
110
+ document.addEventListener("keydown", onKeydown, true); // Capture phase
111
111
  requestAnimationFrame(() => {
112
112
  document.addEventListener("mousedown", onOutsideClick, true);
113
113
  });
@@ -115,13 +115,17 @@ export function showSlashMenu(
115
115
 
116
116
  if (cbs.showFilter) {
117
117
  requestAnimationFrame(() => {
118
- if (_filterEl) _filterEl.focus();
118
+ if (_filterEl) {
119
+ _filterEl.focus();
120
+ }
119
121
  });
120
122
  }
121
123
  }
122
124
 
123
125
  export function dismissSlashMenu() {
124
- if (!open) return;
126
+ if (!open) {
127
+ return;
128
+ }
125
129
  open = false;
126
130
  callbacks = null;
127
131
  _anchorEl = null;
@@ -167,7 +171,7 @@ function render(anchorEl: HTMLElement, showFilter: boolean) {
167
171
  />`
168
172
  : nothing}
169
173
  <sp-menu style="min-width:220px">
170
- ${filteredItems.length
174
+ ${filteredItems.length > 0
171
175
  ? filteredItems.map(
172
176
  (cmd, i) => html`
173
177
  <sp-menu-item
@@ -220,27 +224,34 @@ function onFilterInput(e: Event) {
220
224
  : source;
221
225
 
222
226
  activeIdx = 0;
223
- if (_anchorEl) render(_anchorEl, true);
227
+ if (_anchorEl) {
228
+ render(_anchorEl, true);
229
+ }
224
230
 
225
231
  // Re-focus input after re-render
226
232
  requestAnimationFrame(() => {
227
233
  if (_filterEl && _filterEl !== document.activeElement) {
228
234
  _filterEl.focus();
229
- _filterEl.selectionStart = _filterEl.selectionEnd = _filterEl.value.length;
235
+ _filterEl.selectionStart = _filterEl.value.length;
236
+ _filterEl.selectionEnd = _filterEl.value.length;
230
237
  }
231
238
  });
232
239
  }
233
240
 
234
241
  /** @param {KeyboardEvent} e */
235
242
  function onKeydown(e: KeyboardEvent) {
236
- if (!open) return;
243
+ if (!open) {
244
+ return;
245
+ }
237
246
 
238
247
  const items = getHost().querySelectorAll("sp-menu-item:not([disabled])") as NodeListOf<Element>;
239
248
 
240
249
  if (e.key === "ArrowDown") {
241
250
  e.preventDefault();
242
251
  e.stopPropagation();
243
- if (!items.length) return;
252
+ if (items.length === 0) {
253
+ return;
254
+ }
244
255
  items[activeIdx]?.removeAttribute("focused");
245
256
  activeIdx = (activeIdx + 1) % items.length;
246
257
  items[activeIdx]?.setAttribute("focused", "");
@@ -248,7 +259,9 @@ function onKeydown(e: KeyboardEvent) {
248
259
  } else if (e.key === "ArrowUp") {
249
260
  e.preventDefault();
250
261
  e.stopPropagation();
251
- if (!items.length) return;
262
+ if (items.length === 0) {
263
+ return;
264
+ }
252
265
  items[activeIdx]?.removeAttribute("focused");
253
266
  activeIdx = (activeIdx - 1 + items.length) % items.length;
254
267
  items[activeIdx]?.setAttribute("focused", "");
@@ -257,7 +270,9 @@ function onKeydown(e: KeyboardEvent) {
257
270
  e.preventDefault();
258
271
  e.stopPropagation();
259
272
  const cmd = filteredItems[activeIdx];
260
- if (cmd) select(cmd);
273
+ if (cmd) {
274
+ select(cmd);
275
+ }
261
276
  } else if (e.key === "Escape") {
262
277
  e.preventDefault();
263
278
  e.stopPropagation();
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { getPlatform } from "../platform";
4
4
  import { projectState } from "../store";
5
+ import type { ComponentMeta } from "../types";
5
6
 
6
- export interface ComponentEntry {
7
- tagName: string;
8
- path: string;
9
- props?: { name: string; type?: string; [k: string]: unknown }[];
7
+ /** A discovered component: project components carry a file path; package components may not. */
8
+ export interface ComponentEntry extends Omit<ComponentMeta, "path"> {
9
+ path?: string;
10
10
  source?: string;
11
11
  package?: string;
12
12
  modulePath?: string;
@@ -30,10 +30,12 @@ export async function loadComponentRegistry() {
30
30
  * @param {string} toCompPath
31
31
  */
32
32
  export function computeRelativePath(fromDocPath: string | null, toCompPath: string) {
33
- if (!fromDocPath) return `./${toCompPath}`;
33
+ if (!fromDocPath) {
34
+ return `./${toCompPath}`;
35
+ }
34
36
  const from = fromDocPath.replaceAll("\\", "/");
35
37
  const to = toCompPath.replaceAll("\\", "/");
36
- const fromDir = from.substring(0, from.lastIndexOf("/"));
38
+ const fromDir = from.includes("/") ? from.slice(0, from.lastIndexOf("/")) : "";
37
39
  const fromParts = fromDir.split("/").filter(Boolean);
38
40
  const toParts = to.split("/").filter(Boolean);
39
41
  let common = 0;
@@ -42,7 +44,7 @@ export function computeRelativePath(fromDocPath: string | null, toCompPath: stri
42
44
  common < toParts.length &&
43
45
  fromParts[common] === toParts[common]
44
46
  ) {
45
- common++;
47
+ common += 1;
46
48
  }
47
49
  const ups = fromParts.length - common;
48
50
  const remaining = toParts.slice(common);
@@ -8,21 +8,24 @@
8
8
  */
9
9
 
10
10
  import { locateDocument } from "../services/code-services";
11
+ import { errorMessage } from "@jxsuite/schema/parse";
11
12
  import { statusMessage } from "../panels/statusbar";
12
13
  import { getPlatform } from "../platform";
13
14
  import { activeTab, openTab } from "../workspace/workspace";
14
15
  import { isEditing, stopEditing } from "../editor/inline-edit";
15
16
  import {
16
- loadFormats,
17
- getFormats,
18
- formatForPath,
17
+ defaultContentFormat,
19
18
  formatByName,
19
+ formatForPath,
20
20
  formatParse,
21
21
  formatSerialize,
22
- defaultContentFormat,
22
+ getFormats,
23
+ loadFormats,
24
+ noFormatError,
23
25
  splitFormatDocument,
24
- type StudioFormat,
25
26
  } from "../format/format-host";
27
+ import type { StudioFormat } from "../format/format-host";
28
+ import type { Tab } from "../tabs/tab.js";
26
29
 
27
30
  /**
28
31
  * Parse a format-class source string into document + frontmatter + mode per the format's
@@ -48,10 +51,7 @@ export async function parseSourceForPath(path: string, source: string) {
48
51
  await loadFormats();
49
52
  const format = formatForPath(path);
50
53
  if (!format || !format.capabilities.parse) {
51
- throw new Error(
52
- `No format class imported for "${path}" — add one to project.json imports ` +
53
- `(e.g. "Markdown": "@jxsuite/parser/Markdown.class.json")`,
54
- );
54
+ throw noFormatError(path);
55
55
  }
56
56
  const result = await parseFormatSource(format, source);
57
57
  return { ...result, format };
@@ -64,14 +64,14 @@ export async function openFile() {
64
64
  const formats = getFormats();
65
65
  const pickerTypes = [
66
66
  {
67
- description: "Jx Component",
68
67
  accept: { "application/json": [".json"] },
68
+ description: "Jx Component",
69
69
  },
70
70
  ...formats
71
71
  .filter((f) => f.capabilities.parse)
72
72
  .map((f) => ({
73
- description: f.name,
74
73
  accept: { [f.mediaType ?? "text/plain"]: f.extensions },
74
+ description: f.name,
75
75
  })),
76
76
  ];
77
77
  const acceptExts = [".json", ...formats.flatMap((f) => f.extensions)].join(",");
@@ -86,23 +86,27 @@ export async function openFile() {
86
86
  if (format) {
87
87
  const { document, frontmatter } = await parseFormatSource(format, text);
88
88
  openTab({
89
- id: name,
89
+ document,
90
90
  documentPath,
91
91
  fileHandle: handle,
92
- document,
93
92
  frontmatter,
93
+ id: name,
94
94
  sourceFormat: format.name,
95
95
  });
96
- } else {
96
+ } else if (name.endsWith(".json")) {
97
97
  const document = JSON.parse(text);
98
- openTab({ id: name, documentPath, fileHandle: handle, document });
98
+ openTab({ document, documentPath, fileHandle: handle, id: name });
99
+ } else {
100
+ throw noFormatError(name);
99
101
  }
100
102
  statusMessage(`Opened ${name}`);
101
103
  };
102
104
 
103
105
  if ("showOpenFilePicker" in window) {
104
106
  const [handle] = await (
105
- window as unknown as { showOpenFilePicker: Function }
107
+ window as unknown as {
108
+ showOpenFilePicker: (options?: unknown) => Promise<FileSystemFileHandle[]>;
109
+ }
106
110
  ).showOpenFilePicker({ types: pickerTypes });
107
111
  const file = await handle.getFile();
108
112
  await handleSource(handle.name, await file.text(), handle);
@@ -111,23 +115,31 @@ export async function openFile() {
111
115
  const input = document.createElement("input");
112
116
  input.type = "file";
113
117
  input.accept = acceptExts;
114
- input.onchange = async () => {
118
+ input.addEventListener("change", async () => {
115
119
  const file = input.files?.[0];
116
- if (!file) return;
120
+ if (!file) {
121
+ return;
122
+ }
117
123
  await handleSource(file.name, await file.text());
118
- };
124
+ });
119
125
  input.click();
120
126
  }
121
- } catch (e) {
122
- if ((e as Error).name !== "AbortError") statusMessage(`Error: ${(e as Error).message}`);
127
+ } catch (error) {
128
+ if (!(error instanceof Error && error.name === "AbortError")) {
129
+ statusMessage(`Error: ${errorMessage(error)}`);
130
+ }
123
131
  }
124
132
  }
125
133
 
126
134
  /** Save the current document back to its source location. */
127
135
  export async function saveFile() {
128
- if (isEditing()) stopEditing();
136
+ if (isEditing()) {
137
+ stopEditing();
138
+ }
129
139
  const tab = activeTab.value;
130
- if (!tab) return;
140
+ if (!tab) {
141
+ return;
142
+ }
131
143
  try {
132
144
  const output = await serializeDocument(tab);
133
145
 
@@ -153,13 +165,15 @@ export async function saveFile() {
153
165
  } else {
154
166
  statusMessage("No save target — use Export");
155
167
  }
156
- } catch (e) {
157
- if ((e as Error).name !== "AbortError") statusMessage(`Save error: ${(e as Error).message}`);
168
+ } catch (error) {
169
+ if (!(error instanceof Error && error.name === "AbortError")) {
170
+ statusMessage(`Save error: ${errorMessage(error)}`);
171
+ }
158
172
  }
159
173
  }
160
174
 
161
175
  /** The output format for a tab: its source format, or the default content format. */
162
- function tabFormat(tab: import("../tabs/tab.js").Tab): StudioFormat | undefined {
176
+ function tabFormat(tab: Tab): StudioFormat | undefined {
163
177
  return (
164
178
  formatByName(tab.doc.sourceFormat) ??
165
179
  (tab.doc.mode === "content" ? defaultContentFormat() : undefined)
@@ -169,7 +183,9 @@ function tabFormat(tab: import("../tabs/tab.js").Tab): StudioFormat | undefined
169
183
  /** Export the current document to a new location (Save As / download). */
170
184
  export async function exportFile() {
171
185
  const tab = activeTab.value;
172
- if (!tab) return;
186
+ if (!tab) {
187
+ return;
188
+ }
173
189
  try {
174
190
  await loadFormats();
175
191
  const format = tabFormat(tab);
@@ -182,10 +198,12 @@ export async function exportFile() {
182
198
  if ("showSaveFilePicker" in window) {
183
199
  const suggestedName = tab.documentPath ? tab.documentPath.split("/").pop() : fallbackName;
184
200
  const handle = await (
185
- window as unknown as { showSaveFilePicker: Function }
201
+ window as unknown as {
202
+ showSaveFilePicker: (options?: unknown) => Promise<FileSystemFileHandle>;
203
+ }
186
204
  ).showSaveFilePicker({
187
205
  suggestedName,
188
- types: [{ description, accept: { [mimeType]: [ext] } }],
206
+ types: [{ accept: { [mimeType]: [ext] }, description }],
189
207
  });
190
208
  const writable = await handle.createWritable();
191
209
  await writable.write(output);
@@ -204,8 +222,10 @@ export async function exportFile() {
204
222
  tab.doc.dirty = false;
205
223
  statusMessage("Downloaded");
206
224
  }
207
- } catch (e) {
208
- if ((e as Error).name !== "AbortError") statusMessage(`Export error: ${(e as Error).message}`);
225
+ } catch (error) {
226
+ if (!(error instanceof Error && error.name === "AbortError")) {
227
+ statusMessage(`Export error: ${errorMessage(error)}`);
228
+ }
209
229
  }
210
230
  }
211
231
 
@@ -216,7 +236,7 @@ export async function exportFile() {
216
236
  * @param {import("../tabs/tab.js").Tab} tab
217
237
  * @returns {Promise<string>}
218
238
  */
219
- export async function serializeDocument(tab: import("../tabs/tab.js").Tab): Promise<string> {
239
+ export async function serializeDocument(tab: Tab): Promise<string> {
220
240
  await loadFormats();
221
241
  const sourceFormat = formatByName(tab.doc.sourceFormat);
222
242
  if (sourceFormat?.capabilities.serialize) {
@@ -232,8 +252,8 @@ export async function serializeDocument(tab: import("../tabs/tab.js").Tab): Prom
232
252
  const hasFrontmatter = Object.keys(fm).length > 0;
233
253
  const fullDoc = { ...fm, ...tab.doc.document };
234
254
  return formatSerialize(format.name, fullDoc, {
235
- mode: "roundtrip",
236
255
  frontmatter: hasFrontmatter,
256
+ mode: "roundtrip",
237
257
  });
238
258
  }
239
259
  }