@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
@@ -8,10 +8,11 @@
8
8
  */
9
9
 
10
10
  import { html, nothing } from "lit-html";
11
+ import { errorMessage } from "@jxsuite/schema/parse";
11
12
  import { classMap } from "lit-html/directives/class-map.js";
12
13
  import { ref } from "lit-html/directives/ref.js";
13
- import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers";
14
- import { createState, projectState, setProjectState, requireProjectState } from "../store";
14
+ import { renderPopover, showConfirmDialog, showDialog } from "../ui/layers";
15
+ import { createState, projectState, requireProjectState, setProjectState } from "../store";
15
16
  import { getPlatform } from "../platform";
16
17
  import { statusMessage } from "../panels/statusbar";
17
18
  import { loadComponentRegistry } from "./components";
@@ -22,38 +23,44 @@ import {
22
23
  } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
23
24
  import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
24
25
  import {
25
- workspace,
26
- openTab,
27
26
  activateTab,
27
+ activeTab,
28
+ openTab,
28
29
  renameTab,
29
30
  replaceAllTabs,
30
- activeTab,
31
+ workspace,
31
32
  } from "../workspace/workspace";
32
33
  import { parseSourceForPath, serializeDocument } from "./file-ops";
33
34
  import {
35
+ documentExtensions,
36
+ formatForPath,
34
37
  loadFormats,
38
+ noFormatError,
35
39
  refreshFormats,
36
- formatForPath,
37
- documentExtensions,
38
40
  } from "../format/format-host";
39
41
  import { view } from "../view";
40
42
  import { addRecentProject, trackRecentFile } from "../recent-projects";
43
+ import type { TemplateResult } from "lit-html";
44
+ import type { StudioState } from "../state.js";
45
+ import type { Tab } from "../tabs/tab.js";
41
46
 
42
47
  // ─── File icon map ────────────────────────────────────────────────────────────
43
48
 
44
49
  const fileIconMap = {
45
- "sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
46
- "sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
50
+ "sp-icon-document": html`<sp-icon-document></sp-icon-document>`,
47
51
  "sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
48
52
  "sp-icon-file-txt": html`<sp-icon-file-txt></sp-icon-file-txt>`,
53
+ "sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
54
+ "sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
49
55
  "sp-icon-image": html`<sp-icon-image></sp-icon-image>`,
50
- "sp-icon-document": html`<sp-icon-document></sp-icon-document>`,
51
- } as Record<string, import("lit-html").TemplateResult>;
56
+ } as Record<string, TemplateResult>;
52
57
 
53
58
  // ─── File management ──────────────────────────────────────────────────────────
54
59
 
55
60
  export async function loadDirectory(dirPath: string) {
56
- if (!projectState) return;
61
+ if (!projectState) {
62
+ return;
63
+ }
57
64
  try {
58
65
  const platform = getPlatform();
59
66
  const entries = await platform.listDirectory(dirPath);
@@ -68,23 +75,25 @@ export async function loadProject() {
68
75
  try {
69
76
  const platform = getPlatform();
70
77
  const result = await platform.probeRootProject();
71
- if (!result) return;
78
+ if (!result) {
79
+ return;
80
+ }
72
81
  const { meta, info } = result;
73
82
 
74
83
  refreshFormats();
75
84
  void loadFormats();
76
85
 
77
86
  setProjectState({
78
- root: meta.root,
79
- name: info.isSiteProject ? info.projectConfig?.name || meta.name : meta.name,
80
- projectRoot: ".",
87
+ dirs: new Map(),
88
+ expanded: new Set(),
81
89
  isSiteProject: info.isSiteProject,
90
+ name: info.isSiteProject ? info.projectConfig?.name || meta.name : meta.name,
82
91
  projectConfig: (info.isSiteProject ? info.projectConfig : null) || null,
83
92
  projectDirs: info.directories || [],
84
- dirs: new Map(),
85
- expanded: new Set(),
86
- selectedPath: null,
93
+ projectRoot: ".",
94
+ root: meta.root,
87
95
  searchQuery: "",
96
+ selectedPath: null,
88
97
  });
89
98
 
90
99
  if (info.isSiteProject) {
@@ -118,13 +127,15 @@ export async function openProject({
118
127
  try {
119
128
  const platform = getPlatform();
120
129
  const result = await platform.openProject();
121
- if (!result) return; // User cancelled
130
+ if (!result) {
131
+ return;
132
+ } // User cancelled
122
133
 
123
134
  const { config, handle } = result;
124
135
 
125
136
  replaceAllTabs({
137
+ document: { children: [], tagName: "div" },
126
138
  id: "initial",
127
- document: { tagName: "div", children: [] },
128
139
  });
129
140
 
130
141
  refreshFormats();
@@ -132,21 +143,21 @@ export async function openProject({
132
143
 
133
144
  setProjectState({
134
145
  .../** @type {ProjectState} */ projectState,
135
- projectRoot: handle.root,
136
- isSiteProject: true,
137
- projectConfig: config,
138
- name: config.name || handle.name,
139
146
  dirs: new Map(),
140
147
  expanded: new Set(),
141
- selectedPath: null,
148
+ isSiteProject: true,
149
+ name: config.name || handle.name,
150
+ projectConfig: config,
151
+ projectRoot: handle.root,
142
152
  searchQuery: "",
153
+ selectedPath: null,
143
154
  });
144
155
 
145
156
  await loadDirectory(".");
146
157
  await loadComponentRegistry();
147
158
 
148
159
  // Auto-expand key directories and populate projectDirs for Browse view
149
- const conventionalDirs = [
160
+ const conventionalDirs = new Set([
150
161
  "pages",
151
162
  "layouts",
152
163
  "components",
@@ -154,11 +165,11 @@ export async function openProject({
154
165
  "data",
155
166
  "public",
156
167
  "styles",
157
- ];
168
+ ]);
158
169
  const entries = requireProjectState().dirs.get(".") || [];
159
170
  const foundDirs = [];
160
171
  for (const e of entries) {
161
- if (e.type === "directory" && conventionalDirs.includes(e.name)) {
172
+ if (e.type === "directory" && conventionalDirs.has(e.name)) {
162
173
  foundDirs.push(e.name);
163
174
  requireProjectState().expanded.add(e.path || e.name);
164
175
  await loadDirectory(e.path || e.name);
@@ -173,8 +184,8 @@ export async function openProject({
173
184
  statusMessage(`Opened project: ${requireProjectState().name}`);
174
185
 
175
186
  await openHomePage();
176
- } catch (e) {
177
- statusMessage(`Error: ${(e as Error).message}`);
187
+ } catch (error) {
188
+ statusMessage(`Error: ${errorMessage(error)}`);
178
189
  }
179
190
  }
180
191
 
@@ -203,30 +214,36 @@ function fileTypeIconTpl(name: string, type: string) {
203
214
  } else {
204
215
  const ext = name.split(".").pop()?.toLowerCase();
205
216
  switch (ext) {
206
- case "json":
217
+ case "json": {
207
218
  tag = "sp-icon-file-code";
208
219
  break;
209
- case "md":
220
+ }
221
+ case "md": {
210
222
  tag = "sp-icon-file-txt";
211
223
  break;
224
+ }
212
225
  case "js":
213
- case "ts":
226
+ case "ts": {
214
227
  tag = "sp-icon-file-code";
215
228
  break;
216
- case "css":
229
+ }
230
+ case "css": {
217
231
  tag = "sp-icon-file-code";
218
232
  break;
233
+ }
219
234
  case "png":
220
235
  case "jpg":
221
236
  case "jpeg":
222
237
  case "svg":
223
238
  case "webp":
224
- case "gif":
239
+ case "gif": {
225
240
  tag = "sp-icon-image";
226
241
  break;
227
- default:
242
+ }
243
+ default: {
228
244
  tag = "sp-icon-document";
229
245
  break;
246
+ }
230
247
  }
231
248
  }
232
249
  return fileIconMap[tag] || fileIconMap["sp-icon-document"];
@@ -287,7 +304,9 @@ export function renderFilesTemplate({
287
304
  @click=${async () => {
288
305
  requireProjectState().dirs.clear();
289
306
  await loadDirectory(".");
290
- for (const dir of requireProjectState().expanded) await loadDirectory(dir);
307
+ for (const dir of requireProjectState().expanded) {
308
+ await loadDirectory(dir);
309
+ }
291
310
  renderLeftPanel();
292
311
  }}
293
312
  >
@@ -317,7 +336,7 @@ function renderTreeLevelTemplate(
317
336
  dirPath: string,
318
337
  depth: number,
319
338
  ctx: { openFileFn: (path: string) => void; renderLeftPanel: () => void },
320
- ): import("lit-html").TemplateResult | import("lit-html").TemplateResult[] {
339
+ ): TemplateResult | TemplateResult[] {
321
340
  const entries = requireProjectState().dirs.get(dirPath);
322
341
  if (!entries) {
323
342
  loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
@@ -329,9 +348,13 @@ function renderTreeLevelTemplate(
329
348
  </div>`;
330
349
  }
331
350
 
332
- const sorted = [...entries].sort((a, b) => {
333
- if (a.type === "directory" && b.type !== "directory") return -1;
334
- if (a.type !== "directory" && b.type === "directory") return 1;
351
+ const sorted = [...entries].toSorted((a, b) => {
352
+ if (a.type === "directory" && b.type !== "directory") {
353
+ return -1;
354
+ }
355
+ if (a.type !== "directory" && b.type === "directory") {
356
+ return 1;
357
+ }
335
358
  return a.name.localeCompare(b.name);
336
359
  });
337
360
 
@@ -358,10 +381,13 @@ function renderTreeLevelTemplate(
358
381
  @click=${async (e: MouseEvent) => {
359
382
  e.stopPropagation();
360
383
  if (isDir) {
361
- if (isExpanded) requireProjectState().expanded.delete(entry.path);
362
- else {
384
+ if (isExpanded) {
385
+ requireProjectState().expanded.delete(entry.path);
386
+ } else {
363
387
  requireProjectState().expanded.add(entry.path);
364
- if (!requireProjectState().dirs.has(entry.path)) await loadDirectory(entry.path);
388
+ if (!requireProjectState().dirs.has(entry.path)) {
389
+ await loadDirectory(entry.path);
390
+ }
365
391
  }
366
392
  ctx.renderLeftPanel();
367
393
  } else {
@@ -375,7 +401,7 @@ function renderTreeLevelTemplate(
375
401
  }}
376
402
  >
377
403
  ${isDir
378
- ? html`<span class="file-tree-toggle">${isExpanded ? "\u25bc" : "\u25b6"}</span>`
404
+ ? html`<span class="file-tree-toggle">${isExpanded ? "\u25BC" : "\u25B6"}</span>`
379
405
  : html`<span class="file-tree-toggle empty"> </span>`}
380
406
  <span class="file-tree-icon">${fileTypeIconTpl(entry.path, entry.type)}</span>
381
407
  <span class="file-tree-name">${entry.name}</span>
@@ -391,52 +417,69 @@ export function setupTreeKeyboard(tree: HTMLElement) {
391
417
  tree.addEventListener("keydown", (e: KeyboardEvent) => {
392
418
  const items = [...tree.querySelectorAll(".file-tree-item")] as HTMLElement[];
393
419
  const focused = tree.querySelector(".file-tree-item:focus") as HTMLElement | null;
394
- if (!focused || items.length === 0) return;
420
+ if (!focused || items.length === 0) {
421
+ return;
422
+ }
395
423
 
396
424
  const idx = items.indexOf(focused);
397
425
  let handled = true;
398
426
 
399
427
  switch (e.key) {
400
- case "ArrowDown":
401
- if (idx < items.length - 1) items[idx + 1].focus();
428
+ case "ArrowDown": {
429
+ if (idx < items.length - 1) {
430
+ items[idx + 1].focus();
431
+ }
402
432
  break;
403
- case "ArrowUp":
404
- if (idx > 0) items[idx - 1].focus();
433
+ }
434
+ case "ArrowUp": {
435
+ if (idx > 0) {
436
+ items[idx - 1].focus();
437
+ }
405
438
  break;
406
- case "ArrowRight":
439
+ }
440
+ case "ArrowRight": {
407
441
  if (focused.dataset.type === "directory") {
408
442
  const path = focused.dataset.path as string;
409
443
  if (!requireProjectState().expanded.has(path)) {
410
444
  requireProjectState().expanded.add(path);
411
445
  loadDirectory(path).then(() => {
412
446
  const panel = tree.closest(".panel-body");
413
- if (panel)
447
+ if (panel) {
414
448
  (panel.querySelector(".file-tree-item:focus") as HTMLElement | null)?.click();
449
+ }
415
450
  });
416
451
  }
417
452
  }
418
453
  break;
419
- case "ArrowLeft":
454
+ }
455
+ case "ArrowLeft": {
420
456
  if (focused.dataset.type === "directory") {
421
457
  const path = focused.dataset.path as string;
422
458
  if (requireProjectState().expanded.has(path)) {
423
459
  requireProjectState().expanded.delete(path);
424
- // renderLeftPanel will be called by the caller who sets up keyboard
460
+ // RenderLeftPanel will be called by the caller who sets up keyboard
425
461
  }
426
462
  }
427
463
  break;
428
- case "Enter":
464
+ }
465
+ case "Enter": {
429
466
  focused.click();
430
467
  break;
431
- default:
468
+ }
469
+ default: {
432
470
  handled = false;
471
+ }
472
+ }
473
+ if (handled) {
474
+ e.preventDefault();
433
475
  }
434
- if (handled) e.preventDefault();
435
476
  });
436
477
 
437
478
  // Set first item focusable
438
479
  const first = tree.querySelector(".file-tree-item");
439
- if (first) first.setAttribute("tabindex", "0");
480
+ if (first) {
481
+ first.setAttribute("tabindex", "0");
482
+ }
440
483
  }
441
484
 
442
485
  let _fileTreeDndCleanups: (() => void)[] = [];
@@ -448,25 +491,31 @@ let _fileTreeDndCleanups: (() => void)[] = [];
448
491
  */
449
492
  export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () => void }) {
450
493
  // Clean up previous registrations
451
- for (const fn of _fileTreeDndCleanups) fn();
494
+ for (const fn of _fileTreeDndCleanups) {
495
+ fn();
496
+ }
452
497
  _fileTreeDndCleanups = [];
453
498
 
454
499
  requestAnimationFrame(() => {
455
500
  const tree = document.querySelector(".file-tree") as HTMLElement | null;
456
- if (!tree) return;
501
+ if (!tree) {
502
+ return;
503
+ }
457
504
 
458
505
  const items = tree.querySelectorAll(".file-tree-item") as NodeListOf<HTMLElement>;
459
506
 
460
- items.forEach((row) => {
461
- const path = row.dataset.path;
462
- const type = row.dataset.type;
463
- if (!path) return;
507
+ for (const row of items) {
508
+ const { path } = row.dataset;
509
+ const { type } = row.dataset;
510
+ if (!path) {
511
+ continue;
512
+ }
464
513
 
465
514
  const cleanups = [
466
515
  draggable({
467
516
  element: row,
468
517
  getInitialData() {
469
- return { type: "file-tree", path, entryType: type };
518
+ return { entryType: type, path, type: "file-tree" };
470
519
  },
471
520
  onDragStart() {
472
521
  row.classList.add("dragging");
@@ -480,21 +529,34 @@ export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () =
480
529
  if (type === "directory") {
481
530
  cleanups.push(
482
531
  dropTargetForElements({
483
- element: row,
484
532
  canDrop({ source }) {
485
- if (source.data.type !== "file-tree") return false;
533
+ if (source.data.type !== "file-tree") {
534
+ return false;
535
+ }
486
536
  const srcPath = source.data.path as string;
487
- if (srcPath === path) return false;
488
- if (srcPath.startsWith(path + "/")) return false;
537
+ if (srcPath === path) {
538
+ return false;
539
+ }
540
+ if (srcPath.startsWith(`${path}/`)) {
541
+ return false;
542
+ }
489
543
  const srcParent = parentDir(srcPath);
490
- if (srcParent === path) return false;
544
+ if (srcParent === path) {
545
+ return false;
546
+ }
491
547
  return true;
492
548
  },
493
- onDragEnter() {
494
- row.classList.add("drag-over");
549
+ element: row,
550
+ getData() {
551
+ return { targetDir: path, type: "file-tree-target" };
495
552
  },
496
553
  onDrag() {
497
- if (!row.classList.contains("drag-over")) row.classList.add("drag-over");
554
+ if (!row.classList.contains("drag-over")) {
555
+ row.classList.add("drag-over");
556
+ }
557
+ },
558
+ onDragEnter() {
559
+ row.classList.add("drag-over");
498
560
  },
499
561
  onDragLeave() {
500
562
  row.classList.remove("drag-over");
@@ -502,24 +564,26 @@ export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () =
502
564
  onDrop() {
503
565
  row.classList.remove("drag-over");
504
566
  },
505
- getData() {
506
- return { type: "file-tree-target", targetDir: path };
507
- },
508
567
  }),
509
568
  );
510
569
  }
511
570
 
512
571
  _fileTreeDndCleanups.push(combine(...cleanups));
513
- });
572
+ }
514
573
 
515
574
  // Root-level drop target (move to project root)
516
575
  const rootCleanup = dropTargetForElements({
517
- element: tree,
518
576
  canDrop({ source }) {
519
- if (source.data.type !== "file-tree") return false;
577
+ if (source.data.type !== "file-tree") {
578
+ return false;
579
+ }
520
580
  const srcPath = source.data.path as string;
521
581
  return parentDir(srcPath) !== ".";
522
582
  },
583
+ element: tree,
584
+ getData() {
585
+ return { targetDir: ".", type: "file-tree-target" };
586
+ },
523
587
  onDragEnter() {
524
588
  tree.classList.add("drag-over-root");
525
589
  },
@@ -529,26 +593,31 @@ export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () =
529
593
  onDrop() {
530
594
  tree.classList.remove("drag-over-root");
531
595
  },
532
- getData() {
533
- return { type: "file-tree-target", targetDir: "." };
534
- },
535
596
  });
536
597
  _fileTreeDndCleanups.push(rootCleanup);
537
598
 
538
599
  // Monitor for drop events
539
600
  const monitorCleanup = monitorForElements({
540
601
  onDrop({ source, location }) {
541
- const target = location.current.dropTargets[0];
542
- if (!target) return;
543
- if (source.data.type !== "file-tree") return;
544
- if (target.data.type !== "file-tree-target") return;
602
+ const [target] = location.current.dropTargets;
603
+ if (!target) {
604
+ return;
605
+ }
606
+ if (source.data.type !== "file-tree") {
607
+ return;
608
+ }
609
+ if (target.data.type !== "file-tree-target") {
610
+ return;
611
+ }
545
612
 
546
613
  const srcPath = source.data.path as string;
547
614
  const targetDirPath = target.data.targetDir as string;
548
615
  const fileName = srcPath.split("/").pop();
549
616
  const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
550
617
 
551
- if (newPath === srcPath) return;
618
+ if (newPath === srcPath) {
619
+ return;
620
+ }
552
621
 
553
622
  moveFileEntry(srcPath, newPath!, renderLeftPanel);
554
623
  },
@@ -573,7 +642,7 @@ async function moveFileEntry(oldPath: string, newPath: string, renderLeftPanel:
573
642
  for (const [id] of workspace.tabs.entries()) {
574
643
  if (id === oldPath) {
575
644
  renameTab(oldPath, newPath, newPath);
576
- } else if (id.startsWith(oldPath + "/")) {
645
+ } else if (id.startsWith(`${oldPath}/`)) {
577
646
  const newTabPath = newPath + id.slice(oldPath.length);
578
647
  renameTab(id, newTabPath, newTabPath);
579
648
  }
@@ -583,15 +652,19 @@ async function moveFileEntry(oldPath: string, newPath: string, renderLeftPanel:
583
652
  const oldParent = parentDir(oldPath);
584
653
  const newParent = parentDir(newPath);
585
654
  await loadDirectory(oldParent);
586
- if (newParent !== oldParent) await loadDirectory(newParent);
655
+ if (newParent !== oldParent) {
656
+ await loadDirectory(newParent);
657
+ }
587
658
 
588
659
  // Auto-expand target directory
589
- if (newParent !== ".") requireProjectState().expanded.add(newParent);
660
+ if (newParent !== ".") {
661
+ requireProjectState().expanded.add(newParent);
662
+ }
590
663
 
591
664
  renderLeftPanel();
592
665
  statusMessage(`Moved to ${newPath}`);
593
- } catch (e) {
594
- statusMessage(`Error: ${(e as Error).message}`);
666
+ } catch (error) {
667
+ statusMessage(`Error: ${errorMessage(error)}`);
595
668
  }
596
669
  }
597
670
 
@@ -599,7 +672,7 @@ async function moveFileEntry(oldPath: string, newPath: string, renderLeftPanel:
599
672
  function parentDir(path: string) {
600
673
  const normalized = path.replaceAll("\\", "/");
601
674
  const lastSlash = normalized.lastIndexOf("/");
602
- return lastSlash === -1 ? "." : normalized.substring(0, lastSlash);
675
+ return lastSlash === -1 ? "." : normalized.slice(0, lastSlash);
603
676
  }
604
677
 
605
678
  // ─── Context menu ─────────────────────────────────────────────────────────────
@@ -626,23 +699,23 @@ function showFileContextMenu(
626
699
  const items = [];
627
700
 
628
701
  if (!isDir) {
629
- items.push({ label: "Open", action: () => ctx.openFileFn(entry.path) });
702
+ items.push({ action: () => ctx.openFileFn(entry.path), label: "Open" });
630
703
  }
631
704
  if (isDir) {
632
705
  items.push({
633
- label: "New File\u2026",
634
706
  action: () => createNewFile(entry.path, ctx.renderLeftPanel),
707
+ label: "New File\u2026",
635
708
  });
636
709
  }
637
710
  items.push({ label: "\u2014" });
638
711
  items.push({
639
- label: "Rename\u2026",
640
712
  action: () => renameFile(entry, ctx.renderLeftPanel),
713
+ label: "Rename\u2026",
641
714
  });
642
715
  items.push({
643
- label: "Delete",
644
716
  action: () => deleteFile(entry, ctx.renderLeftPanel),
645
717
  danger: true,
718
+ label: "Delete",
646
719
  });
647
720
 
648
721
  let x = e.clientX,
@@ -653,13 +726,18 @@ function showFileContextMenu(
653
726
  open
654
727
  style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
655
728
  ${ref((el) => {
656
- if (!el) return;
729
+ if (!el) {
730
+ return;
731
+ }
657
732
  requestAnimationFrame(() => {
658
733
  const popover = el as HTMLElement;
659
734
  const menuRect = popover.getBoundingClientRect();
660
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
661
- if (y + menuRect.height > window.innerHeight)
735
+ if (x + menuRect.width > window.innerWidth) {
736
+ x = window.innerWidth - menuRect.width - 4;
737
+ }
738
+ if (y + menuRect.height > window.innerHeight) {
662
739
  y = window.innerHeight - menuRect.height - 4;
740
+ }
663
741
  popover.style.left = `${x}px`;
664
742
  popover.style.top = `${y}px`;
665
743
  });
@@ -691,9 +769,12 @@ function showFileContextMenu(
691
769
 
692
770
  // ─── File CRUD ────────────────────────────────────────────────────────────────
693
771
 
694
- async function createNewFile(dirPath = ".", renderLeftPanel: () => void) {
772
+ async function createNewFile(dirPath: string, renderLeftPanel: () => void) {
773
+ // oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
695
774
  const name = prompt("File name:", "untitled.json");
696
- if (!name) return;
775
+ if (!name) {
776
+ return;
777
+ }
697
778
  const path = dirPath === "." ? name : `${dirPath}/${name}`;
698
779
  await loadFormats();
699
780
  const format = formatForPath(name);
@@ -701,15 +782,15 @@ async function createNewFile(dirPath = ".", renderLeftPanel: () => void) {
701
782
  format?.studio?.newFileTemplate ??
702
783
  (format
703
784
  ? ""
704
- : JSON.stringify({ tagName: "div", children: [{ tagName: "p", children: [] }] }, null, 2));
785
+ : JSON.stringify({ children: [{ children: [], tagName: "p" }], tagName: "div" }, null, 2));
705
786
  try {
706
787
  const platform = getPlatform();
707
788
  await platform.writeFile(path, content);
708
789
  await loadDirectory(dirPath);
709
790
  renderLeftPanel();
710
791
  statusMessage(`Created ${path}`);
711
- } catch (e) {
712
- statusMessage(`Error: ${(e as Error).message}`);
792
+ } catch (error) {
793
+ statusMessage(`Error: ${errorMessage(error)}`);
713
794
  }
714
795
  }
715
796
 
@@ -719,7 +800,9 @@ function showRenameFileDialog(currentName: string): Promise<string | null> {
719
800
  return showDialog<string | null>((done) => {
720
801
  function confirm() {
721
802
  const trimmed = value.trim();
722
- if (!trimmed) return;
803
+ if (!trimmed) {
804
+ return;
805
+ }
723
806
  done(trimmed);
724
807
  }
725
808
  const tpl = html`
@@ -741,18 +824,22 @@ function showRenameFileDialog(currentName: string): Promise<string | null> {
741
824
  value = (e.target as HTMLInputElement).value || "";
742
825
  }}
743
826
  @keydown=${(e: KeyboardEvent) => {
744
- if (e.key === "Enter") confirm();
827
+ if (e.key === "Enter") {
828
+ confirm();
829
+ }
745
830
  }}
746
831
  ></sp-textfield>
747
832
  </sp-dialog-wrapper>
748
833
  `;
749
834
  requestAnimationFrame(() => {
750
- const layer = document.getElementById("layer-dialog");
835
+ const layer = document.querySelector("#layer-dialog");
751
836
  const tf = layer?.querySelector("sp-textfield") as HTMLElement | null;
752
837
  if (tf) {
753
838
  tf.focus();
754
839
  const input = tf.shadowRoot?.querySelector("input");
755
- if (input) input.select();
840
+ if (input) {
841
+ input.select();
842
+ }
756
843
  }
757
844
  });
758
845
  return tpl;
@@ -764,16 +851,18 @@ async function renameFile(
764
851
  renderLeftPanel: () => void,
765
852
  ) {
766
853
  const newName = await showRenameFileDialog(entry.name);
767
- if (!newName || newName === entry.name) return;
854
+ if (!newName || newName === entry.name) {
855
+ return;
856
+ }
768
857
  const entryPath = entry.path.replaceAll("\\", "/");
769
- const parentDir = entryPath.includes("/")
770
- ? entryPath.substring(0, entryPath.lastIndexOf("/"))
858
+ const parentDirPath = entryPath.includes("/")
859
+ ? entryPath.slice(0, entryPath.lastIndexOf("/"))
771
860
  : ".";
772
- const newPath = parentDir === "." ? newName : `${parentDir}/${newName}`;
861
+ const newPath = parentDirPath === "." ? newName : `${parentDirPath}/${newName}`;
773
862
  try {
774
863
  const platform = getPlatform();
775
864
  await platform.renameFile(entry.path, newPath);
776
- await loadDirectory(parentDir);
865
+ await loadDirectory(parentDirPath);
777
866
  if (requireProjectState().selectedPath === entry.path) {
778
867
  requireProjectState().selectedPath = newPath;
779
868
  }
@@ -782,8 +871,8 @@ async function renameFile(
782
871
  }
783
872
  renderLeftPanel();
784
873
  statusMessage(`Renamed to ${newName}`);
785
- } catch (e) {
786
- statusMessage(`Error: ${(e as Error).message}`);
874
+ } catch (error) {
875
+ statusMessage(`Error: ${errorMessage(error)}`);
787
876
  }
788
877
  }
789
878
 
@@ -795,20 +884,22 @@ async function deleteFile(
795
884
  confirmLabel: "Delete",
796
885
  destructive: true,
797
886
  });
798
- if (!confirmed) return;
887
+ if (!confirmed) {
888
+ return;
889
+ }
799
890
  try {
800
891
  const platform = getPlatform();
801
892
  await platform.deleteFile(entry.path);
802
893
  const delPath = entry.path.replaceAll("\\", "/");
803
- const parentDir = delPath.includes("/") ? delPath.substring(0, delPath.lastIndexOf("/")) : ".";
804
- await loadDirectory(parentDir);
894
+ const parentDirPath = delPath.includes("/") ? delPath.slice(0, delPath.lastIndexOf("/")) : ".";
895
+ await loadDirectory(parentDirPath);
805
896
  if (requireProjectState().selectedPath === entry.path) {
806
897
  requireProjectState().selectedPath = null;
807
898
  }
808
899
  renderLeftPanel();
809
900
  statusMessage(`Deleted ${entry.name}`);
810
- } catch (e) {
811
- statusMessage(`Error: ${(e as Error).message}`);
901
+ } catch (error) {
902
+ statusMessage(`Error: ${errorMessage(error)}`);
812
903
  }
813
904
  }
814
905
 
@@ -827,8 +918,8 @@ async function deleteFile(
827
918
  */
828
919
  export async function openFileFromTree(
829
920
  ctx: {
830
- S: import("../state.js").StudioState;
831
- commit: (s: import("../state.js").StudioState) => void;
921
+ S: StudioState;
922
+ commit: (s: StudioState) => void;
832
923
  render: () => void;
833
924
  loadMarkdown: (source: string, handle: unknown) => void;
834
925
  },
@@ -841,35 +932,39 @@ export async function openFileFromTree(
841
932
  try {
842
933
  const tabLike = {
843
934
  doc: {
844
- sourceFormat: formatForPath(ctx.S.documentPath)?.name ?? null,
845
- mode: ctx.S.mode,
846
935
  content: ctx.S.content,
847
936
  document: ctx.S.document,
937
+ mode: ctx.S.mode,
938
+ sourceFormat: formatForPath(ctx.S.documentPath)?.name ?? null,
848
939
  },
849
- } as unknown as import("../tabs/tab.js").Tab;
940
+ } as unknown as Tab;
850
941
  const output = await serializeDocument(tabLike);
851
942
  await platform.writeFile(ctx.S.documentPath, output);
852
- } catch (e) {
853
- statusMessage(`Save error: ${(e as Error).message}`);
943
+ } catch (error) {
944
+ statusMessage(`Save error: ${errorMessage(error)}`);
854
945
  }
855
946
  }
856
947
 
857
948
  // Fetch the file
858
949
  try {
859
950
  const content = await platform.readFile(path);
860
- if (!content) return;
951
+ if (!content) {
952
+ return;
953
+ }
861
954
 
862
955
  if (formatForPath(path)) {
863
956
  await ctx.loadMarkdown(content, null);
864
957
  ctx.S.documentPath = path;
865
958
  ctx.S.dirty = false;
866
959
  ctx.commit(ctx.S);
867
- } else {
960
+ } else if (path.endsWith(".json")) {
868
961
  const doc = JSON.parse(content);
869
962
  const newS = createState(doc);
870
963
  newS.documentPath = path;
871
964
  newS.dirty = false;
872
965
  ctx.commit(newS);
966
+ } else {
967
+ throw noFormatError(path);
873
968
  }
874
969
 
875
970
  // Update tree selection
@@ -877,8 +972,8 @@ export async function openFileFromTree(
877
972
 
878
973
  ctx.render();
879
974
  statusMessage(`Opened ${path}`);
880
- } catch (e) {
881
- statusMessage(`Error: ${(e as Error).message}`);
975
+ } catch (error) {
976
+ statusMessage(`Error: ${errorMessage(error)}`);
882
977
  }
883
978
  }
884
979
 
@@ -899,17 +994,21 @@ export async function openFileInTab(path: string) {
899
994
  const platform = getPlatform();
900
995
  try {
901
996
  const content = await platform.readFile(path);
902
- if (!content) return;
997
+ if (!content) {
998
+ return;
999
+ }
903
1000
 
904
1001
  await loadFormats();
905
1002
  let document, frontmatter;
906
1003
  const format = formatForPath(path);
907
1004
  if (format) {
908
1005
  const result = await parseSourceForPath(path, content);
909
- document = result.document;
910
- frontmatter = result.frontmatter;
911
- } else {
1006
+ ({ document } = result);
1007
+ ({ frontmatter } = result);
1008
+ } else if (path.endsWith(".json")) {
912
1009
  document = JSON.parse(content);
1010
+ } else {
1011
+ throw noFormatError(path);
913
1012
  }
914
1013
 
915
1014
  const id = path;
@@ -921,16 +1020,18 @@ export async function openFileInTab(path: string) {
921
1020
  sourceFormat: format?.name ?? null,
922
1021
  });
923
1022
  requireProjectState().selectedPath = path;
924
- trackRecentFile({ path, name: path.split("/").pop() || path });
1023
+ trackRecentFile({ name: path.split("/").pop() || path, path });
925
1024
 
926
1025
  if (path === "project.json") {
927
1026
  const tab = activeTab.value;
928
- if (tab) tab.session.ui.canvasMode = "stylebook";
1027
+ if (tab) {
1028
+ tab.session.ui.canvasMode = "stylebook";
1029
+ }
929
1030
  }
930
1031
 
931
1032
  statusMessage(`Opened ${path.split("/").pop()}`);
932
- } catch (e) {
933
- statusMessage(`Error: ${(e as Error).message}`);
1033
+ } catch (error) {
1034
+ statusMessage(`Error: ${errorMessage(error)}`);
934
1035
  }
935
1036
  }
936
1037
 
@@ -946,13 +1047,15 @@ export async function reloadFileInTab(path: string) {
946
1047
  const platform = getPlatform();
947
1048
  try {
948
1049
  const content = await platform.readFile(path);
949
- if (!content) return;
1050
+ if (!content) {
1051
+ return;
1052
+ }
950
1053
  await loadFormats();
951
1054
  if (formatForPath(path)) {
952
1055
  const { document, frontmatter } = await parseSourceForPath(path, content);
953
1056
  tab.doc.document = document;
954
1057
  tab.doc.content.frontmatter = frontmatter;
955
- } else {
1058
+ } else if (path.endsWith(".json")) {
956
1059
  tab.doc.document = JSON.parse(content);
957
1060
  }
958
1061
  tab.doc.dirty = false;