@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/studio.ts CHANGED
@@ -7,74 +7,74 @@
7
7
  */
8
8
 
9
9
  import "./services/monaco-setup.js";
10
+ import { errorMessage } from "@jxsuite/schema/parse";
10
11
 
11
12
  import {
12
- getNodeAtPath,
13
13
  canvasWrap,
14
- toolbarEl,
14
+ getNodeAtPath,
15
+ initShellRefs,
16
+ projectState,
15
17
  registerRenderer,
16
18
  render,
17
- projectState,
18
- setProjectState,
19
19
  requireProjectState,
20
+ setProjectState,
21
+ toolbarEl,
20
22
  updateUi,
21
- initShellRefs,
22
23
  } from "./store";
23
24
 
24
- import { activeTab, openTab, closeAllTabs } from "./workspace/workspace";
25
- import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact";
25
+ import { activeTab, closeAllTabs, openTab } from "./workspace/workspace";
26
+ import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "./tabs/transact";
26
27
  import { effect } from "./reactivity";
27
28
 
28
29
  import { view } from "./view";
29
30
 
30
- import { isEditing, isEditableBlock } from "./editor/inline-edit";
31
+ import { isEditableBlock, isEditing } from "./editor/inline-edit";
31
32
  import { initComponentInlineEdit } from "./editor/component-inline-edit";
32
33
  import { enterInlineEdit } from "./editor/content-inline-edit";
33
- import { initCanvasUtils, applyTransform, positionZoomIndicator } from "./canvas/canvas-utils";
34
- import { initCanvasHelpers, getActivePanel, findCanvasElement } from "./canvas/canvas-helpers";
34
+ import { applyTransform, initCanvasUtils, positionZoomIndicator } from "./canvas/canvas-utils";
35
+ import { findCanvasElement, getActivePanel, initCanvasHelpers } from "./canvas/canvas-helpers";
35
36
  import { initCanvasRender, renderCanvas } from "./canvas/canvas-render";
36
37
  import { initCanvasLiveRender } from "./canvas/canvas-live-render";
37
38
  import {
39
+ mountStatusbar,
38
40
  renderStatusbar,
39
- statusMessage,
40
41
  setStatusbarRenderer,
41
- mountStatusbar,
42
+ statusMessage,
42
43
  } from "./panels/statusbar";
43
- import { parseSourceForPath, saveFile, exportFile, serializeDocument } from "./files/file-ops";
44
- import { loadFormats, formatForPath, documentExtensions } from "./format/format-host";
44
+ import { exportFile, parseSourceForPath, saveFile, serializeDocument } from "./files/file-ops";
45
+ import { documentExtensions, formatForPath, loadFormats } from "./format/format-host";
45
46
  import {
46
47
  loadProject as _loadProject,
47
48
  openProject as _openProject,
48
49
  renderFilesTemplate as _renderFilesTemplate,
50
+ loadDirectory,
49
51
  openFileInTab,
50
52
  openHomePage,
51
- setupTreeKeyboard,
52
53
  registerFileTreeDnD,
53
- loadDirectory,
54
+ setupTreeKeyboard,
54
55
  } from "./files/files";
55
56
  import { renderImportsTemplate } from "./panels/imports-panel";
56
57
  import { renderHeadTemplate } from "./panels/head-panel";
57
58
  import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
58
59
 
59
- import { registerPlatform, getPlatform, hasPlatform } from "./platform";
60
+ import { getPlatform, hasPlatform, registerPlatform } from "./platform";
60
61
  import { parseMediaEntries } from "./utils/canvas-media";
61
62
  import { createDevServerPlatform } from "./platforms/devserver";
62
63
  import { mountResizeEdges } from "./resize-edges";
63
64
  import { codeService } from "./services/code-services";
64
- import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel";
65
+ import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/signals-panel";
65
66
  import { loadComponentRegistry } from "./files/components";
66
67
 
67
68
  import { html, render as litRender } from "lit-html";
68
69
 
69
70
  import webdata from "../data/webdata.json";
70
71
  import { renderDataExplorerTemplate } from "./panels/data-explorer";
71
- import { renderGitPanel } from "./panels/git-panel";
72
+ import { cloneRepository, renderGitPanel } from "./panels/git-panel";
72
73
 
73
74
  // ─── Spectrum Web Components ──────────────────────────────────────────────────
74
75
  // Explicit class imports + registration — bare side-effect imports are tree-shaken
75
- // by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
76
+ // By Bun's bundler despite sideEffects declarations in Spectrum's package.json.
76
77
  import { components as _swc } from "./ui/spectrum";
77
- void _swc;
78
78
  import "./ui/panel-resize.js";
79
79
  import { initLayers } from "./ui/layers";
80
80
  import { initShortcuts } from "./editor/shortcuts";
@@ -96,14 +96,15 @@ import { initQuickSearch } from "./panels/quick-search";
96
96
  import { addRecentProject } from "./recent-projects";
97
97
  import { initWelcome } from "./panels/welcome-screen";
98
98
  import { openNewProjectModal } from "./new-project/new-project-modal";
99
- import { cloneRepository } from "./panels/git-panel";
100
99
  import type { DocumentStackEntry, GitDiffState } from "./types";
101
100
  import type { JxPath } from "./state";
102
101
  import type { JxMutableNode } from "@jxsuite/schema/types";
103
102
 
103
+ void _swc;
104
+
104
105
  // ─── Globals ──────────────────────────────────────────────────────────────────
105
106
  // These mutable variables are local to studio.js for now. As sections are extracted
106
- // into their own modules, they will migrate to ctx in store.js.
107
+ // Into their own modules, they will migrate to ctx in store.js.
107
108
 
108
109
  function getCanvasMode() {
109
110
  return activeTab.value?.session.ui.canvasMode ?? "design";
@@ -115,7 +116,9 @@ function setCanvasMode(mode: string) {
115
116
  gitDiffState = null;
116
117
  }
117
118
  const tab = activeTab.value;
118
- if (tab) tab.session.ui.canvasMode = mode;
119
+ if (tab) {
120
+ tab.session.ui.canvasMode = mode;
121
+ }
119
122
  }
120
123
 
121
124
  let gitDiffState: GitDiffState | null = null;
@@ -127,21 +130,27 @@ async function navigateToComponent(componentPath: string) {
127
130
  try {
128
131
  const platform = getPlatform();
129
132
  const content = await platform.readFile(componentPath);
130
- if (!content) return;
133
+ if (!content) {
134
+ return;
135
+ }
131
136
  const parsed = JSON.parse(content);
132
137
  const tab = activeTab.value;
133
- if (!tab) return;
138
+ if (!tab) {
139
+ return;
140
+ }
134
141
 
135
142
  // Push current state onto the document stack
136
143
  const frame = {
144
+ dirty: tab.doc.dirty,
137
145
  document: tab.doc.document,
138
- selection: tab.session.selection,
139
146
  documentPath: tab.documentPath,
140
- dirty: tab.doc.dirty,
141
147
  mode: tab.doc.mode,
148
+ selection: tab.session.selection,
142
149
  sourceFormat: tab.doc.sourceFormat,
143
150
  };
144
- if (!tab.session.documentStack) tab.session.documentStack = [];
151
+ if (!tab.session.documentStack) {
152
+ tab.session.documentStack = [];
153
+ }
145
154
  tab.session.documentStack.push(frame);
146
155
 
147
156
  // Load the component
@@ -157,28 +166,32 @@ async function navigateToComponent(componentPath: string) {
157
166
 
158
167
  render();
159
168
  statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
160
- } catch (e) {
161
- const err = e as Error;
169
+ } catch (error) {
170
+ const err = error as Error;
162
171
  statusMessage(`Error: ${err.message}`);
163
172
  }
164
173
  }
165
174
 
166
175
  async function navigateBack() {
167
176
  const tab = activeTab.value;
168
- if (!tab?.session.documentStack || tab.session.documentStack.length === 0) return;
177
+ if (!tab?.session.documentStack || tab.session.documentStack.length === 0) {
178
+ return;
179
+ }
169
180
  if (tab.doc.dirty && tab.documentPath) {
170
181
  try {
171
182
  const platform = getPlatform();
172
183
  await platform.writeFile(tab.documentPath, await serializeDocument(tab));
173
- } catch (e) {
174
- const err = e as Error;
184
+ } catch (error) {
185
+ const err = error as Error;
175
186
  statusMessage(`Save error: ${err.message}`);
176
187
  }
177
188
  }
178
189
 
179
190
  // Pop the stack
180
191
  const frame = tab.session.documentStack.pop() as Record<string, unknown> | undefined;
181
- if (!frame) return;
192
+ if (!frame) {
193
+ return;
194
+ }
182
195
  tab.doc.document = frame.document as JxMutableNode;
183
196
  tab.doc.dirty = frame.dirty as boolean;
184
197
  tab.doc.mode = frame.mode as string;
@@ -195,13 +208,15 @@ async function navigateBack() {
195
208
  async function navigateToLevel(targetIndex: number) {
196
209
  const tab = activeTab.value;
197
210
  const stack = tab?.session.documentStack;
198
- if (!stack || targetIndex < 0 || targetIndex >= stack.length) return;
211
+ if (!stack || targetIndex < 0 || targetIndex >= stack.length) {
212
+ return;
213
+ }
199
214
  if (tab.doc.dirty && tab.documentPath) {
200
215
  try {
201
216
  const platform = getPlatform();
202
217
  await platform.writeFile(tab.documentPath, await serializeDocument(tab));
203
- } catch (e) {
204
- const err = e as Error;
218
+ } catch (error) {
219
+ const err = error as Error;
205
220
  statusMessage(`Save error: ${err.message}`);
206
221
  }
207
222
  }
@@ -225,7 +240,9 @@ async function closeFunctionEditor() {
225
240
  const editing =
226
241
  /** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ tab
227
242
  ?.session.ui.editingFunction;
228
- if (!editing || !tab) return;
243
+ if (!editing || !tab) {
244
+ return;
245
+ }
229
246
  if (view.functionEditor) {
230
247
  const currentCode = view.functionEditor.getValue();
231
248
  const minResult = await codeService("minify", { code: currentCode });
@@ -253,7 +270,7 @@ async function closeFunctionEditor() {
253
270
 
254
271
  const datalistHost = document.createElement("div");
255
272
  datalistHost.style.display = "contents";
256
- document.body.appendChild(datalistHost);
273
+ document.body.append(datalistHost);
257
274
  litRender(
258
275
  html`
259
276
  <datalist id="tag-names">
@@ -265,15 +282,17 @@ litRender(
265
282
  );
266
283
 
267
284
  requestIdleCallback(() => {
268
- const dl = document.getElementById("css-props");
269
- if (!dl) return;
285
+ const dl = document.querySelector("#css-props");
286
+ if (!dl) {
287
+ return;
288
+ }
270
289
  const frag = document.createDocumentFragment();
271
290
  for (const [name] of webdata.cssProps) {
272
291
  const opt = document.createElement("option");
273
292
  opt.value = name;
274
- frag.appendChild(opt);
293
+ frag.append(opt);
275
294
  }
276
- dl.appendChild(frag);
295
+ dl.append(frag);
277
296
  });
278
297
 
279
298
  initCssData(webdata);
@@ -295,17 +314,17 @@ initShellRefs();
295
314
 
296
315
  // Mount extracted panel modules
297
316
  toolbarPanel.mount(toolbarEl, {
317
+ closeFunctionEditor: () => closeFunctionEditor(),
318
+ getCanvasMode,
298
319
  navigateBack: () => navigateBack(),
299
320
  navigateToLevel: (i: number) => navigateToLevel(i),
300
- closeFunctionEditor: () => closeFunctionEditor(),
301
321
  openProject: () => openProject(),
302
322
  openRecentProject: (root: string) => openRecentProject(root),
303
- saveFile: () => saveFile(),
304
323
  parseMediaEntries,
305
- getCanvasMode,
306
- setCanvasMode,
307
324
  renderCanvas: () => renderCanvas(),
308
325
  safeRenderRightPanel: () => safeRenderRightPanel(),
326
+ saveFile: () => saveFile(),
327
+ setCanvasMode,
309
328
  });
310
329
 
311
330
  initLayers();
@@ -332,41 +351,45 @@ initCanvasHelpers({
332
351
  initCanvasUtils({
333
352
  getCanvasMode,
334
353
  getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
354
+ renderStylebookOverlays,
335
355
  setZoomDirect: (zoom) => {
336
- if (activeTab.value) activeTab.value.session.ui.zoom = zoom;
356
+ if (activeTab.value) {
357
+ activeTab.value.session.ui.zoom = zoom;
358
+ }
337
359
  },
338
- renderStylebookOverlays,
339
360
  });
340
361
  initPanelEvents({
341
- getCanvasMode,
342
362
  enterInlineEdit,
363
+ getCanvasMode,
343
364
  navigateToComponent,
344
365
  });
345
366
  initCanvasLiveRender({
346
367
  getCanvasMode,
347
368
  });
348
369
  initCanvasRender({
349
- getCanvasMode,
350
- setCanvasMode,
351
- openFileFromTree,
352
- exportFile,
353
370
  closeFunctionEditor: () => closeFunctionEditor(),
371
+ exportFile,
372
+ getCanvasMode,
354
373
  get gitDiffState() {
355
374
  return gitDiffState;
356
375
  },
376
+ openFileFromTree,
377
+ setCanvasMode,
357
378
  setGitDiffState: (state: GitDiffState | null) => {
358
379
  gitDiffState = state;
359
380
  },
360
381
  });
361
382
 
362
383
  initWelcome({
363
- openProject: () => openProject(),
364
- openRecentProject: (root: string) => openRecentProject(root),
384
+ cloneRepository: () => cloneRepository({ openRecentProject }),
365
385
  openNewProject: async () => {
366
386
  const result = await openNewProjectModal();
367
- if (result) openRecentProject(result.root);
387
+ if (result) {
388
+ openRecentProject(result.root);
389
+ }
368
390
  },
369
- cloneRepository: () => cloneRepository({ openRecentProject }),
391
+ openProject: () => openProject(),
392
+ openRecentProject: (root: string) => openRecentProject(root),
370
393
  });
371
394
 
372
395
  // Effect-driven canvas rendering: auto-triggers renderCanvas when reactive deps change.
@@ -391,8 +414,8 @@ effect(() => {
391
414
  _canvasRafId = 0;
392
415
  try {
393
416
  renderCanvas();
394
- } catch (e) {
395
- console.error("renderCanvas error:", e);
417
+ } catch (error) {
418
+ console.error("renderCanvas error:", error);
396
419
  }
397
420
  });
398
421
  });
@@ -400,36 +423,36 @@ effect(() => {
400
423
  });
401
424
 
402
425
  rightPanelMod.mount({
403
- navigateToComponent,
404
426
  getCanvasMode,
427
+ navigateToComponent,
405
428
  renderCanvas: () => renderCanvas(),
406
429
  updateForcedPseudoPreview,
407
430
  });
408
431
 
409
432
  leftPanelMod.mount({
410
- getCanvasMode,
411
- setCanvasMode,
412
- renderImportsTemplate,
413
- renderFilesTemplate,
414
- renderSignalsTemplate,
415
- renderDataExplorerTemplate,
416
- renderHeadTemplate,
417
- renderGitPanel,
418
- renderCanvas: () => renderCanvas(),
419
- defCategory,
433
+ cloneRepository: () => cloneRepository({ openRecentProject }),
420
434
  defBadgeLabel,
421
- navigateToComponent,
422
- webdata,
435
+ defCategory,
423
436
  defaultDef,
424
- registerLayersDnD,
425
- registerElementsDnD,
437
+ getCanvasMode,
438
+ navigateToComponent,
426
439
  registerComponentsDnD,
427
- setupTreeKeyboard,
440
+ registerElementsDnD,
428
441
  registerFileTreeDnD,
429
- cloneRepository: () => cloneRepository({ openRecentProject }),
442
+ registerLayersDnD,
443
+ renderCanvas: () => renderCanvas(),
444
+ renderDataExplorerTemplate,
445
+ renderFilesTemplate,
446
+ renderGitPanel,
447
+ renderHeadTemplate,
448
+ renderImportsTemplate,
449
+ renderSignalsTemplate,
450
+ setCanvasMode,
430
451
  setGitDiffState: (state: GitDiffState | null) => {
431
452
  gitDiffState = state;
432
453
  },
454
+ setupTreeKeyboard,
455
+ webdata,
433
456
  });
434
457
 
435
458
  // Register all renderers with the store so render()/renderOnly() work
@@ -444,8 +467,12 @@ mountActivityBar();
444
467
 
445
468
  // Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
446
469
  canvasWrap.addEventListener("click", (e: MouseEvent) => {
447
- if (e.target !== canvasWrap && e.target !== view.panzoomWrap) return;
448
- if (!activeTab.value?.session.selection) return;
470
+ if (e.target !== canvasWrap && e.target !== view.panzoomWrap) {
471
+ return;
472
+ }
473
+ if (!activeTab.value?.session.selection) {
474
+ return;
475
+ }
449
476
  activeTab.value.session.selection = null;
450
477
  });
451
478
 
@@ -471,6 +498,7 @@ if (_projectParam) {
471
498
  } else {
472
499
  render();
473
500
  const platform = getPlatform();
501
+ // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: project probing must not block the initial render
474
502
  (async () => {
475
503
  try {
476
504
  const siteCtx = platform.resolveSiteContext
@@ -482,26 +510,28 @@ if (_projectParam) {
482
510
  if (siteCtx.sitePath) {
483
511
  platform.projectRoot = siteCtx.sitePath;
484
512
  // Await activation so the server resolves project-relative static files
485
- if (platform.activate) await platform.activate();
513
+ if (platform.activate) {
514
+ await platform.activate();
515
+ }
486
516
  }
487
517
 
488
518
  setProjectState({
489
- root: siteCtx.sitePath,
490
- name: siteCtx.projectConfig?.name || "Project",
491
- projectRoot: siteCtx.sitePath,
519
+ dirs: new Map(),
520
+ expanded: new Set(),
492
521
  isSiteProject: true,
522
+ name: siteCtx.projectConfig?.name || "Project",
493
523
  projectConfig: siteCtx.projectConfig || null,
494
524
  projectDirs: [],
495
- dirs: new Map(),
496
- expanded: new Set(),
497
- selectedPath: siteCtx.fileRelPath || null,
525
+ projectRoot: siteCtx.sitePath,
526
+ root: siteCtx.sitePath,
498
527
  searchQuery: "",
528
+ selectedPath: siteCtx.fileRelPath || null,
499
529
  });
500
530
 
501
531
  await loadComponentRegistry();
502
532
 
503
533
  // Load directory tree and populate projectDirs from conventional dirs found
504
- const conventionalDirs = [
534
+ const conventionalDirs = new Set([
505
535
  "pages",
506
536
  "layouts",
507
537
  "components",
@@ -509,12 +539,12 @@ if (_projectParam) {
509
539
  "data",
510
540
  "public",
511
541
  "styles",
512
- ];
542
+ ]);
513
543
  const dirEntries = await platform.listDirectory(".");
514
544
  requireProjectState().dirs.set(".", dirEntries);
515
545
  const foundDirs = [];
516
546
  for (const e of dirEntries) {
517
- if (e.type === "directory" && conventionalDirs.includes(e.name)) {
547
+ if (e.type === "directory" && conventionalDirs.has(e.name)) {
518
548
  foundDirs.push(e.name);
519
549
  requireProjectState().expanded.add(e.path || e.name);
520
550
  const sub = await platform.listDirectory(e.path || e.name);
@@ -544,18 +574,22 @@ if (_projectParam) {
544
574
  break;
545
575
  } catch {}
546
576
  }
547
- if (!opened) fileRelPath = "project.json";
577
+ if (!opened) {
578
+ fileRelPath = "project.json";
579
+ }
548
580
  }
549
581
 
550
582
  const content = await platform.readFile(fileRelPath);
551
583
  if (content) {
552
- let parsedDoc, frontmatter, parsedMode;
584
+ let frontmatter, parsedDoc, parsedMode;
553
585
  await loadFormats();
554
586
  const fileFormat = formatForPath(fileRelPath);
555
- if (fileFormat) {
587
+ if (fileFormat || !fileRelPath.endsWith(".json")) {
588
+ // ParseSourceForPath throws a descriptive error when no format class claims the
589
+ // Extension (better than letting JSON.parse choke on non-JSON source).
556
590
  const result = await parseSourceForPath(fileRelPath, content);
557
591
  parsedDoc = result.document;
558
- frontmatter = result.frontmatter;
592
+ ({ frontmatter } = result);
559
593
  parsedMode = result.mode;
560
594
  } else {
561
595
  parsedDoc = JSON.parse(content);
@@ -570,7 +604,9 @@ if (_projectParam) {
570
604
  sourceFormat: fileFormat?.name ?? null,
571
605
  });
572
606
 
573
- if (parsedMode === "content" && activeTab.value) activeTab.value.doc.mode = "content";
607
+ if (parsedMode === "content" && activeTab.value) {
608
+ activeTab.value.doc.mode = "content";
609
+ }
574
610
  if (fileRelPath === "project.json" && activeTab.value) {
575
611
  activeTab.value.session.ui.canvasMode = "stylebook";
576
612
  }
@@ -578,8 +614,8 @@ if (_projectParam) {
578
614
  render();
579
615
  statusMessage(`Opened ${fileRelPath}`);
580
616
  }
581
- } catch (e) {
582
- statusMessage(`Error: ${(e as Error).message}`);
617
+ } catch (error) {
618
+ statusMessage(`Error: ${errorMessage(error)}`);
583
619
  }
584
620
  })();
585
621
  }
@@ -595,27 +631,6 @@ function renderLeftPanel() {
595
631
  leftPanelMod.render();
596
632
  }
597
633
 
598
- // ─── DnD registration: delegated to panels/dnd.js ───────────────────────────
599
-
600
- // ─── Stylebook ───────────────────────────────────────────────────────────────
601
- // Extracted to panels/stylebook-panel.js
602
-
603
- // ─── Inspector ────────────────────────────────────────────────────────────────
604
- // Extracted to panels/properties-panel.js
605
-
606
- // ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
607
-
608
- // UNIT_RE — imported from ui/unit-selector.js
609
-
610
- // inferInputType — imported from studio-utils.js
611
-
612
- // ─── Style panel ────────────────────────────────────────────────────────────
613
- // Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
614
-
615
- // ─── Source/Function editors: delegated to panels/editors.js ─────────────────
616
-
617
- // ─── File tree (delegated to files.js) ───────────────────────────────────────
618
-
619
634
  function loadProject() {
620
635
  return _loadProject();
621
636
  }
@@ -636,20 +651,20 @@ async function openRecentProject(root: string) {
636
651
 
637
652
  setProjectState({
638
653
  ...projectState,
639
- projectRoot: root,
640
- isSiteProject: true,
641
- projectConfig: config,
642
- name: config.name || root.split("/").pop(),
643
654
  dirs: new Map(),
644
655
  expanded: new Set(),
645
- selectedPath: null,
656
+ isSiteProject: true,
657
+ name: config.name || root.split("/").pop(),
658
+ projectConfig: config,
659
+ projectRoot: root,
646
660
  searchQuery: "",
661
+ selectedPath: null,
647
662
  });
648
663
 
649
664
  await loadDirectory(".");
650
665
  await loadComponentRegistry();
651
666
 
652
- const conventionalDirs = [
667
+ const conventionalDirs = new Set([
653
668
  "pages",
654
669
  "layouts",
655
670
  "components",
@@ -657,10 +672,10 @@ async function openRecentProject(root: string) {
657
672
  "data",
658
673
  "public",
659
674
  "styles",
660
- ];
675
+ ]);
661
676
  const entries = requireProjectState().dirs.get(".") || [];
662
677
  for (const e of entries) {
663
- if (e.type === "directory" && conventionalDirs.includes(e.name)) {
678
+ if (e.type === "directory" && conventionalDirs.has(e.name)) {
664
679
  requireProjectState().expanded.add(e.path || e.name);
665
680
  await loadDirectory(e.path || e.name);
666
681
  }
@@ -673,14 +688,14 @@ async function openRecentProject(root: string) {
673
688
  statusMessage(`Opened project: ${requireProjectState().name}`);
674
689
 
675
690
  await openHomePage();
676
- } catch (e) {
677
- statusMessage(`Error: ${(e as Error).message}`);
691
+ } catch (error) {
692
+ statusMessage(`Error: ${errorMessage(error)}`);
678
693
  }
679
694
  }
680
695
  function renderFilesTemplate() {
681
696
  return _renderFilesTemplate({
682
- openProject,
683
697
  openFileFromTree,
698
+ openProject,
684
699
  renderLeftPanel,
685
700
  });
686
701
  }
@@ -690,19 +705,9 @@ function openFileFromTree(path: string) {
690
705
 
691
706
  // ─── Keyboard shortcuts ───────────────────────────────────────────────────────
692
707
  initShortcuts(() => ({
693
- canvasMode: getCanvasMode(),
694
- panX: view.panX,
695
- panY: view.panY,
696
- setPan: (x, y) => {
697
- view.panX = x;
698
- view.panY = y;
699
- view.needsCenter = false;
700
- },
701
708
  applyTransform,
702
- positionZoomIndicator,
709
+ canvasMode: getCanvasMode(),
703
710
  componentInlineEdit: view.componentInlineEdit,
704
- saveFile,
705
- openProject,
706
711
  enterEditOnPath(path) {
707
712
  requestAnimationFrame(() => {
708
713
  const activePanel = getActivePanel();
@@ -714,16 +719,30 @@ initShortcuts(() => ({
714
719
  }
715
720
  });
716
721
  },
722
+ openProject,
723
+ panX: view.panX,
724
+ panY: view.panY,
725
+ positionZoomIndicator,
726
+ saveFile,
727
+ setPan: (x, y) => {
728
+ view.panX = x;
729
+ view.panY = y;
730
+ view.needsCenter = false;
731
+ },
717
732
  }));
718
733
 
719
734
  // ─── Autosave (registered as update middleware) ──────────────────────────────
720
735
 
721
- const AUTO_SAVE_DELAY: number = 2000;
736
+ const AUTO_SAVE_DELAY = 2000;
722
737
 
723
738
  function scheduleAutosave() {
724
739
  const tab = activeTab.value;
725
- if (!tab?.fileHandle || !tab.doc.dirty) return;
726
- if (view.autosaveTimer) clearTimeout(view.autosaveTimer);
740
+ if (!tab?.fileHandle || !tab.doc.dirty) {
741
+ return;
742
+ }
743
+ if (view.autosaveTimer) {
744
+ clearTimeout(view.autosaveTimer);
745
+ }
727
746
  view.autosaveTimer = setTimeout(async () => {
728
747
  const t = activeTab.value;
729
748
  if (t?.fileHandle && t.doc.dirty && "createWritable" in t.fileHandle) {
@@ -739,5 +758,7 @@ function scheduleAutosave() {
739
758
  }
740
759
 
741
760
  effect(() => {
742
- if (activeTab.value?.doc.dirty) scheduleAutosave();
761
+ if (activeTab.value?.doc.dirty) {
762
+ scheduleAutosave();
763
+ }
743
764
  });