@jxsuite/studio 0.10.2 → 0.13.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 (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
@@ -5,45 +5,48 @@
5
5
  * shortcuts on the canvas / document.
6
6
  */
7
7
 
8
+ import { getNodeAtPath, parentElementPath, childIndex, canvasWrap } from "../store.js";
9
+ import { activeTab, workspace, closeTab } from "../workspace/workspace.js";
8
10
  import {
9
- selectNode,
10
- undo,
11
- redo,
12
- removeNode,
13
- insertNode,
14
- duplicateNode,
15
- getNodeAtPath,
16
- parentElementPath,
17
- childIndex,
18
- canvasWrap,
19
- update,
20
- } from "../store.js";
11
+ transactDoc,
12
+ mutateInsertNode,
13
+ mutateRemoveNode,
14
+ mutateDuplicateNode,
15
+ undo as tabUndo,
16
+ redo as tabRedo,
17
+ } from "../tabs/transact.js";
21
18
  import { isEditing } from "./inline-edit.js";
22
19
  import { copyNode, cutNode, pasteNode } from "./context-menu.js";
23
20
 
21
+ /**
22
+ * @typedef {import("../state.js").StudioState} StudioState
23
+ *
24
+ * @typedef {import("../state.js").JxPath} JxPath
25
+ */
26
+
24
27
  /**
25
28
  * Initialise all keyboard (and wheel/pointer) shortcuts.
26
29
  *
27
30
  * @param {() => {
28
- * S: any;
29
- * setS: (s: any) => void;
31
+ * S: StudioState;
32
+ * setS: (s: StudioState) => void;
30
33
  * canvasMode: string;
31
34
  * panX: number;
32
35
  * panY: number;
33
36
  * setPan: (x: number, y: number) => void;
34
37
  * applyTransform: () => void;
35
38
  * positionZoomIndicator: () => void;
36
- * componentInlineEdit: any;
39
+ * componentInlineEdit: object | null;
37
40
  * saveFile: () => void;
38
41
  * openProject: () => void;
39
- * enterEditOnPath: (path: any) => void;
42
+ * enterEditOnPath: (path: JxPath) => void;
40
43
  * }} getContext
41
44
  */
42
45
  export function initShortcuts(getContext) {
43
46
  // Wheel handler: Ctrl+Scroll = zoom (cursor-centered), plain scroll = pan
44
47
  canvasWrap.addEventListener(
45
48
  "wheel",
46
- (/** @type {any} */ e) => {
49
+ (/** @type {WheelEvent} */ e) => {
47
50
  const { S, setS, canvasMode, panX, panY, setPan, applyTransform } = getContext();
48
51
  // Edit (content) mode: let the scroll container handle scrolling natively
49
52
  if (canvasMode === "edit") return;
@@ -73,7 +76,7 @@ export function initShortcuts(getContext) {
73
76
  );
74
77
 
75
78
  // Middle-mouse drag panning
76
- canvasWrap.addEventListener("pointerdown", (/** @type {any} */ e) => {
79
+ canvasWrap.addEventListener("pointerdown", (/** @type {PointerEvent} */ e) => {
77
80
  const ctx = getContext();
78
81
  if (ctx.canvasMode === "edit") return; // no panning in edit mode
79
82
  if (e.button !== 1) return; // middle button only
@@ -81,7 +84,7 @@ export function initShortcuts(getContext) {
81
84
  canvasWrap.setPointerCapture(e.pointerId);
82
85
  let lastX = e.clientX,
83
86
  lastY = e.clientY;
84
- const onMove = (/** @type {any} */ ev) => {
87
+ const onMove = (/** @type {PointerEvent} */ ev) => {
85
88
  const { panX, panY, setPan, applyTransform } = getContext();
86
89
  setPan(panX + (ev.clientX - lastX), panY + (ev.clientY - lastY));
87
90
  lastX = ev.clientX;
@@ -125,14 +128,19 @@ export function initShortcuts(getContext) {
125
128
  e.preventDefault();
126
129
  saveFile();
127
130
  }
131
+ if (mod && e.key === "w") {
132
+ e.preventDefault();
133
+ }
128
134
  return;
129
135
  }
130
136
  if (isEditing()) {
131
- // Let inline editor handle its own keyboard events; only intercept Save
132
137
  if (mod && e.key === "s") {
133
138
  e.preventDefault();
134
139
  saveFile();
135
140
  }
141
+ if (mod && e.key === "w") {
142
+ e.preventDefault();
143
+ }
136
144
  return;
137
145
  }
138
146
  if (componentInlineEdit) {
@@ -140,11 +148,25 @@ export function initShortcuts(getContext) {
140
148
  e.preventDefault();
141
149
  saveFile();
142
150
  }
151
+ if (mod && e.key === "w") {
152
+ e.preventDefault();
153
+ }
143
154
  return;
144
155
  }
145
156
 
146
157
  if (mod) {
147
158
  switch (e.key) {
159
+ case "w":
160
+ e.preventDefault();
161
+ if (workspace.activeTabId && workspace.tabOrder.length > 1) {
162
+ const tab = workspace.tabs.get(workspace.activeTabId);
163
+ if (tab?.doc.dirty) {
164
+ const name = tab.documentPath?.split("/").pop() || "Untitled";
165
+ if (!window.confirm(`"${name}" has unsaved changes. Close without saving?`)) break;
166
+ }
167
+ closeTab(workspace.activeTabId);
168
+ }
169
+ break;
148
170
  case "o":
149
171
  e.preventDefault();
150
172
  openProject();
@@ -155,11 +177,15 @@ export function initShortcuts(getContext) {
155
177
  break;
156
178
  case "z":
157
179
  e.preventDefault();
158
- update(e.shiftKey ? redo(S) : undo(S));
180
+ if (e.shiftKey) tabRedo(activeTab.value);
181
+ else tabUndo(activeTab.value);
159
182
  break;
160
183
  case "d":
161
184
  e.preventDefault();
162
- if (S.selection) update(duplicateNode(S, S.selection));
185
+ if (S.selection) {
186
+ const sel = S.selection;
187
+ transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, sel));
188
+ }
163
189
  break;
164
190
  case "c":
165
191
  e.preventDefault();
@@ -202,21 +228,23 @@ export function initShortcuts(getContext) {
202
228
  case "Backspace":
203
229
  if (S.selection && S.selection.length >= 2) {
204
230
  e.preventDefault();
205
- update(removeNode(S, S.selection));
231
+ const sel = S.selection;
232
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, sel));
206
233
  }
207
234
  break;
208
235
  case "Escape":
209
- update(selectNode(S, null));
236
+ activeTab.value.session.selection = null;
210
237
  break;
211
238
  case "Enter":
212
239
  if (S.selection && S.selection.length >= 2) {
213
240
  e.preventDefault();
214
- const pp = /** @type {any} */ (parentElementPath(S.selection));
241
+ const pp = /** @type {JxPath} */ (parentElementPath(S.selection));
215
242
  const idx = /** @type {number} */ (childIndex(S.selection));
216
- let s = insertNode(S, pp, idx + 1, { tagName: "p", textContent: "" });
217
243
  const newPath = [...pp, "children", idx + 1];
218
- s = selectNode(s, newPath);
219
- update(s);
244
+ transactDoc(activeTab.value, (t) => {
245
+ mutateInsertNode(t, pp, idx + 1, { tagName: "p", textContent: "" });
246
+ t.session.selection = newPath;
247
+ });
220
248
  enterEditOnPath(newPath);
221
249
  }
222
250
  break;
@@ -231,7 +259,7 @@ export function initShortcuts(getContext) {
231
259
  case "ArrowLeft":
232
260
  e.preventDefault();
233
261
  if (S.selection && S.selection.length >= 2) {
234
- update(selectNode(S, parentElementPath(S.selection)));
262
+ activeTab.value.session.selection = parentElementPath(S.selection);
235
263
  }
236
264
  break;
237
265
  case "ArrowRight":
@@ -239,7 +267,7 @@ export function initShortcuts(getContext) {
239
267
  if (S.selection) {
240
268
  const node = getNodeAtPath(S.document, S.selection);
241
269
  if (node?.children?.length > 0) {
242
- update(selectNode(S, [...S.selection, "children", 0]));
270
+ activeTab.value.session.selection = [...S.selection, "children", 0];
243
271
  }
244
272
  }
245
273
  break;
@@ -249,8 +277,8 @@ export function initShortcuts(getContext) {
249
277
  // Block ctrl+scroll (browser zoom) on all non-canvas areas
250
278
  document.addEventListener(
251
279
  "wheel",
252
- (/** @type {any} */ e) => {
253
- if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(e.target)) {
280
+ (/** @type {WheelEvent} */ e) => {
281
+ if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(/** @type {Node} */ (e.target))) {
254
282
  e.preventDefault();
255
283
  }
256
284
  },
@@ -259,22 +287,24 @@ export function initShortcuts(getContext) {
259
287
  }
260
288
 
261
289
  /**
262
- * @param {any} S
290
+ * @param {StudioState} S
263
291
  * @param {number} [direction]
264
292
  */
265
293
  function navigateSelection(S, direction = -1) {
266
294
  if (!S.selection) {
267
- update(selectNode(S, []));
295
+ activeTab.value.session.selection = [];
268
296
  return;
269
297
  }
270
- if (S.selection.length < 2) return; // can't navigate from root
298
+ if (S.selection.length < 2) return;
271
299
 
272
- const parent = getNodeAtPath(S.document, /** @type {any} */ (parentElementPath(S.selection)));
300
+ const parent = getNodeAtPath(S.document, /** @type {JxPath} */ (parentElementPath(S.selection)));
273
301
  const idx = /** @type {number} */ (childIndex(S.selection));
274
302
  const newIdx = idx + direction;
275
303
  if (parent?.children && newIdx >= 0 && newIdx < parent.children.length) {
276
- update(
277
- selectNode(S, [.../** @type {any[]} */ (parentElementPath(S.selection)), "children", newIdx]),
278
- );
304
+ activeTab.value.session.selection = [
305
+ .../** @type {JxPath} */ (parentElementPath(S.selection)),
306
+ "children",
307
+ newIdx,
308
+ ];
279
309
  }
280
310
  }
@@ -3,8 +3,19 @@
3
3
  import { getPlatform } from "../platform.js";
4
4
  import { projectState } from "../store.js";
5
5
 
6
- /** @type {any[]} */
7
- export let componentRegistry = []; // cached list from /__studio/components
6
+ /**
7
+ * @typedef {{
8
+ * tagName: string;
9
+ * path: string;
10
+ * props?: { name: string; type?: string; [k: string]: any }[];
11
+ * source?: string;
12
+ * package?: string;
13
+ * modulePath?: string;
14
+ * }} ComponentEntry
15
+ */
16
+
17
+ /** @type {ComponentEntry[]} */
18
+ export let componentRegistry = [];
8
19
  export let _componentRegistryLoaded = false;
9
20
 
10
21
  export async function loadComponentRegistry() {
@@ -18,8 +29,8 @@ export async function loadComponentRegistry() {
18
29
  }
19
30
 
20
31
  /**
21
- * @param {any} fromDocPath
22
- * @param {any} toCompPath
32
+ * @param {string | null} fromDocPath
33
+ * @param {string} toCompPath
23
34
  */
24
35
  export function computeRelativePath(fromDocPath, toCompPath) {
25
36
  if (!fromDocPath) return `./${toCompPath}`;
@@ -15,10 +15,12 @@ import { createState, projectState, setProjectState } from "../store.js";
15
15
  import { getPlatform } from "../platform.js";
16
16
  import { statusMessage } from "../panels/statusbar.js";
17
17
  import { loadComponentRegistry } from "./components.js";
18
+ import { workspace, openTab, activateTab } from "../workspace/workspace.js";
19
+ import { loadMarkdown } from "./file-ops.js";
18
20
 
19
21
  // ─── File icon map ────────────────────────────────────────────────────────────
20
22
 
21
- const fileIconMap = /** @type {Record<string, any>} */ ({
23
+ const fileIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
22
24
  "sp-icon-folder-open": html`<sp-icon-folder-open></sp-icon-folder-open>`,
23
25
  "sp-icon-folder": html`<sp-icon-folder></sp-icon-folder>`,
24
26
  "sp-icon-file-code": html`<sp-icon-file-code></sp-icon-file-code>`,
@@ -29,7 +31,7 @@ const fileIconMap = /** @type {Record<string, any>} */ ({
29
31
 
30
32
  // ─── File management ──────────────────────────────────────────────────────────
31
33
 
32
- async function loadDirectory(/** @type {any} */ dirPath) {
34
+ async function loadDirectory(/** @type {string} */ dirPath) {
33
35
  if (!projectState) return;
34
36
  try {
35
37
  const platform = getPlatform();
@@ -138,7 +140,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
138
140
 
139
141
  // ─── File tree templates ──────────────────────────────────────────────────────
140
142
 
141
- function fileTypeIconTpl(/** @type {any} */ name, /** @type {any} */ type) {
143
+ function fileTypeIconTpl(/** @type {string} */ name, /** @type {string} */ type) {
142
144
  let tag;
143
145
  if (type === "directory") {
144
146
  tag = projectState?.expanded?.has(name) ? "sp-icon-folder-open" : "sp-icon-folder";
@@ -237,11 +239,11 @@ export function renderFilesTemplate({
237
239
  quiet
238
240
  placeholder="Filter files…"
239
241
  value=${projectState.searchQuery}
240
- @input=${(/** @type {any} */ e) => {
241
- projectState.searchQuery = e.target.value;
242
+ @input=${(/** @type {Event} */ e) => {
243
+ projectState.searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
242
244
  renderLeftPanel();
243
245
  }}
244
- @submit=${(/** @type {any} */ e) => e.preventDefault()}
246
+ @submit=${(/** @type {Event} */ e) => e.preventDefault()}
245
247
  ></sp-search>
246
248
  </div>
247
249
  <div class="file-tree" role="tree" aria-label="Project files">
@@ -250,10 +252,10 @@ export function renderFilesTemplate({
250
252
  `;
251
253
  }
252
254
 
253
- /** @returns {any} */
255
+ /** @returns {import("lit-html").TemplateResult | import("lit-html").TemplateResult[]} */
254
256
  function renderTreeLevelTemplate(
255
- /** @type {any} */ dirPath,
256
- /** @type {any} */ depth,
257
+ /** @type {string} */ dirPath,
258
+ /** @type {number} */ depth,
257
259
  /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
258
260
  ) {
259
261
  const entries = projectState.dirs.get(dirPath);
@@ -293,7 +295,7 @@ function renderTreeLevelTemplate(
293
295
  data-path=${entry.path}
294
296
  data-type=${entry.type}
295
297
  aria-expanded=${isDir ? String(isExpanded) : nothing}
296
- @click=${async (/** @type {any} */ e) => {
298
+ @click=${async (/** @type {MouseEvent} */ e) => {
297
299
  e.stopPropagation();
298
300
  if (isDir) {
299
301
  if (isExpanded) projectState.expanded.delete(entry.path);
@@ -306,7 +308,7 @@ function renderTreeLevelTemplate(
306
308
  ctx.openFileFn(entry.path);
307
309
  }
308
310
  }}
309
- @contextmenu=${(/** @type {any} */ e) => {
311
+ @contextmenu=${(/** @type {MouseEvent} */ e) => {
310
312
  e.preventDefault();
311
313
  e.stopPropagation();
312
314
  showFileContextMenu(e, entry, ctx);
@@ -325,10 +327,10 @@ function renderTreeLevelTemplate(
325
327
  });
326
328
  }
327
329
 
328
- export function setupTreeKeyboard(/** @type {any} */ tree) {
329
- tree.addEventListener("keydown", (/** @type {any} */ e) => {
330
- const items = [...tree.querySelectorAll(".file-tree-item")];
331
- const focused = tree.querySelector(".file-tree-item:focus");
330
+ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
331
+ tree.addEventListener("keydown", (/** @type {KeyboardEvent} */ e) => {
332
+ const items = /** @type {HTMLElement[]} */ ([...tree.querySelectorAll(".file-tree-item")]);
333
+ const focused = /** @type {HTMLElement | null} */ (tree.querySelector(".file-tree-item:focus"));
332
334
  if (!focused || items.length === 0) return;
333
335
 
334
336
  const idx = items.indexOf(focused);
@@ -343,12 +345,15 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
343
345
  break;
344
346
  case "ArrowRight":
345
347
  if (focused.dataset.type === "directory") {
346
- const path = focused.dataset.path;
348
+ const path = /** @type {string} */ (focused.dataset.path);
347
349
  if (!projectState.expanded.has(path)) {
348
350
  projectState.expanded.add(path);
349
351
  loadDirectory(path).then(() => {
350
352
  const panel = tree.closest(".panel-body");
351
- if (panel) panel.querySelector(".file-tree-item:focus")?.click();
353
+ if (panel)
354
+ /** @type {HTMLElement | null} */ (
355
+ panel.querySelector(".file-tree-item:focus")
356
+ )?.click();
352
357
  });
353
358
  }
354
359
  }
@@ -378,12 +383,12 @@ export function setupTreeKeyboard(/** @type {any} */ tree) {
378
383
 
379
384
  // ─── Context menu ─────────────────────────────────────────────────────────────
380
385
 
381
- /** @type {any} */
386
+ /** @type {HTMLElement | null} */
382
387
  let fileContextPopover = null;
383
388
 
384
389
  function showFileContextMenu(
385
- /** @type {any} */ e,
386
- /** @type {any} */ entry,
390
+ /** @type {MouseEvent} */ e,
391
+ /** @type {{ name: string; path: string; type: string }} */ entry,
387
392
  /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
388
393
  ) {
389
394
  if (fileContextPopover) {
@@ -442,8 +447,8 @@ function showFileContextMenu(
442
447
  litRender(tpl, fileContextPopover);
443
448
  document.body.appendChild(fileContextPopover);
444
449
 
445
- const closeHandler = (/** @type {any} */ ev) => {
446
- if (!fileContextPopover?.contains(ev.target)) {
450
+ const closeHandler = (/** @type {MouseEvent} */ ev) => {
451
+ if (!fileContextPopover?.contains(/** @type {Node} */ (ev.target))) {
447
452
  closeFileContextMenu();
448
453
  document.removeEventListener("mousedown", closeHandler, true);
449
454
  }
@@ -478,7 +483,10 @@ async function createNewFile(dirPath = ".", /** @type {() => void} */ renderLeft
478
483
  }
479
484
  }
480
485
 
481
- async function renameFile(/** @type {any} */ entry, /** @type {() => void} */ renderLeftPanel) {
486
+ async function renameFile(
487
+ /** @type {{ name: string; path: string; type: string }} */ entry,
488
+ /** @type {() => void} */ renderLeftPanel,
489
+ ) {
482
490
  const newName = prompt("New name:", entry.name);
483
491
  if (!newName || newName === entry.name) return;
484
492
  const entryPath = entry.path.replaceAll("\\", "/");
@@ -500,7 +508,10 @@ async function renameFile(/** @type {any} */ entry, /** @type {() => void} */ re
500
508
  }
501
509
  }
502
510
 
503
- async function deleteFile(/** @type {any} */ entry, /** @type {() => void} */ renderLeftPanel) {
511
+ async function deleteFile(
512
+ /** @type {{ name: string; path: string; type: string }} */ entry,
513
+ /** @type {() => void} */ renderLeftPanel,
514
+ ) {
504
515
  if (!confirm(`Delete "${entry.name}"?`)) return;
505
516
  try {
506
517
  const platform = getPlatform();
@@ -583,3 +594,40 @@ export async function openFileFromTree(ctx, path) {
583
594
  statusMessage(`Error: ${e.message}`);
584
595
  }
585
596
  }
597
+
598
+ /**
599
+ * Open a file from the tree into a tab. Activates existing tab if already open.
600
+ *
601
+ * @param {string} path
602
+ */
603
+ export async function openFileInTab(path) {
604
+ for (const [id, tab] of workspace.tabs.entries()) {
605
+ if (tab.documentPath === path) {
606
+ activateTab(id);
607
+ projectState.selectedPath = path;
608
+ return;
609
+ }
610
+ }
611
+
612
+ const platform = getPlatform();
613
+ try {
614
+ const content = await platform.readFile(path);
615
+ if (!content) return;
616
+
617
+ let document, frontmatter;
618
+ if (path.endsWith(".md")) {
619
+ const state = await loadMarkdown(content, null);
620
+ document = state.document;
621
+ frontmatter = state.content?.frontmatter;
622
+ } else {
623
+ document = JSON.parse(content);
624
+ }
625
+
626
+ const id = path;
627
+ openTab({ id, documentPath: path, document, frontmatter });
628
+ projectState.selectedPath = path;
629
+ statusMessage(`Opened ${path.split("/").pop()}`);
630
+ } catch (/** @type {any} */ e) {
631
+ statusMessage(`Error: ${e.message}`);
632
+ }
633
+ }
@@ -1,7 +1,29 @@
1
1
  /** Activity bar — tab icons for switching left panel views. */
2
2
 
3
3
  import { html, render as litRender, nothing } from "lit-html";
4
- import { activityBar, update, getState, renderOnly } from "../store.js";
4
+ import { activityBar } from "../store.js";
5
+ import { effect, effectScope } from "../reactivity.js";
6
+ import { activeTab } from "../workspace/workspace.js";
7
+
8
+ /** @type {import("@vue/reactivity").EffectScope | null} */
9
+ let _scope = null;
10
+
11
+ export function mount() {
12
+ _scope = effectScope();
13
+ _scope.run(() => {
14
+ effect(() => {
15
+ const tab = activeTab.value;
16
+ if (!tab) return;
17
+ void tab.session.ui.leftTab;
18
+ renderActivityBar();
19
+ });
20
+ });
21
+ }
22
+
23
+ export function unmount() {
24
+ _scope?.stop();
25
+ _scope = null;
26
+ }
5
27
 
6
28
  const gitBranchIcon = (/** @type {any} */ s) => html`
7
29
  <svg
@@ -58,8 +80,10 @@ export function tabIcon(tag, size) {
58
80
  return fn ? fn(size || "s") : nothing;
59
81
  }
60
82
 
61
- /** @param {any} S — current studio state */
62
- export function renderActivityBar(S) {
83
+ export function renderActivityBar() {
84
+ const tab = activeTab.value;
85
+ if (!tab) return;
86
+ const leftTab = tab.session.ui.leftTab;
63
87
  const tabs = [
64
88
  { value: "files", icon: "sp-icon-folder", label: "Files" },
65
89
  { value: "layers", icon: "sp-icon-layers", label: "Layers" },
@@ -72,13 +96,11 @@ export function renderActivityBar(S) {
72
96
  ];
73
97
  const tpl = html`
74
98
  <sp-tabs
75
- selected=${S.ui.leftTab}
99
+ selected=${leftTab}
76
100
  direction="vertical"
77
101
  quiet
78
102
  @change=${(/** @type {any} */ e) => {
79
- const current = getState();
80
- update({ ...current, ui: { ...current.ui, leftTab: e.target.selected } });
81
- renderOnly("activityBar", "leftPanel");
103
+ activeTab.value.session.ui.leftTab = e.target.selected;
82
104
  }}
83
105
  >
84
106
  ${tabs.map(