@jxsuite/studio 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +13476 -12330
  3. package/dist/studio.js.map +152 -148
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +19 -7
  8. package/src/browse/browse-modal.js +16 -13
  9. package/src/browse/browse.js +44 -35
  10. package/src/canvas/canvas-diff.js +24 -16
  11. package/src/canvas/canvas-helpers.js +24 -19
  12. package/src/canvas/canvas-live-render.js +113 -39
  13. package/src/canvas/canvas-render.js +119 -73
  14. package/src/canvas/canvas-utils.js +51 -21
  15. package/src/canvas/nested-site-style.js +50 -0
  16. package/src/editor/component-inline-edit.js +14 -13
  17. package/src/editor/content-inline-edit.js +22 -10
  18. package/src/editor/context-menu.js +28 -14
  19. package/src/editor/convert-targets.js +60 -0
  20. package/src/editor/convert-to-component.js +20 -19
  21. package/src/editor/convert-to-repeater.js +226 -0
  22. package/src/editor/inline-edit.js +69 -28
  23. package/src/editor/insertion-helper.js +16 -12
  24. package/src/editor/shortcuts.js +14 -4
  25. package/src/editor/slash-menu.js +40 -19
  26. package/src/files/components.js +1 -1
  27. package/src/files/file-ops.js +37 -16
  28. package/src/files/files.js +322 -68
  29. package/src/github/github-auth.js +122 -0
  30. package/src/github/github-publish.js +126 -0
  31. package/src/markdown/md-convert.js +172 -107
  32. package/src/new-project/new-project-modal.js +204 -0
  33. package/src/panels/activity-bar.js +27 -25
  34. package/src/panels/ai-panel.js +406 -0
  35. package/src/panels/block-action-bar.js +77 -28
  36. package/src/panels/canvas-dnd.js +8 -2
  37. package/src/panels/data-explorer.js +19 -13
  38. package/src/panels/dnd.js +144 -92
  39. package/src/panels/editors.js +55 -35
  40. package/src/panels/elements-panel.js +33 -19
  41. package/src/panels/events-panel.js +15 -10
  42. package/src/panels/git-panel.js +475 -99
  43. package/src/panels/head-panel.js +121 -81
  44. package/src/panels/imports-panel.js +94 -65
  45. package/src/panels/layers-panel.js +64 -43
  46. package/src/panels/left-panel.js +105 -48
  47. package/src/panels/overlays.js +1 -0
  48. package/src/panels/panel-events.js +45 -30
  49. package/src/panels/preview-render.js +23 -18
  50. package/src/panels/properties-panel.js +378 -257
  51. package/src/panels/pseudo-preview.js +2 -2
  52. package/src/panels/quick-search.js +11 -9
  53. package/src/panels/right-panel.js +42 -16
  54. package/src/panels/shared.js +4 -4
  55. package/src/panels/signals-panel.js +415 -166
  56. package/src/panels/statusbar.js +10 -6
  57. package/src/panels/style-inputs.js +67 -35
  58. package/src/panels/style-panel.js +239 -134
  59. package/src/panels/style-utils.js +34 -16
  60. package/src/panels/stylebook-layers-panel.js +12 -10
  61. package/src/panels/stylebook-panel.js +134 -66
  62. package/src/panels/tab-strip.js +5 -2
  63. package/src/panels/toolbar.js +333 -109
  64. package/src/panels/welcome-screen.js +121 -0
  65. package/src/platform.js +2 -4
  66. package/src/platforms/devserver.js +113 -7
  67. package/src/resize-edges.js +98 -0
  68. package/src/services/cem-export.js +12 -7
  69. package/src/services/code-services.js +30 -12
  70. package/src/settings/content-types-editor.js +8 -8
  71. package/src/settings/css-vars-editor.js +30 -24
  72. package/src/settings/defs-editor.js +12 -7
  73. package/src/settings/general-settings.js +85 -3
  74. package/src/settings/head-editor.js +30 -24
  75. package/src/settings/schema-field-ui.js +58 -39
  76. package/src/settings/settings-modal.js +12 -9
  77. package/src/site-context.js +41 -31
  78. package/src/state.js +70 -29
  79. package/src/store.js +36 -35
  80. package/src/studio.js +120 -90
  81. package/src/tabs/tab.js +67 -43
  82. package/src/tabs/transact.js +23 -13
  83. package/src/ui/button-group.js +24 -24
  84. package/src/ui/color-selector.js +28 -23
  85. package/src/ui/field-row.js +3 -3
  86. package/src/ui/icons.js +2 -2
  87. package/src/ui/layers.js +31 -1
  88. package/src/ui/media-picker.js +7 -7
  89. package/src/ui/spectrum.js +5 -1
  90. package/src/ui/unit-selector.js +18 -16
  91. package/src/ui/value-selector.js +21 -15
  92. package/src/ui/widgets.js +22 -19
  93. package/src/utils/canvas-media.js +5 -4
  94. package/src/utils/edit-display.js +32 -26
  95. package/src/utils/google-fonts.js +11 -11
  96. package/src/utils/inherited-style.js +9 -8
  97. package/src/utils/studio-utils.js +18 -18
  98. package/src/view.js +58 -1
  99. package/src/workspace/workspace.js +29 -7
@@ -7,20 +7,28 @@
7
7
 
8
8
  import { html, nothing } from "lit-html";
9
9
  import { classMap } from "lit-html/directives/class-map.js";
10
+ import { ref } from "lit-html/directives/ref.js";
10
11
  import { unified } from "unified";
11
12
  import remarkStringify from "remark-stringify";
12
- import { renderPopover } from "../ui/layers.js";
13
+ import { renderPopover, showDialog, showConfirmDialog } from "../ui/layers.js";
13
14
  import remarkDirective from "remark-directive";
14
15
  import { stringify as stringifyYaml } from "yaml";
15
16
  import { jxToMd } from "../markdown/md-convert.js";
16
- import { createState, projectState, setProjectState } from "../store.js";
17
+ import { createState, projectState, setProjectState, requireProjectState } from "../store.js";
17
18
  import { getPlatform } from "../platform.js";
18
19
  import { statusMessage } from "../panels/statusbar.js";
19
20
  import { loadComponentRegistry } from "./components.js";
21
+ import {
22
+ draggable,
23
+ dropTargetForElements,
24
+ monitorForElements,
25
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
26
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
20
27
  import {
21
28
  workspace,
22
29
  openTab,
23
30
  activateTab,
31
+ renameTab,
24
32
  replaceAllTabs,
25
33
  activeTab,
26
34
  } from "../workspace/workspace.js";
@@ -65,7 +73,7 @@ export async function loadProject() {
65
73
  name: info.isSiteProject ? info.projectConfig?.name || meta.name : meta.name,
66
74
  projectRoot: ".",
67
75
  isSiteProject: info.isSiteProject,
68
- projectConfig: info.isSiteProject ? info.projectConfig : null,
76
+ projectConfig: (info.isSiteProject ? info.projectConfig : null) || null,
69
77
  projectDirs: info.directories || [],
70
78
  dirs: new Map(),
71
79
  expanded: new Set(),
@@ -105,7 +113,7 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
105
113
  replaceAllTabs({ id: "initial", document: { tagName: "div", children: [] } });
106
114
 
107
115
  setProjectState({
108
- ...projectState,
116
+ .../** @type {ProjectState} */ (projectState),
109
117
  projectRoot: handle.root,
110
118
  isSiteProject: true,
111
119
  projectConfig: config,
@@ -129,26 +137,26 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
129
137
  "public",
130
138
  "styles",
131
139
  ];
132
- const entries = projectState.dirs.get(".") || [];
140
+ const entries = requireProjectState().dirs.get(".") || [];
133
141
  const foundDirs = [];
134
142
  for (const e of entries) {
135
143
  if (e.type === "directory" && conventionalDirs.includes(e.name)) {
136
144
  foundDirs.push(e.name);
137
- projectState.expanded.add(e.path || e.name);
145
+ requireProjectState().expanded.add(e.path || e.name);
138
146
  await loadDirectory(e.path || e.name);
139
147
  }
140
148
  }
141
- projectState.projectDirs = foundDirs;
149
+ requireProjectState().projectDirs = foundDirs;
142
150
 
143
151
  view.leftTab = "files";
144
- addRecentProject(projectState.name, projectState.projectRoot);
152
+ addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
145
153
  renderActivityBar();
146
154
  renderLeftPanel();
147
- statusMessage(`Opened project: ${projectState.name}`);
155
+ statusMessage(`Opened project: ${requireProjectState().name}`);
148
156
 
149
157
  await openHomePage();
150
- } catch (/** @type {any} */ e) {
151
- statusMessage(`Error: ${e.message}`);
158
+ } catch (/** @type {unknown} */ e) {
159
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
152
160
  }
153
161
  }
154
162
 
@@ -251,9 +259,9 @@ export function renderFilesTemplate({
251
259
  size="xs"
252
260
  label="Refresh"
253
261
  @click=${async () => {
254
- projectState.dirs.clear();
262
+ requireProjectState().dirs.clear();
255
263
  await loadDirectory(".");
256
- for (const dir of projectState.expanded) await loadDirectory(dir);
264
+ for (const dir of requireProjectState().expanded) await loadDirectory(dir);
257
265
  renderLeftPanel();
258
266
  }}
259
267
  >
@@ -264,9 +272,9 @@ export function renderFilesTemplate({
264
272
  size="s"
265
273
  quiet
266
274
  placeholder="Filter files…"
267
- value=${projectState.searchQuery}
275
+ value=${requireProjectState().searchQuery}
268
276
  @input=${(/** @type {Event} */ e) => {
269
- projectState.searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
277
+ requireProjectState().searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
270
278
  renderLeftPanel();
271
279
  }}
272
280
  @submit=${(/** @type {Event} */ e) => e.preventDefault()}
@@ -284,7 +292,7 @@ function renderTreeLevelTemplate(
284
292
  /** @type {number} */ depth,
285
293
  /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
286
294
  ) {
287
- const entries = projectState.dirs.get(dirPath);
295
+ const entries = requireProjectState().dirs.get(dirPath);
288
296
  if (!entries) {
289
297
  loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
290
298
  return html`<div
@@ -301,15 +309,15 @@ function renderTreeLevelTemplate(
301
309
  return a.name.localeCompare(b.name);
302
310
  });
303
311
 
304
- const query = projectState.searchQuery.toLowerCase();
312
+ const query = requireProjectState().searchQuery.toLowerCase();
305
313
  const filtered = query
306
314
  ? sorted.filter((e) => e.type === "directory" || e.name.toLowerCase().includes(query))
307
315
  : sorted;
308
316
 
309
317
  return filtered.map((entry) => {
310
318
  const isDir = entry.type === "directory";
311
- const isExpanded = projectState.expanded.has(entry.path);
312
- const isSelected = projectState.selectedPath === entry.path;
319
+ const isExpanded = requireProjectState().expanded.has(entry.path);
320
+ const isSelected = requireProjectState().selectedPath === entry.path;
313
321
 
314
322
  return html`
315
323
  <div
@@ -324,10 +332,10 @@ function renderTreeLevelTemplate(
324
332
  @click=${async (/** @type {MouseEvent} */ e) => {
325
333
  e.stopPropagation();
326
334
  if (isDir) {
327
- if (isExpanded) projectState.expanded.delete(entry.path);
335
+ if (isExpanded) requireProjectState().expanded.delete(entry.path);
328
336
  else {
329
- projectState.expanded.add(entry.path);
330
- if (!projectState.dirs.has(entry.path)) await loadDirectory(entry.path);
337
+ requireProjectState().expanded.add(entry.path);
338
+ if (!requireProjectState().dirs.has(entry.path)) await loadDirectory(entry.path);
331
339
  }
332
340
  ctx.renderLeftPanel();
333
341
  } else {
@@ -372,8 +380,8 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
372
380
  case "ArrowRight":
373
381
  if (focused.dataset.type === "directory") {
374
382
  const path = /** @type {string} */ (focused.dataset.path);
375
- if (!projectState.expanded.has(path)) {
376
- projectState.expanded.add(path);
383
+ if (!requireProjectState().expanded.has(path)) {
384
+ requireProjectState().expanded.add(path);
377
385
  loadDirectory(path).then(() => {
378
386
  const panel = tree.closest(".panel-body");
379
387
  if (panel)
@@ -386,9 +394,9 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
386
394
  break;
387
395
  case "ArrowLeft":
388
396
  if (focused.dataset.type === "directory") {
389
- const path = focused.dataset.path;
390
- if (projectState.expanded.has(path)) {
391
- projectState.expanded.delete(path);
397
+ const path = /** @type {string} */ (focused.dataset.path);
398
+ if (requireProjectState().expanded.has(path)) {
399
+ requireProjectState().expanded.delete(path);
392
400
  // renderLeftPanel will be called by the caller who sets up keyboard
393
401
  }
394
402
  }
@@ -407,6 +415,170 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
407
415
  if (first) first.setAttribute("tabindex", "0");
408
416
  }
409
417
 
418
+ /** @type {(() => void)[]} */
419
+ let _fileTreeDndCleanups = [];
420
+
421
+ /**
422
+ * Register drag-and-drop on file tree items. Called after each file tree render.
423
+ *
424
+ * @param {{ renderLeftPanel: () => void }} ctx
425
+ */
426
+ export function registerFileTreeDnD({ renderLeftPanel }) {
427
+ // Clean up previous registrations
428
+ for (const fn of _fileTreeDndCleanups) fn();
429
+ _fileTreeDndCleanups = [];
430
+
431
+ requestAnimationFrame(() => {
432
+ const tree = /** @type {HTMLElement | null} */ (document.querySelector(".file-tree"));
433
+ if (!tree) return;
434
+
435
+ const items = /** @type {NodeListOf<HTMLElement>} */ (tree.querySelectorAll(".file-tree-item"));
436
+
437
+ items.forEach((row) => {
438
+ const path = row.dataset.path;
439
+ const type = row.dataset.type;
440
+ if (!path) return;
441
+
442
+ const cleanups = [
443
+ draggable({
444
+ element: row,
445
+ getInitialData() {
446
+ return { type: "file-tree", path, entryType: type };
447
+ },
448
+ onDragStart() {
449
+ row.classList.add("dragging");
450
+ },
451
+ onDrop() {
452
+ row.classList.remove("dragging");
453
+ },
454
+ }),
455
+ ];
456
+
457
+ if (type === "directory") {
458
+ cleanups.push(
459
+ dropTargetForElements({
460
+ element: row,
461
+ canDrop({ source }) {
462
+ if (source.data.type !== "file-tree") return false;
463
+ const srcPath = /** @type {string} */ (source.data.path);
464
+ if (srcPath === path) return false;
465
+ if (srcPath.startsWith(path + "/")) return false;
466
+ const srcParent = parentDir(srcPath);
467
+ if (srcParent === path) return false;
468
+ return true;
469
+ },
470
+ onDragEnter() {
471
+ row.classList.add("drag-over");
472
+ },
473
+ onDrag() {
474
+ if (!row.classList.contains("drag-over")) row.classList.add("drag-over");
475
+ },
476
+ onDragLeave() {
477
+ row.classList.remove("drag-over");
478
+ },
479
+ onDrop() {
480
+ row.classList.remove("drag-over");
481
+ },
482
+ getData() {
483
+ return { type: "file-tree-target", targetDir: path };
484
+ },
485
+ }),
486
+ );
487
+ }
488
+
489
+ _fileTreeDndCleanups.push(combine(...cleanups));
490
+ });
491
+
492
+ // Root-level drop target (move to project root)
493
+ const rootCleanup = dropTargetForElements({
494
+ element: tree,
495
+ canDrop({ source }) {
496
+ if (source.data.type !== "file-tree") return false;
497
+ const srcPath = /** @type {string} */ (source.data.path);
498
+ return parentDir(srcPath) !== ".";
499
+ },
500
+ onDragEnter() {
501
+ tree.classList.add("drag-over-root");
502
+ },
503
+ onDragLeave() {
504
+ tree.classList.remove("drag-over-root");
505
+ },
506
+ onDrop() {
507
+ tree.classList.remove("drag-over-root");
508
+ },
509
+ getData() {
510
+ return { type: "file-tree-target", targetDir: "." };
511
+ },
512
+ });
513
+ _fileTreeDndCleanups.push(rootCleanup);
514
+
515
+ // Monitor for drop events
516
+ const monitorCleanup = monitorForElements({
517
+ onDrop({ source, location }) {
518
+ const target = location.current.dropTargets[0];
519
+ if (!target) return;
520
+ if (source.data.type !== "file-tree") return;
521
+ if (target.data.type !== "file-tree-target") return;
522
+
523
+ const srcPath = /** @type {string} */ (source.data.path);
524
+ const targetDirPath = /** @type {string} */ (target.data.targetDir);
525
+ const fileName = srcPath.split("/").pop();
526
+ const newPath = targetDirPath === "." ? fileName : `${targetDirPath}/${fileName}`;
527
+
528
+ if (newPath === srcPath) return;
529
+
530
+ moveFileEntry(srcPath, /** @type {string} */ (newPath), renderLeftPanel);
531
+ },
532
+ });
533
+ _fileTreeDndCleanups.push(monitorCleanup);
534
+ });
535
+ }
536
+
537
+ /**
538
+ * Move a file/directory and update all affected state.
539
+ *
540
+ * @param {string} oldPath
541
+ * @param {string} newPath
542
+ * @param {() => void} renderLeftPanel
543
+ */
544
+ async function moveFileEntry(oldPath, newPath, renderLeftPanel) {
545
+ const platform = getPlatform();
546
+ try {
547
+ await platform.renameFile(oldPath, newPath);
548
+
549
+ // Update open tabs referencing the moved path
550
+ for (const [id] of workspace.tabs.entries()) {
551
+ if (id === oldPath) {
552
+ renameTab(oldPath, newPath, newPath);
553
+ } else if (id.startsWith(oldPath + "/")) {
554
+ const newTabPath = newPath + id.slice(oldPath.length);
555
+ renameTab(id, newTabPath, newTabPath);
556
+ }
557
+ }
558
+
559
+ // Refresh affected directories
560
+ const oldParent = parentDir(oldPath);
561
+ const newParent = parentDir(newPath);
562
+ await loadDirectory(oldParent);
563
+ if (newParent !== oldParent) await loadDirectory(newParent);
564
+
565
+ // Auto-expand target directory
566
+ if (newParent !== ".") requireProjectState().expanded.add(newParent);
567
+
568
+ renderLeftPanel();
569
+ statusMessage(`Moved to ${newPath}`);
570
+ } catch (/** @type {unknown} */ e) {
571
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
572
+ }
573
+ }
574
+
575
+ /** @param {string} path @returns {string} */
576
+ function parentDir(path) {
577
+ const normalized = path.replaceAll("\\", "/");
578
+ const lastSlash = normalized.lastIndexOf("/");
579
+ return lastSlash === -1 ? "." : normalized.substring(0, lastSlash);
580
+ }
581
+
410
582
  // ─── Context menu ─────────────────────────────────────────────────────────────
411
583
 
412
584
  /** @type {ReturnType<typeof renderPopover> | null} */
@@ -452,7 +624,22 @@ function showFileContextMenu(
452
624
  y = e.clientY;
453
625
 
454
626
  _fileCtxHandle = renderPopover(
455
- html`<sp-popover open style="position:fixed;z-index:10000;left:${x}px;top:${y}px">
627
+ html`<sp-popover
628
+ open
629
+ style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
630
+ ${ref((el) => {
631
+ if (!el) return;
632
+ requestAnimationFrame(() => {
633
+ const popover = /** @type {HTMLElement} */ (el);
634
+ const menuRect = popover.getBoundingClientRect();
635
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
636
+ if (y + menuRect.height > window.innerHeight)
637
+ y = window.innerHeight - menuRect.height - 4;
638
+ popover.style.left = `${x}px`;
639
+ popover.style.top = `${y}px`;
640
+ });
641
+ })}
642
+ >
456
643
  <sp-menu>
457
644
  ${items.map((item) =>
458
645
  item.label === "\u2014"
@@ -475,18 +662,6 @@ function showFileContextMenu(
475
662
  },
476
663
  },
477
664
  );
478
-
479
- requestAnimationFrame(() => {
480
- const popover = /** @type {HTMLElement | null} */ (
481
- _fileCtxHandle?.host.querySelector("sp-popover")
482
- );
483
- if (!popover) return;
484
- const menuRect = popover.getBoundingClientRect();
485
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
486
- if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
487
- popover.style.left = `${x}px`;
488
- popover.style.top = `${y}px`;
489
- });
490
665
  }
491
666
 
492
667
  // ─── File CRUD ────────────────────────────────────────────────────────────────
@@ -497,23 +672,69 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
497
672
  const path = dirPath === "." ? name : `${dirPath}/${name}`;
498
673
  const content = name.endsWith(".md")
499
674
  ? "---\ntitle: Untitled\n---\n\n"
500
- : JSON.stringify({ tagName: "div", children: [] }, null, 2);
675
+ : JSON.stringify({ tagName: "div", children: [{ tagName: "p", children: [] }] }, null, 2);
501
676
  try {
502
677
  const platform = getPlatform();
503
678
  await platform.writeFile(path, content);
504
679
  await loadDirectory(dirPath);
505
680
  renderLeftPanel();
506
681
  statusMessage(`Created ${path}`);
507
- } catch (/** @type {any} */ e) {
508
- statusMessage(`Error: ${e.message}`);
682
+ } catch (/** @type {unknown} */ e) {
683
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
509
684
  }
510
685
  }
511
686
 
687
+ /** @param {string} currentName @returns {Promise<string | null>} */
688
+ function showRenameFileDialog(currentName) {
689
+ let value = currentName;
690
+ return showDialog((done) => {
691
+ function confirm() {
692
+ const trimmed = value.trim();
693
+ if (!trimmed) return;
694
+ done(trimmed);
695
+ }
696
+ const tpl = html`
697
+ <sp-dialog-wrapper
698
+ open
699
+ underlay
700
+ headline="Rename"
701
+ confirm-label="Rename"
702
+ cancel-label="Cancel"
703
+ size="s"
704
+ @confirm=${confirm}
705
+ @cancel=${() => done(null)}
706
+ @close=${() => done(null)}
707
+ >
708
+ <sp-textfield
709
+ style="width:100%"
710
+ value=${currentName}
711
+ @input=${(/** @type {Event} */ e) => {
712
+ value = /** @type {HTMLInputElement} */ (e.target).value || "";
713
+ }}
714
+ @keydown=${(/** @type {KeyboardEvent} */ e) => {
715
+ if (e.key === "Enter") confirm();
716
+ }}
717
+ ></sp-textfield>
718
+ </sp-dialog-wrapper>
719
+ `;
720
+ requestAnimationFrame(() => {
721
+ const layer = document.getElementById("layer-dialog");
722
+ const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
723
+ if (tf) {
724
+ tf.focus();
725
+ const input = tf.shadowRoot?.querySelector("input");
726
+ if (input) input.select();
727
+ }
728
+ });
729
+ return tpl;
730
+ });
731
+ }
732
+
512
733
  async function renameFile(
513
734
  /** @type {{ name: string; path: string; type: string }} */ entry,
514
735
  /** @type {() => void} */ renderLeftPanel,
515
736
  ) {
516
- const newName = prompt("New name:", entry.name);
737
+ const newName = await showRenameFileDialog(entry.name);
517
738
  if (!newName || newName === entry.name) return;
518
739
  const entryPath = entry.path.replaceAll("\\", "/");
519
740
  const parentDir = entryPath.includes("/")
@@ -524,13 +745,16 @@ async function renameFile(
524
745
  const platform = getPlatform();
525
746
  await platform.renameFile(entry.path, newPath);
526
747
  await loadDirectory(parentDir);
527
- if (projectState.selectedPath === entry.path) {
528
- projectState.selectedPath = newPath;
748
+ if (requireProjectState().selectedPath === entry.path) {
749
+ requireProjectState().selectedPath = newPath;
750
+ }
751
+ if (workspace.tabs.has(entry.path)) {
752
+ renameTab(entry.path, newPath, newPath);
529
753
  }
530
754
  renderLeftPanel();
531
755
  statusMessage(`Renamed to ${newName}`);
532
- } catch (/** @type {any} */ e) {
533
- statusMessage(`Error: ${e.message}`);
756
+ } catch (/** @type {unknown} */ e) {
757
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
534
758
  }
535
759
  }
536
760
 
@@ -538,20 +762,24 @@ async function deleteFile(
538
762
  /** @type {{ name: string; path: string; type: string }} */ entry,
539
763
  /** @type {() => void} */ renderLeftPanel,
540
764
  ) {
541
- if (!confirm(`Delete "${entry.name}"?`)) return;
765
+ const confirmed = await showConfirmDialog("Delete File", `Delete "${entry.name}"?`, {
766
+ confirmLabel: "Delete",
767
+ destructive: true,
768
+ });
769
+ if (!confirmed) return;
542
770
  try {
543
771
  const platform = getPlatform();
544
772
  await platform.deleteFile(entry.path);
545
773
  const delPath = entry.path.replaceAll("\\", "/");
546
774
  const parentDir = delPath.includes("/") ? delPath.substring(0, delPath.lastIndexOf("/")) : ".";
547
775
  await loadDirectory(parentDir);
548
- if (projectState.selectedPath === entry.path) {
549
- projectState.selectedPath = null;
776
+ if (requireProjectState().selectedPath === entry.path) {
777
+ requireProjectState().selectedPath = null;
550
778
  }
551
779
  renderLeftPanel();
552
780
  statusMessage(`Deleted ${entry.name}`);
553
- } catch (/** @type {any} */ e) {
554
- statusMessage(`Error: ${e.message}`);
781
+ } catch (/** @type {unknown} */ e) {
782
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
555
783
  }
556
784
  }
557
785
 
@@ -561,10 +789,10 @@ async function deleteFile(
561
789
  * Open a file from the file tree — auto-saves current dirty doc, then loads the new one.
562
790
  *
563
791
  * @param {{
564
- * S: any;
565
- * commit: (s: any) => void;
792
+ * S: import("../state.js").StudioState;
793
+ * commit: (s: import("../state.js").StudioState) => void;
566
794
  * render: () => void;
567
- * loadMarkdown: (source: string, handle: any) => void;
795
+ * loadMarkdown: (source: string, handle: unknown) => void;
568
796
  * }} ctx
569
797
  * @param {string} path
570
798
  */
@@ -576,11 +804,11 @@ export async function openFileFromTree(ctx, path) {
576
804
  const isContent = ctx.S.mode === "content";
577
805
  let output;
578
806
  if (isContent) {
579
- const mdast = jxToMd(ctx.S.document);
807
+ const mdast = jxToMd(/** @type {JxElement} */ (ctx.S.document));
580
808
  const md = unified()
581
809
  .use(remarkDirective)
582
810
  .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
583
- .stringify(mdast);
811
+ .stringify(/** @type {import("mdast").Root} */ (/** @type {unknown} */ (mdast)));
584
812
  const fm = ctx.S.content?.frontmatter;
585
813
  const hasFrontmatter = fm && Object.keys(fm).length > 0;
586
814
  output = hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
@@ -588,8 +816,8 @@ export async function openFileFromTree(ctx, path) {
588
816
  output = JSON.stringify(ctx.S.document, null, 2);
589
817
  }
590
818
  await platform.writeFile(ctx.S.documentPath, output);
591
- } catch (/** @type {any} */ e) {
592
- statusMessage(`Save error: ${e.message}`);
819
+ } catch (/** @type {unknown} */ e) {
820
+ statusMessage(`Save error: ${/** @type {Error} */ (e).message}`);
593
821
  }
594
822
  }
595
823
 
@@ -612,12 +840,12 @@ export async function openFileFromTree(ctx, path) {
612
840
  }
613
841
 
614
842
  // Update tree selection
615
- projectState.selectedPath = path;
843
+ requireProjectState().selectedPath = path;
616
844
 
617
845
  ctx.render();
618
846
  statusMessage(`Opened ${path}`);
619
- } catch (/** @type {any} */ e) {
620
- statusMessage(`Error: ${e.message}`);
847
+ } catch (/** @type {unknown} */ e) {
848
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
621
849
  }
622
850
  }
623
851
 
@@ -630,7 +858,7 @@ export async function openFileInTab(path) {
630
858
  for (const [id, tab] of workspace.tabs.entries()) {
631
859
  if (tab.documentPath === path) {
632
860
  activateTab(id);
633
- projectState.selectedPath = path;
861
+ requireProjectState().selectedPath = path;
634
862
  return;
635
863
  }
636
864
  }
@@ -657,9 +885,8 @@ export async function openFileInTab(path) {
657
885
  frontmatter,
658
886
  sourceFormat: path.endsWith(".md") ? "md" : null,
659
887
  });
660
- projectState.selectedPath = path;
888
+ requireProjectState().selectedPath = path;
661
889
  trackRecentFile({ path, name: path.split("/").pop() || path });
662
- view.leftTab = "layers";
663
890
 
664
891
  if (path === "project.json") {
665
892
  const tab = activeTab.value;
@@ -667,7 +894,34 @@ export async function openFileInTab(path) {
667
894
  }
668
895
 
669
896
  statusMessage(`Opened ${path.split("/").pop()}`);
670
- } catch (/** @type {any} */ e) {
671
- statusMessage(`Error: ${e.message}`);
897
+ } catch (/** @type {unknown} */ e) {
898
+ statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Reload an already-open tab from disk without changing the active tab. Used to refresh after AI
904
+ * assistant writes to a file.
905
+ *
906
+ * @param {string} path
907
+ */
908
+ export async function reloadFileInTab(path) {
909
+ for (const [, tab] of workspace.tabs.entries()) {
910
+ if (tab.documentPath === path) {
911
+ const platform = getPlatform();
912
+ try {
913
+ const content = await platform.readFile(path);
914
+ if (!content) return;
915
+ if (path.endsWith(".md")) {
916
+ const { document, frontmatter } = await loadMarkdown(content);
917
+ tab.doc.document = document;
918
+ tab.doc.content.frontmatter = frontmatter;
919
+ } else {
920
+ tab.doc.document = JSON.parse(content);
921
+ }
922
+ tab.doc.dirty = false;
923
+ } catch {}
924
+ return;
925
+ }
672
926
  }
673
927
  }