@jxsuite/studio 0.13.0 → 0.15.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 (38) hide show
  1. package/dist/studio.js +467 -625
  2. package/dist/studio.js.map +37 -37
  3. package/package.json +1 -1
  4. package/src/canvas/canvas-live-render.js +25 -0
  5. package/src/canvas/canvas-render.js +20 -2
  6. package/src/canvas/canvas-utils.js +1 -2
  7. package/src/editor/component-inline-edit.js +1 -3
  8. package/src/editor/content-inline-edit.js +1 -3
  9. package/src/editor/context-menu.js +73 -35
  10. package/src/editor/insertion-helper.js +0 -1
  11. package/src/editor/shortcuts.js +39 -45
  12. package/src/files/file-ops.js +57 -108
  13. package/src/files/files.js +13 -8
  14. package/src/panels/activity-bar.js +6 -4
  15. package/src/panels/canvas-dnd.js +4 -10
  16. package/src/panels/dnd.js +45 -7
  17. package/src/panels/editors.js +14 -16
  18. package/src/panels/elements-panel.js +13 -13
  19. package/src/panels/git-panel.js +2 -1
  20. package/src/panels/layers-panel.js +11 -12
  21. package/src/panels/left-panel.js +2 -2
  22. package/src/panels/panel-events.js +19 -25
  23. package/src/panels/preview-render.js +7 -3
  24. package/src/panels/pseudo-preview.js +9 -8
  25. package/src/panels/statusbar.js +8 -16
  26. package/src/panels/style-inputs.js +2 -3
  27. package/src/panels/style-utils.js +8 -6
  28. package/src/panels/stylebook-layers-panel.js +5 -5
  29. package/src/panels/stylebook-panel.js +16 -16
  30. package/src/panels/toolbar.js +2 -2
  31. package/src/state.js +2 -3
  32. package/src/store.js +2 -133
  33. package/src/studio.js +111 -239
  34. package/src/tabs/tab.js +19 -4
  35. package/src/tabs/transact.js +19 -1
  36. package/src/ui/color-selector.js +4 -5
  37. package/src/view.js +3 -0
  38. package/src/workspace/workspace.js +5 -0
@@ -1,7 +1,7 @@
1
1
  /** Elements panel — block/component palette with categorized accordion and search filter. */
2
2
 
3
3
  import { html, nothing } from "lit-html";
4
- import { getState, getNodeAtPath } from "../store.js";
4
+ import { getNodeAtPath } from "../store.js";
5
5
  import { activeTab } from "../workspace/workspace.js";
6
6
  import { transactDoc, mutateInsertNode } from "../tabs/transact.js";
7
7
  import { view } from "../view.js";
@@ -13,7 +13,7 @@ import { componentRegistry } from "../files/components.js";
13
13
  * @returns {import("lit-html").TemplateResult}
14
14
  */
15
15
  export function renderElementsTemplate(ctx) {
16
- const S = getState();
16
+ const tab = activeTab.value;
17
17
 
18
18
  const categories = Object.entries(ctx.webdata.elements).map(
19
19
  (/** @type {any} */ [category, elements]) => {
@@ -38,12 +38,12 @@ export function renderElementsTemplate(ctx) {
38
38
  class="element-card"
39
39
  data-block-tag=${tag}
40
40
  @click=${() => {
41
- const s = getState();
42
- const parentPath = s.selection || [];
43
- const parent = getNodeAtPath(s.document, parentPath);
41
+ const t = activeTab.value;
42
+ const parentPath = t?.session.selection || [];
43
+ const parent = getNodeAtPath(t?.doc.document, parentPath);
44
44
  const idx = parent?.children ? parent.children.length : 0;
45
- transactDoc(activeTab.value, (t) =>
46
- mutateInsertNode(t, parentPath, idx, structuredClone(def)),
45
+ transactDoc(t, (tr) =>
46
+ mutateInsertNode(tr, parentPath, idx, structuredClone(def)),
47
47
  );
48
48
  }}
49
49
  >
@@ -57,7 +57,7 @@ export function renderElementsTemplate(ctx) {
57
57
  },
58
58
  );
59
59
 
60
- const effectiveEls = getEffectiveElements(S.document?.$elements);
60
+ const effectiveEls = getEffectiveElements(tab?.doc.document?.$elements);
61
61
  /** @type {Set<string>} */
62
62
  const enabledTags = new Set();
63
63
  for (const entry of effectiveEls) {
@@ -105,9 +105,9 @@ export function renderElementsTemplate(ctx) {
105
105
  ? `${comp.package}: <${comp.tagName}>`
106
106
  : comp.path}
107
107
  @click=${() => {
108
- const s = getState();
109
- const parentPath = s.selection || [];
110
- const parent = getNodeAtPath(s.document, parentPath);
108
+ const t = activeTab.value;
109
+ const parentPath = t?.session.selection || [];
110
+ const parent = getNodeAtPath(t?.doc.document, parentPath);
111
111
  const idx = parent?.children ? parent.children.length : 0;
112
112
  const instanceDef = {
113
113
  tagName: comp.tagName,
@@ -118,8 +118,8 @@ export function renderElementsTemplate(ctx) {
118
118
  ]),
119
119
  ),
120
120
  };
121
- transactDoc(activeTab.value, (t) =>
122
- mutateInsertNode(t, parentPath, idx, structuredClone(instanceDef)),
121
+ transactDoc(t, (tr) =>
122
+ mutateInsertNode(tr, parentPath, idx, structuredClone(instanceDef)),
123
123
  );
124
124
  }}
125
125
  >
@@ -7,6 +7,7 @@ import { html, nothing } from "lit-html";
7
7
  import { live } from "lit-html/directives/live.js";
8
8
  import { getPlatform } from "../platform.js";
9
9
  import { updateUi, renderOnly } from "../store.js";
10
+ import { view } from "../view.js";
10
11
 
11
12
  async function refreshGitStatus() {
12
13
  const plat = getPlatform();
@@ -60,7 +61,7 @@ export function renderGitPanel(S, ctx) {
60
61
 
61
62
  if (!_pollTimer) {
62
63
  _pollTimer = setInterval(() => {
63
- if (S.ui.leftTab === "git" && !S.ui.gitLoading) refreshGitStatus();
64
+ if (view.leftTab === "git" && !S.ui.gitLoading) refreshGitStatus();
64
65
  }, 30000);
65
66
  }
66
67
 
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { html, nothing } from "lit-html";
7
7
  import {
8
- getState,
9
8
  flattenTree,
10
9
  getNodeAtPath,
11
10
  pathKey,
@@ -26,14 +25,13 @@ import { showContextMenu } from "../editor/context-menu.js";
26
25
  * @returns {import("lit-html").TemplateResult}
27
26
  */
28
27
  export function renderLayersTemplate(ctx) {
29
- const S = getState();
28
+ const tab = activeTab.value;
30
29
 
31
30
  for (const fn of view.dndCleanups) fn();
32
31
  view.dndCleanups = [];
33
32
 
34
- const rows = flattenTree(S.document);
35
- const _S = /** @type {any} */ (S);
36
- const collapsed = _S._collapsed || (_S._collapsed = new Set());
33
+ const rows = flattenTree(tab?.doc.document);
34
+ const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
37
35
 
38
36
  /** @type {any[]} */
39
37
  const layerRows = [];
@@ -48,7 +46,7 @@ export function renderLayersTemplate(ctx) {
48
46
  }
49
47
  if (hidden) continue;
50
48
 
51
- if (S.mode === "content" && path.length === 0) continue;
49
+ if (tab?.doc.mode === "content" && path.length === 0) continue;
52
50
 
53
51
  if (nodeType === "text") {
54
52
  const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
@@ -66,12 +64,12 @@ export function renderLayersTemplate(ctx) {
66
64
 
67
65
  if (path.length >= 2 && nodeType === "element") {
68
66
  const pPath = parentElementPath(path);
69
- const parentNode = pPath ? getNodeAtPath(S.document, pPath) : null;
67
+ const parentNode = pPath ? getNodeAtPath(tab?.doc.document, pPath) : null;
70
68
  if (parentNode && isInlineElement(node, parentNode)) continue;
71
69
  }
72
70
 
73
71
  const key = pathKey(path);
74
- const isSelected = pathsEqual(path, S.selection);
72
+ const isSelected = pathsEqual(path, tab?.session.selection);
75
73
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
76
74
  const hasMapChildren =
77
75
  node.children && typeof node.children === "object" && node.children.$prototype === "Array";
@@ -115,10 +113,10 @@ export function renderLayersTemplate(ctx) {
115
113
  }
116
114
 
117
115
  const isElement = nodeType === "element";
118
- const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
116
+ const isRoot = tab?.doc.mode === "content" ? path.length === 0 : path.length < 2;
119
117
  const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
120
118
  const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
121
- const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
119
+ const parentNode = parentPath ? getNodeAtPath(tab?.doc.document, parentPath) : null;
122
120
  const siblingCount = parentNode?.children?.length || 0;
123
121
  const canMoveUp = isElement && !isRoot && idx > 0;
124
122
  const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
@@ -141,10 +139,11 @@ export function renderLayersTemplate(ctx) {
141
139
  data-dnd-row=${isElement ? key : nothing}
142
140
  data-dnd-depth=${isElement ? depth : nothing}
143
141
  data-dnd-void=${isElement && isVoidEl ? "" : nothing}
142
+ data-dnd-expanded=${isElement && isExpandable && !collapsed.has(key) ? "" : nothing}
144
143
  @click=${() => (activeTab.value.session.selection = path)}
145
144
  @contextmenu=${isElement
146
145
  ? (/** @type {any} */ e) =>
147
- showContextMenu(e, path, getState(), {
146
+ showContextMenu(e, path, {
148
147
  onEditComponent: ctx.navigateToComponent,
149
148
  })
150
149
  : nothing}
@@ -207,7 +206,7 @@ export function renderLayersTemplate(ctx) {
207
206
  e.stopPropagation();
208
207
  /** @type {HTMLElement} */ (e.currentTarget).blur();
209
208
  const prevPath = [...parentPath, idx - 1];
210
- const prev = getNodeAtPath(getState().document, prevPath);
209
+ const prev = getNodeAtPath(activeTab.value?.doc.document, prevPath);
211
210
  const len = prev?.children?.length || 0;
212
211
  transactDoc(activeTab.value, (t) => mutateMoveNode(t, path, prevPath, len));
213
212
  }}
@@ -10,6 +10,7 @@ import { html, render as litRender, nothing } from "lit-html";
10
10
  import { leftPanel, updateSession } from "../store.js";
11
11
  import { effect, effectScope } from "../reactivity.js";
12
12
  import { activeTab } from "../workspace/workspace.js";
13
+ import { view } from "../view.js";
13
14
  import { transact, mutateUpdateFrontmatter } from "../tabs/transact.js";
14
15
 
15
16
  import { renderLayersTemplate } from "./layers-panel.js";
@@ -67,7 +68,6 @@ export function mount(ctx) {
67
68
  void tab.doc.document;
68
69
  void tab.doc.mode;
69
70
  void tab.session.selection;
70
- void tab.session.ui.leftTab;
71
71
  void tab.session.ui.settingsTab;
72
72
  render();
73
73
  });
@@ -121,7 +121,7 @@ function _render() {
121
121
  mode: aTab.doc.mode,
122
122
  selection: aTab.session.selection,
123
123
  });
124
- const tab = S.ui.leftTab;
124
+ const tab = view.leftTab;
125
125
 
126
126
  /** @type {TemplateResult | typeof nothing} */
127
127
  let content;
@@ -6,7 +6,6 @@
6
6
 
7
7
  import {
8
8
  updateUi,
9
- hoverNode,
10
9
  elToPath,
11
10
  pathsEqual,
12
11
  parentElementPath,
@@ -30,8 +29,6 @@ let _ctx = null;
30
29
  * Initialize the panel events module.
31
30
  *
32
31
  * @param {{
33
- * getState: () => any;
34
- * setState: (s: any) => void;
35
32
  * getCanvasMode: () => string;
36
33
  * enterInlineEdit: (el: any, path: any) => void;
37
34
  * navigateToComponent: (path: any) => void;
@@ -77,7 +74,7 @@ export function registerPanelEvents(panel) {
77
74
  stopEditing();
78
75
  }
79
76
 
80
- const S = _ctx.getState();
77
+ const tab = activeTab.value;
81
78
  const canvasMode = _ctx.getCanvasMode();
82
79
 
83
80
  const elements = withPanelPointerEvents(() =>
@@ -97,23 +94,22 @@ export function registerPanelEvents(panel) {
97
94
 
98
95
  const originalPath = elToPath.get(el);
99
96
  if (originalPath) {
100
- let path = bubbleInlinePath(S.document, originalPath);
97
+ let path = bubbleInlinePath(tab?.doc.document, originalPath);
101
98
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
102
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
103
99
 
104
100
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
105
101
 
106
102
  if (
107
- pathsEqual(path, S.selection) &&
103
+ pathsEqual(path, tab?.session.selection) &&
108
104
  isEditableBlock(resolvedEl) &&
109
- (canvasMode === "edit" || S.mode === "content")
105
+ (canvasMode === "edit" || tab?.doc.mode === "content")
110
106
  ) {
111
- _ctx.setState(withMedia);
107
+ activeTab.value.session.ui.activeMedia = newMedia;
112
108
  _ctx.enterInlineEdit(resolvedEl, path);
113
109
  return;
114
110
  }
115
111
 
116
- if (canvasMode === "design" && S.mode !== "content") {
112
+ if (canvasMode === "design" && tab?.doc.mode !== "content") {
117
113
  updateUi("pendingInlineEdit", { path, mediaName });
118
114
  activeTab.value.session.ui.activeMedia = newMedia;
119
115
  activeTab.value.session.selection = path;
@@ -148,7 +144,7 @@ export function registerPanelEvents(panel) {
148
144
  const canvasMode = _ctx.getCanvasMode();
149
145
  if (canvasMode !== "edit" && canvasMode !== "design") return;
150
146
 
151
- const S = _ctx.getState();
147
+ const tab = activeTab.value;
152
148
  const elements = withPanelPointerEvents(() =>
153
149
  document.elementsFromPoint(e.clientX, e.clientY),
154
150
  );
@@ -157,7 +153,7 @@ export function registerPanelEvents(panel) {
157
153
  if (canvas.contains(el) && el !== canvas) {
158
154
  const originalPath = elToPath.get(el);
159
155
  if (originalPath) {
160
- const path = bubbleInlinePath(S.document, originalPath);
156
+ const path = bubbleInlinePath(tab?.doc.document, originalPath);
161
157
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
162
158
  if (isEditableBlock(resolvedEl)) {
163
159
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
@@ -187,7 +183,7 @@ export function registerPanelEvents(panel) {
187
183
  )
188
184
  return;
189
185
  }
190
- const S = _ctx.getState();
186
+ const tab = activeTab.value;
191
187
  const elements = withPanelPointerEvents(() =>
192
188
  document.elementsFromPoint(e.clientX, e.clientY),
193
189
  );
@@ -195,8 +191,8 @@ export function registerPanelEvents(panel) {
195
191
  if (canvas.contains(el) && el !== canvas) {
196
192
  let path = elToPath.get(el);
197
193
  if (path) {
198
- path = bubbleInlinePath(S.document, path);
199
- showContextMenu(e, path, S, { onEditComponent: _ctx.navigateToComponent });
194
+ path = bubbleInlinePath(tab?.doc.document, path);
195
+ showContextMenu(e, path, { onEditComponent: _ctx.navigateToComponent });
200
196
  return;
201
197
  }
202
198
  }
@@ -220,19 +216,19 @@ export function registerPanelEvents(panel) {
220
216
  )
221
217
  return;
222
218
  }
223
- let S = _ctx.getState();
219
+ const tab = activeTab.value;
224
220
  const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
225
221
  if (el && canvas.contains(el) && el !== canvas) {
226
222
  let path = elToPath.get(el);
227
223
  if (path) {
228
- path = bubbleInlinePath(S.document, path);
229
- if (!pathsEqual(path, S.hover)) {
230
- _ctx.setState(hoverNode(S, path));
224
+ path = bubbleInlinePath(tab?.doc.document, path);
225
+ if (!pathsEqual(path, tab?.session.hover)) {
226
+ activeTab.value.session.hover = path;
231
227
  renderOnly("overlays");
232
228
  }
233
229
  }
234
- } else if (S.hover) {
235
- _ctx.setState(hoverNode(S, null));
230
+ } else if (tab?.session.hover) {
231
+ activeTab.value.session.hover = null;
236
232
  renderOnly("overlays");
237
233
  }
238
234
  },
@@ -242,9 +238,8 @@ export function registerPanelEvents(panel) {
242
238
  overlayClk.addEventListener(
243
239
  "mouseleave",
244
240
  () => {
245
- const S = _ctx.getState();
246
- if (S.hover) {
247
- _ctx.setState(hoverNode(S, null));
241
+ if (activeTab.value?.session.hover) {
242
+ activeTab.value.session.hover = null;
248
243
  renderOnly("overlays");
249
244
  }
250
245
  },
@@ -252,7 +247,6 @@ export function registerPanelEvents(panel) {
252
247
  );
253
248
 
254
249
  insertionHelper.mount({
255
- getState: _ctx.getState,
256
250
  getCanvasMode: _ctx.getCanvasMode,
257
251
  withPanelPointerEvents,
258
252
  effectiveZoom: effectiveZoom,
@@ -3,7 +3,8 @@
3
3
  * DOM from Jx node trees as a fallback when runtime rendering fails.
4
4
  */
5
5
 
6
- import { getState, elToPath } from "../store.js";
6
+ import { elToPath } from "../store.js";
7
+ import { activeTab } from "../workspace/workspace.js";
7
8
  import { applyCanvasStyle } from "../utils/canvas-media.js";
8
9
  import { resolveDefaultForCanvas } from "../panels/signals-panel.js";
9
10
 
@@ -32,7 +33,10 @@ export function renderCanvasNode(node, path, parent, activeBreakpoints, featureT
32
33
  if (typeof node.textContent === "string") {
33
34
  el.textContent = node.textContent;
34
35
  } else if (typeof node.textContent === "object" && node.textContent?.$ref) {
35
- const resolved = resolveDefaultForCanvas(node.textContent, getState().document.state);
36
+ const resolved = resolveDefaultForCanvas(
37
+ node.textContent,
38
+ activeTab.value?.doc.document?.state,
39
+ );
36
40
  el.textContent = resolved;
37
41
  el.style.opacity = "0.7";
38
42
  el.style.fontStyle = "italic";
@@ -48,7 +52,7 @@ export function renderCanvasNode(node, path, parent, activeBreakpoints, featureT
48
52
  for (const [attr, val] of Object.entries(node.attributes)) {
49
53
  try {
50
54
  if (typeof val === "object" && val?.$ref) {
51
- const resolved = resolveDefaultForCanvas(val, getState().document.state);
55
+ const resolved = resolveDefaultForCanvas(val, activeTab.value?.doc.document?.state);
52
56
  el.setAttribute(attr, resolved);
53
57
  } else {
54
58
  el.setAttribute(attr, val);
@@ -3,7 +3,8 @@
3
3
  * :focus, etc.) is active in the style sidebar, force those styles onto the selected element.
4
4
  */
5
5
 
6
- import { getState, getNodeAtPath } from "../store.js";
6
+ import { getNodeAtPath } from "../store.js";
7
+ import { activeTab } from "../workspace/workspace.js";
7
8
  import { view } from "../view.js";
8
9
  import { getActivePanel, findCanvasElement } from "../canvas/canvas-helpers.js";
9
10
 
@@ -21,20 +22,20 @@ export function updateForcedPseudoPreview() {
21
22
  view.forcedAttrEl = null;
22
23
  }
23
24
 
24
- const S = getState();
25
- const sel = S.ui?.activeSelector;
26
- if (!sel || !sel.startsWith(":") || !S.selection) return;
25
+ const tab = activeTab.value;
26
+ const sel = tab?.session.ui?.activeSelector;
27
+ if (!sel || !sel.startsWith(":") || !tab?.session.selection) return;
27
28
 
28
29
  const panel = getActivePanel();
29
30
  if (!panel) return;
30
- const el = findCanvasElement(S.selection, panel.canvas);
31
+ const el = findCanvasElement(tab.session.selection, panel.canvas);
31
32
  if (!el) return;
32
33
 
33
- const node = getNodeAtPath(S.document, S.selection);
34
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
34
35
  if (!node?.style) return;
35
- const activeTab = S.ui.activeMedia;
36
+ const activeMedia = tab.session.ui.activeMedia;
36
37
  /** @type {any} */
37
- const ctx = activeTab ? node.style[`@${activeTab}`] || {} : node.style;
38
+ const ctx = activeMedia ? node.style[`@${activeMedia}`] || {} : node.style;
38
39
  const rules = ctx[sel];
39
40
  if (!rules || typeof rules !== "object") return;
40
41
 
@@ -34,11 +34,7 @@ export function mountStatusbar() {
34
34
  void tab.doc.document;
35
35
  void tab.doc.mode;
36
36
  void tab.session.selection;
37
- renderStatusbar({
38
- mode: tab.doc.mode,
39
- document: tab.doc.document,
40
- selection: tab.session.selection,
41
- });
37
+ renderStatusbar();
42
38
  });
43
39
  });
44
40
  }
@@ -50,19 +46,15 @@ export function unmountStatusbar() {
50
46
 
51
47
  // ─── Statusbar ───────────────────────────────────────────────────────────────
52
48
 
53
- /**
54
- * Render the statusbar text. Receives current studio state so the module stays decoupled from the
55
- * mutable `S` local in studio.js.
56
- *
57
- * @param {any} S - Current studio state
58
- */
59
- export function renderStatusbar(S) {
49
+ /** Render the statusbar text. */
50
+ export function renderStatusbar() {
51
+ const tab = activeTab.value;
60
52
  const parts = [];
61
- if (S.mode === "content") parts.push("Content Mode");
62
- if (S.selection) {
63
- const node = getNodeAtPath(S.document, S.selection);
53
+ if (tab?.doc.mode === "content") parts.push("Content Mode");
54
+ if (tab?.session.selection) {
55
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
64
56
  parts.push(`Selected: ${nodeLabel(node)}`);
65
- parts.push(`Path: ${S.selection.join(" > ") || "root"}`);
57
+ parts.push(`Path: ${tab.session.selection.join(" > ") || "root"}`);
66
58
  }
67
59
  if (statusMsg) parts.push(statusMsg);
68
60
  statusbarEl.textContent = parts.join(" | ") || "Jx Studio";
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { html } from "lit-html";
4
4
  import { live } from "lit-html/directives/live.js";
5
- import { getState, debouncedStyleCommit } from "../store.js";
5
+ import { debouncedStyleCommit } from "../store.js";
6
6
  import { activeTab } from "../workspace/workspace.js";
7
7
  import { transactDoc, mutateUpdateStyle } from "../tabs/transact.js";
8
8
  import { widgetForType as _widgetForType } from "../ui/widgets.js";
@@ -53,9 +53,8 @@ export function renderSelectInput(entry, prop, value, onChange) {
53
53
 
54
54
  /** @param {any} preset @param {any} onChange */
55
55
  function handleFontPresetSelection(preset, onChange) {
56
- const S = getState();
57
56
  const varName = friendlyNameToVar(preset.title, "--font-");
58
- if (!S.document?.style?.[varName]) {
57
+ if (!activeTab.value?.doc.document?.style?.[varName]) {
59
58
  transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, preset.value));
60
59
  }
61
60
  onChange(`var(${varName})`);
@@ -1,6 +1,7 @@
1
1
  /** Style utilities — pure CSS helper functions used by the style panel. */
2
2
 
3
- import { getState, getNodeAtPath } from "../store.js";
3
+ import { getNodeAtPath } from "../store.js";
4
+ import { activeTab } from "../workspace/workspace.js";
4
5
  import { camelToKebab } from "../utils/studio-utils.js";
5
6
  import cssMeta from "../../data/css-meta.json";
6
7
 
@@ -159,8 +160,7 @@ export function compressBorderSide(/** @type {string[]} */ vals) {
159
160
 
160
161
  /** Extract --font-* CSS custom properties from the document root style. */
161
162
  export function getFontVars() {
162
- const S = getState();
163
- const style = S.document?.style;
163
+ const style = activeTab.value?.doc.document?.style;
164
164
  if (!style) return [];
165
165
  const vars = [];
166
166
  for (const [k, v] of Object.entries(style)) {
@@ -181,12 +181,14 @@ export const TYPO_PREVIEW_PROPS = new Set([
181
181
 
182
182
  /** Resolve the current font family for typography preview (handles var() references) */
183
183
  export function currentFontFamily() {
184
- const S = getState();
185
- const node = S.selection ? getNodeAtPath(S.document, S.selection) : null;
184
+ const tab = activeTab.value;
185
+ const node = tab?.session.selection
186
+ ? getNodeAtPath(tab.doc.document, tab.session.selection)
187
+ : null;
186
188
  const raw = node?.style?.fontFamily;
187
189
  if (!raw) return "";
188
190
  const m = typeof raw === "string" && raw.match(/^var\((--[^)]+)\)$/);
189
- if (m) return S.document?.style?.[m[1]] || "";
191
+ if (m) return tab?.doc.document?.style?.[m[1]] || "";
190
192
  return raw;
191
193
  }
192
194
 
@@ -1,7 +1,7 @@
1
1
  /** Stylebook layers panel — shows element/variable tree when in stylebook (settings) mode. */
2
2
 
3
3
  import { html, nothing } from "lit-html";
4
- import { getState } from "../store.js";
4
+ import { activeTab } from "../workspace/workspace.js";
5
5
  import { componentRegistry } from "../files/components.js";
6
6
 
7
7
  /**
@@ -18,11 +18,11 @@ function hasTagStyle(rootStyle, tag) {
18
18
  * @returns {import("lit-html").TemplateResult}
19
19
  */
20
20
  export function renderStylebookLayersTemplate(ctx) {
21
- const S = getState();
22
- const rootStyle = S.document?.style || {};
23
- const selectedTag = S.ui.stylebookSelection;
21
+ const tab = activeTab.value;
22
+ const rootStyle = tab?.doc.document?.style || {};
23
+ const selectedTag = tab?.session.ui.stylebookSelection;
24
24
 
25
- if (S.ui.stylebookTab === "elements") {
25
+ if (tab?.session.ui.stylebookTab === "elements") {
26
26
  /**
27
27
  * @param {any} entry
28
28
  * @param {number} depth
@@ -8,7 +8,6 @@ import { ref } from "lit-html/directives/ref.js";
8
8
  import { styleMap } from "lit-html/directives/style-map.js";
9
9
 
10
10
  import {
11
- getState,
12
11
  updateSession,
13
12
  updateUi,
14
13
  canvasWrap,
@@ -49,9 +48,8 @@ let _ctx = null;
49
48
  */
50
49
  export function renderStylebookMode(ctx) {
51
50
  _ctx = ctx;
52
- const S = getState();
53
51
 
54
- const settingsTab = S.ui.settingsTab || "stylebook";
52
+ const settingsTab = activeTab.value?.session.ui.settingsTab || "stylebook";
55
53
 
56
54
  const settingsChromeBarTpl = html`
57
55
  <div
@@ -94,11 +92,14 @@ export function renderStylebookMode(ctx) {
94
92
 
95
93
  // Stylebook tab — element catalog / variables
96
94
  view.stylebookElToTag = new WeakMap();
97
- const rootStyle = getEffectiveStyle(S.document.style);
98
- const filter = (S.ui.stylebookFilter || "").toLowerCase();
99
- const customizedOnly = S.ui.stylebookCustomizedOnly;
95
+ const tab = activeTab.value;
96
+ const rootStyle = getEffectiveStyle(tab?.doc.document?.style);
97
+ const filter = (tab?.session.ui.stylebookFilter || "").toLowerCase();
98
+ const customizedOnly = tab?.session.ui.stylebookCustomizedOnly;
100
99
 
101
- const { sizeBreakpoints, baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
100
+ const { sizeBreakpoints, baseWidth } = parseMediaEntries(
101
+ getEffectiveMedia(tab?.doc.document?.$media),
102
+ );
102
103
  const hasMedia = sizeBreakpoints.length > 0;
103
104
 
104
105
  const onTabClick = (/** @type {string} */ t) => {
@@ -110,7 +111,7 @@ export function renderStylebookMode(ctx) {
110
111
  };
111
112
 
112
113
  const onCustomizedToggle = () => {
113
- updateUi("stylebookCustomizedOnly", !S.ui.stylebookCustomizedOnly);
114
+ updateUi("stylebookCustomizedOnly", !tab?.session.ui.stylebookCustomizedOnly);
114
115
  };
115
116
 
116
117
  const chromeBarTpl = html`
@@ -121,7 +122,7 @@ export function renderStylebookMode(ctx) {
121
122
  >
122
123
  <sp-tabs
123
124
  size="s"
124
- selected=${S.ui.stylebookTab || "elements"}
125
+ selected=${tab?.session.ui.stylebookTab || "elements"}
125
126
  @change=${(/** @type {any} */ e) => {
126
127
  onTabClick(e.target.selected);
127
128
  }}
@@ -132,17 +133,17 @@ export function renderStylebookMode(ctx) {
132
133
  `,
133
134
  )}
134
135
  </sp-tabs>
135
- ${S.ui.stylebookTab === "elements"
136
+ ${tab?.session.ui.stylebookTab === "elements"
136
137
  ? html`
137
138
  <input
138
139
  class="field-input"
139
140
  style="flex:1;max-width:200px;margin-left:8px"
140
141
  placeholder="Filter…"
141
- .value=${S.ui.stylebookFilter}
142
+ .value=${tab?.session.ui.stylebookFilter}
142
143
  @input=${onFilterInput}
143
144
  />
144
145
  <button
145
- class="tb-toggle${S.ui.stylebookCustomizedOnly ? " active" : ""}"
146
+ class="tb-toggle${tab?.session.ui.stylebookCustomizedOnly ? " active" : ""}"
146
147
  style="margin-left:4px"
147
148
  @click=${onCustomizedToggle}
148
149
  >
@@ -176,7 +177,7 @@ export function renderStylebookMode(ctx) {
176
177
 
177
178
  const renderIntoPanel = (/** @type {any} */ panel, /** @type {any} */ activeBreakpoints) => {
178
179
  panel.canvas.classList.add("sb-canvas");
179
- if (S.ui.stylebookTab === "elements") {
180
+ if (tab?.session.ui.stylebookTab === "elements") {
180
181
  renderStylebookElementsIntoCanvas(
181
182
  panel.canvas,
182
183
  rootStyle,
@@ -197,7 +198,7 @@ export function renderStylebookMode(ctx) {
197
198
  /** @type {{ tpl: any; panel: any; activeSet: any }[]} */
198
199
  let panelEntries;
199
200
  if (!hasMedia) {
200
- const effectiveMedia = getEffectiveMedia(S.document.$media);
201
+ const effectiveMedia = getEffectiveMedia(tab?.doc.document?.$media);
201
202
  const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
202
203
  const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
203
204
  const entry = ctx.canvasPanelTemplate(
@@ -268,8 +269,7 @@ export function renderStylebookOverlays() {
268
269
  if (!_ctx) return;
269
270
  if (canvasPanels.length === 0) return;
270
271
 
271
- const S = getState();
272
- const selectedTag = S.ui.stylebookSelection;
272
+ const selectedTag = activeTab.value?.session.ui.stylebookSelection;
273
273
 
274
274
  for (const panel of canvasPanels) {
275
275
  const hoverTag = /** @type {any} */ (panel)._lastHoverTag;
@@ -72,9 +72,9 @@ export function mount(rootEl, ctx) {
72
72
  void tab.doc.dirty;
73
73
  void tab.doc.mode;
74
74
  void tab.session.selection;
75
+ void tab.session.ui.canvasMode;
75
76
  void tab.session.ui.editingFunction;
76
77
  void tab.session.ui.featureToggles;
77
- void tab.session.ui.leftTab;
78
78
  void tab.session.ui.rightTab;
79
79
  render();
80
80
  });
@@ -213,7 +213,7 @@ function toolbarTemplate() {
213
213
  /** @type {Record<string, any>} */
214
214
  const uiPatch = { editingFunction: null };
215
215
  if (m.key === "settings") uiPatch.rightTab = "style";
216
- if (m.key === "manage") uiPatch.leftTab = "files";
216
+ if (m.key === "manage") view.leftTab = "files";
217
217
  updateSession({ ui: uiPatch });
218
218
  _ctx.renderCanvas();
219
219
  _ctx.safeRenderRightPanel();