@jxsuite/studio 0.11.0 → 0.14.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 (55) hide show
  1. package/dist/studio.js +6517 -5870
  2. package/dist/studio.js.map +57 -51
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +28 -2
  7. package/src/canvas/canvas-render.js +170 -20
  8. package/src/canvas/canvas-utils.js +22 -25
  9. package/src/editor/component-inline-edit.js +55 -44
  10. package/src/editor/content-inline-edit.js +47 -50
  11. package/src/editor/context-menu.js +78 -53
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -11
  14. package/src/editor/shortcuts.js +88 -64
  15. package/src/files/components.js +15 -4
  16. package/src/files/file-ops.js +57 -108
  17. package/src/files/files.js +81 -28
  18. package/src/panels/activity-bar.js +31 -7
  19. package/src/panels/block-action-bar.js +104 -80
  20. package/src/panels/canvas-dnd.js +4 -10
  21. package/src/panels/dnd.js +77 -35
  22. package/src/panels/editors.js +17 -26
  23. package/src/panels/elements-panel.js +17 -11
  24. package/src/panels/events-panel.js +44 -39
  25. package/src/panels/git-panel.js +47 -4
  26. package/src/panels/layers-panel.js +25 -21
  27. package/src/panels/left-panel.js +109 -44
  28. package/src/panels/overlays.js +91 -41
  29. package/src/panels/panel-events.js +28 -37
  30. package/src/panels/preview-render.js +7 -3
  31. package/src/panels/properties-panel.js +179 -104
  32. package/src/panels/pseudo-preview.js +9 -8
  33. package/src/panels/right-panel.js +85 -37
  34. package/src/panels/shared.js +0 -22
  35. package/src/panels/signals-panel.js +125 -54
  36. package/src/panels/statusbar.js +26 -19
  37. package/src/panels/style-inputs.js +5 -4
  38. package/src/panels/style-panel.js +128 -105
  39. package/src/panels/style-utils.js +8 -6
  40. package/src/panels/stylebook-layers-panel.js +5 -5
  41. package/src/panels/stylebook-panel.js +27 -31
  42. package/src/panels/tab-strip.js +124 -0
  43. package/src/panels/toolbar.js +40 -10
  44. package/src/reactivity.js +28 -0
  45. package/src/settings/content-types-editor.js +56 -7
  46. package/src/settings/defs-editor.js +56 -7
  47. package/src/settings/schema-field-ui.js +60 -25
  48. package/src/state.js +2 -459
  49. package/src/store.js +61 -219
  50. package/src/studio.js +163 -300
  51. package/src/tabs/tab.js +168 -0
  52. package/src/tabs/transact.js +406 -0
  53. package/src/ui/color-selector.js +4 -5
  54. package/src/view.js +3 -0
  55. package/src/workspace/workspace.js +90 -0
@@ -7,40 +7,94 @@
7
7
  */
8
8
 
9
9
  import { html, render as litRender, nothing } from "lit-html";
10
- import {
11
- getState,
12
- leftPanel,
13
- updateSession,
14
- update,
15
- applyMutation,
16
- updateFrontmatter,
17
- } from "../store.js";
18
- import { ensureLitState } from "./shared.js";
10
+ import { leftPanel, updateSession } from "../store.js";
11
+ import { effect, effectScope } from "../reactivity.js";
12
+ import { activeTab } from "../workspace/workspace.js";
13
+ import { view } from "../view.js";
14
+ import { transact, mutateUpdateFrontmatter } from "../tabs/transact.js";
15
+
19
16
  import { renderLayersTemplate } from "./layers-panel.js";
20
17
  import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
21
18
  import { renderElementsTemplate } from "./elements-panel.js";
22
19
  import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
23
20
 
24
- /** @type {any} */
21
+ /** @typedef {import("lit-html").TemplateResult} TemplateResult */
22
+
23
+ /**
24
+ * @typedef {{
25
+ * getCanvasMode: () => string;
26
+ * setCanvasMode: (mode: string) => void;
27
+ * renderImportsTemplate: (...args: any[]) => TemplateResult;
28
+ * renderFilesTemplate: () => TemplateResult;
29
+ * renderSignalsTemplate: (...args: any[]) => TemplateResult;
30
+ * renderDataExplorerTemplate: (...args: any[]) => TemplateResult;
31
+ * renderHeadTemplate: (...args: any[]) => TemplateResult;
32
+ * renderGitPanel: (...args: any[]) => TemplateResult;
33
+ * renderCanvas: () => void;
34
+ * defCategory: (tag: string) => string;
35
+ * defBadgeLabel: (tag: string) => string;
36
+ * navigateToComponent: (path: string) => void;
37
+ * webdata: object;
38
+ * defaultDef: (tag: string) => object;
39
+ * registerLayersDnD: () => void;
40
+ * registerElementsDnD: () => void;
41
+ * registerComponentsDnD: () => void;
42
+ * setupTreeKeyboard: (tree: HTMLElement) => void;
43
+ * }} LeftPanelCtx
44
+ */
45
+
46
+ /** @type {LeftPanelCtx | null} */
25
47
  let _ctx = null;
26
48
 
49
+ /** @type {import("@vue/reactivity").EffectScope | null} */
50
+ let _scope = null;
51
+
52
+ let _rendering = false;
53
+ let _scheduled = false;
54
+
27
55
  /**
28
56
  * Mount the left panel orchestrator.
29
57
  *
30
- * @param {any} ctx — callbacks and references that avoid circular dependencies
58
+ * @param {LeftPanelCtx} ctx
31
59
  */
32
60
  export function mount(ctx) {
33
61
  _ctx = ctx;
62
+ _scope = effectScope();
63
+ _scope.run(() => {
64
+ effect(() => {
65
+ const tab = activeTab.value;
66
+ if (!tab) return;
67
+ // Track properties the left panel reads
68
+ void tab.doc.document;
69
+ void tab.doc.mode;
70
+ void tab.session.selection;
71
+ void tab.session.ui.settingsTab;
72
+ render();
73
+ });
74
+ });
34
75
  }
35
76
 
36
77
  export function unmount() {
78
+ _scope?.stop();
79
+ _scope = null;
37
80
  _ctx = null;
38
81
  }
39
82
 
40
83
  export function render() {
41
84
  if (!_ctx) return;
85
+ if (_rendering) return;
86
+ if (!_scheduled) {
87
+ _scheduled = true;
88
+ queueMicrotask(_flush);
89
+ }
90
+ }
91
+
92
+ function _flush() {
93
+ _scheduled = false;
94
+ if (!_ctx) return;
95
+ if (_rendering) return;
96
+ _rendering = true;
42
97
  try {
43
- ensureLitState(leftPanel);
44
98
  _render();
45
99
  } catch (e) {
46
100
  console.error("left-panel render error:", e);
@@ -52,90 +106,101 @@ export function render() {
52
106
  } catch (e2) {
53
107
  console.error("left-panel retry failed:", e2);
54
108
  }
109
+ } finally {
110
+ _rendering = false;
55
111
  }
56
112
  }
57
113
 
58
114
  function _render() {
59
- const S = getState();
60
- const tab = S.ui.leftTab;
115
+ const ctx = /** @type {LeftPanelCtx} */ (_ctx);
116
+ const aTab = activeTab.value;
117
+ if (!aTab) return;
118
+ const S = /** @type {any} */ ({
119
+ ui: aTab.session.ui,
120
+ document: aTab.doc.document,
121
+ mode: aTab.doc.mode,
122
+ selection: aTab.session.selection,
123
+ });
124
+ const tab = view.leftTab;
61
125
 
62
- /** @type {any} */
126
+ /** @type {TemplateResult | typeof nothing} */
63
127
  let content;
64
128
  if (tab === "layers")
65
129
  content =
66
- _ctx.getCanvasMode() === "settings"
130
+ ctx.getCanvasMode() === "settings"
67
131
  ? renderStylebookLayersTemplate({
68
132
  selectStylebookTag,
69
133
  stylebookMeta,
70
134
  })
71
135
  : renderLayersTemplate({
72
- navigateToComponent: _ctx.navigateToComponent,
136
+ navigateToComponent: ctx.navigateToComponent,
73
137
  rerender: render,
74
138
  });
75
139
  else if (tab === "imports")
76
- content = _ctx.renderImportsTemplate({
140
+ content = ctx.renderImportsTemplate({
77
141
  renderLeftPanel: render,
78
142
  documentPath: S.documentPath,
79
143
  documentElements: S.document.$elements || [],
80
- applyMutation: (/** @type {any} */ fn) => {
81
- update(applyMutation(getState(), fn));
144
+ applyMutation: (/** @type {(doc: object) => void} */ fn) => {
145
+ transact(activeTab.value, fn);
82
146
  },
83
147
  });
84
- else if (tab === "files") content = _ctx.renderFilesTemplate();
148
+ else if (tab === "files") content = ctx.renderFilesTemplate();
85
149
  else if (tab === "blocks")
86
150
  content = renderElementsTemplate({
87
- webdata: _ctx.webdata,
88
- defaultDef: _ctx.defaultDef,
151
+ webdata: ctx.webdata,
152
+ defaultDef: ctx.defaultDef,
89
153
  rerender: render,
90
154
  });
91
155
  else if (tab === "state")
92
- content = _ctx.renderSignalsTemplate(S, {
156
+ content = ctx.renderSignalsTemplate(S, {
93
157
  renderLeftPanel: render,
94
- renderCanvas: _ctx.renderCanvas,
158
+ renderCanvas: ctx.renderCanvas,
95
159
  updateSession,
96
160
  });
97
161
  else if (tab === "data")
98
- content = _ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
99
- renderCanvas: _ctx.renderCanvas,
162
+ content = ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
163
+ renderCanvas: ctx.renderCanvas,
100
164
  renderLeftPanel: render,
101
- defCategory: _ctx.defCategory,
102
- defBadgeLabel: _ctx.defBadgeLabel,
165
+ defCategory: ctx.defCategory,
166
+ defBadgeLabel: ctx.defBadgeLabel,
103
167
  });
104
168
  else if (tab === "head") {
105
169
  const isContent = S.mode === "content";
106
170
  const fm = S.content?.frontmatter ?? {};
107
171
  const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
108
- content = _ctx.renderHeadTemplate({
172
+ content = ctx.renderHeadTemplate({
109
173
  document: headDoc,
110
174
  applyMutation: isContent
111
- ? (/** @type {any} */ fn) => {
175
+ ? (/** @type {(doc: object) => void} */ fn) => {
176
+ const tab = activeTab.value;
177
+ const fm = /** @type {Record<string, any>} */ (tab.doc.content?.frontmatter ?? {});
112
178
  const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
113
179
  fn(tmp);
114
- let s = getState();
115
- if (tmp.title !== fm.title) s = updateFrontmatter(s, "title", tmp.title);
180
+ if (tmp.title !== fm.title)
181
+ mutateUpdateFrontmatter(tab, "title", /** @type {any} */ (tmp.title));
116
182
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
117
- s = updateFrontmatter(s, "$head", newHead);
118
- update(s);
183
+ mutateUpdateFrontmatter(tab, "$head", newHead);
119
184
  }
120
- : (/** @type {any} */ fn) => {
121
- update(applyMutation(getState(), fn));
185
+ : (/** @type {(doc: object) => void} */ fn) => {
186
+ transact(activeTab.value, fn);
122
187
  },
123
188
  renderLeftPanel: render,
124
189
  });
125
- } else if (tab === "git") content = _ctx.renderGitPanel(S);
190
+ } else if (tab === "git") content = ctx.renderGitPanel(S, ctx);
126
191
  else content = nothing;
127
192
 
128
- litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
193
+ litRender(html`<div class="panel-body">${content}</div>`, leftPanel);
129
194
 
130
195
  // Post-render side effects
131
- if (tab === "layers" && _ctx.getCanvasMode() !== "settings") _ctx.registerLayersDnD();
196
+ if (tab === "layers" && ctx.getCanvasMode() !== "settings") ctx.registerLayersDnD();
132
197
  else if (tab === "imports") {
133
198
  /* no post-render DnD needed */
134
199
  } else if (tab === "blocks") {
135
- _ctx.registerElementsDnD();
136
- _ctx.registerComponentsDnD();
200
+ ctx.registerElementsDnD();
201
+ ctx.registerComponentsDnD();
137
202
  } else if (tab === "files") {
138
- const tree = /** @type {any} */ (leftPanel)?.querySelector(".file-tree");
139
- if (tree) _ctx.setupTreeKeyboard(tree);
203
+ const tree = /** @type {HTMLElement | null} */ (leftPanel.querySelector(".file-tree"));
204
+ if (tree) ctx.setupTreeKeyboard(tree);
140
205
  }
141
206
  }
@@ -4,42 +4,83 @@
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
- import { getState, canvasPanels, pathsEqual, subscribe } from "../store.js";
7
+ import { canvasPanels, pathsEqual } from "../store.js";
8
+ import { effect, effectScope } from "../reactivity.js";
9
+ import { activeTab } from "../workspace/workspace.js";
8
10
  import { view } from "../view.js";
9
- import {
10
- findCanvasElement,
11
- getActivePanel,
12
- overlayBoxDescriptor,
13
- } from "../canvas/canvas-helpers.js";
11
+ import { findCanvasElement, getActivePanel, effectiveZoom } from "../canvas/canvas-helpers.js";
14
12
  import { layoutElements } from "../canvas/canvas-live-render.js";
15
13
 
16
- /** @type {any} */
14
+ /**
15
+ * @typedef {{
16
+ * cls: string;
17
+ * top: string;
18
+ * left: string;
19
+ * width: string;
20
+ * height: string;
21
+ * border?: string;
22
+ * isLayout?: boolean;
23
+ * }} OverlayBox
24
+ */
25
+
26
+ /**
27
+ * @typedef {{
28
+ * getCanvasMode: () => string;
29
+ * isEditing: () => boolean;
30
+ * renderBlockActionBar: () => void;
31
+ * }} OverlaysCtx
32
+ */
33
+
34
+ /** @type {OverlaysCtx | null} */
17
35
  let _ctx = null;
18
36
 
19
- /** @type {(() => void) | null} */
20
- let _unsub = null;
37
+ /** @type {import("@vue/reactivity").EffectScope | null} */
38
+ let _scope = null;
39
+
40
+ let _scheduled = false;
21
41
 
22
42
  /**
23
43
  * Mount the overlays panel.
24
44
  *
25
- * @param {any} ctx — { getCanvasMode, isEditing, renderBlockActionBar }
45
+ * @param {OverlaysCtx} ctx
26
46
  */
27
47
  export function mount(ctx) {
28
48
  _ctx = ctx;
29
- _unsub = subscribe((change) => {
30
- if (change.selection || change.hover || change.mode || change.ui || change.doc) render();
49
+ _scope = effectScope();
50
+ _scope.run(() => {
51
+ effect(() => {
52
+ const tab = activeTab.value;
53
+ if (!tab) return;
54
+ // Track selection, hover, and mode
55
+ void tab.session.selection;
56
+ void tab.session.hover;
57
+ void tab.doc.mode;
58
+ render();
59
+ });
31
60
  });
32
61
  }
33
62
 
34
63
  export function unmount() {
35
- _unsub?.();
36
- _unsub = null;
64
+ _scope?.stop();
65
+ _scope = null;
37
66
  _ctx = null;
38
67
  }
39
68
 
40
69
  export function render() {
41
70
  if (!_ctx) return;
42
- const S = getState();
71
+ if (!_scheduled) {
72
+ _scheduled = true;
73
+ queueMicrotask(_flush);
74
+ }
75
+ }
76
+
77
+ function _flush() {
78
+ _scheduled = false;
79
+ if (!_ctx) return;
80
+ const tab = activeTab.value;
81
+ if (!tab) return;
82
+ const { selection, hover } = tab.session;
83
+ const { stylebookTab } = tab.session.ui;
43
84
  const canvasMode = _ctx.getCanvasMode();
44
85
 
45
86
  if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "settings") {
@@ -55,7 +96,7 @@ export function render() {
55
96
  }
56
97
 
57
98
  if (canvasMode === "settings") {
58
- const enable = S.ui.stylebookTab === "elements";
99
+ const enable = stylebookTab === "elements";
59
100
  for (const p of canvasPanels) {
60
101
  p.overlayClk.style.pointerEvents = enable ? "" : "none";
61
102
  }
@@ -72,33 +113,46 @@ export function render() {
72
113
  }
73
114
 
74
115
  for (const p of canvasPanels) {
75
- /**
76
- * @type {{
77
- * cls: string;
78
- * top: string;
79
- * left: string;
80
- * width: string;
81
- * height: string;
82
- * border?: string;
83
- * }[]}
84
- */
116
+ /** @type {OverlayBox[]} */
85
117
  const boxes = [];
86
118
 
87
- if (S.hover && !pathsEqual(S.hover, S.selection)) {
88
- const el = findCanvasElement(S.hover, p.canvas);
119
+ // Batch layout reads: read viewport geometry once per panel
120
+ const vpRect = p.viewport.getBoundingClientRect();
121
+ const scrollTop = p.viewport.scrollTop;
122
+ const scrollLeft = p.viewport.scrollLeft;
123
+ const scale = effectiveZoom();
124
+
125
+ if (hover && !pathsEqual(hover, selection)) {
126
+ const el = findCanvasElement(hover, p.canvas);
89
127
  if (el) {
90
- const desc = overlayBoxDescriptor(el, "hover", p);
91
- if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
128
+ const elRect = el.getBoundingClientRect();
129
+ /** @type {OverlayBox} */
130
+ const desc = {
131
+ cls: "overlay-box overlay-hover",
132
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
133
+ left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
134
+ width: `${elRect.width / scale}px`,
135
+ height: `${elRect.height / scale}px`,
136
+ };
137
+ if (layoutElements.has(el)) desc.isLayout = true;
92
138
  boxes.push(desc);
93
139
  }
94
140
  }
95
141
 
96
- if (S.selection && p === getActivePanel()) {
97
- const el = findCanvasElement(S.selection, p.canvas);
142
+ if (selection && p === getActivePanel()) {
143
+ const el = findCanvasElement(selection, p.canvas);
98
144
  if (el) {
99
- const desc = overlayBoxDescriptor(el, "selection", p);
100
- if (view.componentInlineEdit || _ctx.isEditing()) /** @type {any} */ (desc).border = "none";
101
- if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
145
+ const elRect = el.getBoundingClientRect();
146
+ /** @type {OverlayBox} */
147
+ const desc = {
148
+ cls: "overlay-box overlay-selection",
149
+ top: `${(elRect.top - vpRect.top + scrollTop) / scale}px`,
150
+ left: `${(elRect.left - vpRect.left + scrollLeft) / scale}px`,
151
+ width: `${elRect.width / scale}px`,
152
+ height: `${elRect.height / scale}px`,
153
+ };
154
+ if (view.componentInlineEdit || _ctx.isEditing()) desc.border = "none";
155
+ if (layoutElements.has(el)) desc.isLayout = true;
102
156
  boxes.push(desc);
103
157
  }
104
158
  }
@@ -109,16 +163,12 @@ export function render() {
109
163
  ${boxes.map(
110
164
  (b) => html`
111
165
  <div
112
- class="${b.cls}${/** @type {any} */ (b).isLayout ? " overlay-layout" : ""}"
166
+ class="${b.cls}${b.isLayout ? " overlay-layout" : ""}"
113
167
  style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}${b.border
114
168
  ? `;border:${b.border}`
115
169
  : ""}"
116
170
  >
117
- ${
118
- /** @type {any} */ (b).isLayout
119
- ? html`<span class="overlay-layout-badge">Layout</span>`
120
- : nothing
121
- }
171
+ ${b.isLayout ? html`<span class="overlay-layout-badge">Layout</span>` : nothing}
122
172
  </div>
123
173
  `,
124
174
  )}
@@ -5,18 +5,15 @@
5
5
  */
6
6
 
7
7
  import {
8
- update,
9
8
  updateUi,
10
- selectNode,
11
- hoverNode,
12
9
  elToPath,
13
10
  pathsEqual,
14
- insertNode,
15
11
  parentElementPath,
16
12
  childIndex,
17
13
  getNodeAtPath,
18
14
  renderOnly,
19
15
  } from "../store.js";
16
+ import { activeTab } from "../workspace/workspace.js";
20
17
  import { view } from "../view.js";
21
18
  import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit.js";
22
19
  import { showContextMenu } from "../editor/context-menu.js";
@@ -32,8 +29,6 @@ let _ctx = null;
32
29
  * Initialize the panel events module.
33
30
  *
34
31
  * @param {{
35
- * getState: () => any;
36
- * setState: (s: any) => void;
37
32
  * getCanvasMode: () => string;
38
33
  * enterInlineEdit: (el: any, path: any) => void;
39
34
  * navigateToComponent: (path: any) => void;
@@ -79,7 +74,7 @@ export function registerPanelEvents(panel) {
79
74
  stopEditing();
80
75
  }
81
76
 
82
- const S = _ctx.getState();
77
+ const tab = activeTab.value;
83
78
  const canvasMode = _ctx.getCanvasMode();
84
79
 
85
80
  const elements = withPanelPointerEvents(() =>
@@ -91,7 +86,7 @@ export function registerPanelEvents(panel) {
91
86
  // Layout element clicked — show layout info instead of selecting in page doc
92
87
  if (layoutElements.has(el)) {
93
88
  view.layoutSelection = { el, layoutPath: activeLayoutPath };
94
- update(selectNode(S, null));
89
+ activeTab.value.session.selection = null;
95
90
  renderOnly("rightPanel");
96
91
  return;
97
92
  }
@@ -99,34 +94,35 @@ export function registerPanelEvents(panel) {
99
94
 
100
95
  const originalPath = elToPath.get(el);
101
96
  if (originalPath) {
102
- let path = bubbleInlinePath(S.document, originalPath);
97
+ let path = bubbleInlinePath(tab?.doc.document, originalPath);
103
98
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
104
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
105
99
 
106
100
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
107
101
 
108
102
  if (
109
- pathsEqual(path, S.selection) &&
103
+ pathsEqual(path, tab?.session.selection) &&
110
104
  isEditableBlock(resolvedEl) &&
111
- (canvasMode === "edit" || S.mode === "content")
105
+ (canvasMode === "edit" || tab?.doc.mode === "content")
112
106
  ) {
113
- _ctx.setState(withMedia);
107
+ activeTab.value.session.ui.activeMedia = newMedia;
114
108
  _ctx.enterInlineEdit(resolvedEl, path);
115
109
  return;
116
110
  }
117
111
 
118
- if (canvasMode === "design" && S.mode !== "content") {
112
+ if (canvasMode === "design" && tab?.doc.mode !== "content") {
119
113
  updateUi("pendingInlineEdit", { path, mediaName });
120
- update(selectNode(withMedia, path));
114
+ activeTab.value.session.ui.activeMedia = newMedia;
115
+ activeTab.value.session.selection = path;
121
116
  return;
122
117
  }
123
118
 
124
- update(selectNode(withMedia, path));
119
+ activeTab.value.session.ui.activeMedia = newMedia;
120
+ activeTab.value.session.selection = path;
125
121
  return;
126
122
  }
127
123
  }
128
124
  }
129
- update(selectNode(S, null));
125
+ activeTab.value.session.selection = null;
130
126
  },
131
127
  opts,
132
128
  );
@@ -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,12 +153,12 @@ 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);
164
- const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
165
- update(selectNode(withMedia, path));
160
+ activeTab.value.session.ui.activeMedia = newMedia;
161
+ activeTab.value.session.selection = path;
166
162
  _ctx.enterInlineEdit(resolvedEl, path);
167
163
  return;
168
164
  }
@@ -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,14 +247,10 @@ export function registerPanelEvents(panel) {
252
247
  );
253
248
 
254
249
  insertionHelper.mount({
255
- getState: _ctx.getState,
256
- update,
257
250
  getCanvasMode: _ctx.getCanvasMode,
258
251
  withPanelPointerEvents,
259
252
  effectiveZoom: effectiveZoom,
260
253
  defaultDef,
261
- insertNode,
262
- selectNode,
263
254
  parentElementPath,
264
255
  childIndex,
265
256
  getNodeAtPath,
@@ -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);