@jxsuite/studio 0.11.0 → 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 (47) hide show
  1. package/dist/studio.js +4248 -3412
  2. package/dist/studio.js.map +49 -43
  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 +3 -2
  7. package/src/canvas/canvas-render.js +152 -20
  8. package/src/canvas/canvas-utils.js +21 -23
  9. package/src/editor/component-inline-edit.js +54 -41
  10. package/src/editor/content-inline-edit.js +46 -47
  11. package/src/editor/context-menu.js +63 -39
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -10
  14. package/src/editor/shortcuts.js +69 -39
  15. package/src/files/components.js +15 -4
  16. package/src/files/files.js +72 -24
  17. package/src/panels/activity-bar.js +29 -7
  18. package/src/panels/block-action-bar.js +104 -80
  19. package/src/panels/dnd.js +32 -28
  20. package/src/panels/editors.js +7 -14
  21. package/src/panels/elements-panel.js +9 -3
  22. package/src/panels/events-panel.js +44 -39
  23. package/src/panels/git-panel.js +45 -3
  24. package/src/panels/layers-panel.js +16 -11
  25. package/src/panels/left-panel.js +108 -43
  26. package/src/panels/overlays.js +91 -41
  27. package/src/panels/panel-events.js +9 -12
  28. package/src/panels/properties-panel.js +179 -104
  29. package/src/panels/right-panel.js +85 -37
  30. package/src/panels/shared.js +0 -22
  31. package/src/panels/signals-panel.js +125 -54
  32. package/src/panels/statusbar.js +23 -8
  33. package/src/panels/style-inputs.js +4 -2
  34. package/src/panels/style-panel.js +128 -105
  35. package/src/panels/stylebook-panel.js +11 -15
  36. package/src/panels/tab-strip.js +124 -0
  37. package/src/panels/toolbar.js +39 -9
  38. package/src/reactivity.js +28 -0
  39. package/src/settings/content-types-editor.js +56 -7
  40. package/src/settings/defs-editor.js +56 -7
  41. package/src/settings/schema-field-ui.js +60 -25
  42. package/src/state.js +0 -456
  43. package/src/store.js +97 -124
  44. package/src/studio.js +112 -121
  45. package/src/tabs/tab.js +153 -0
  46. package/src/tabs/transact.js +406 -0
  47. package/src/workspace/workspace.js +89 -0
@@ -1,6 +1,8 @@
1
- import { getNodeAtPath, updateProperty, update } from "../store.js";
1
+ import { getNodeAtPath } from "../store.js";
2
2
  import { html, nothing } from "lit-html";
3
3
  import { live } from "lit-html/directives/live.js";
4
+ import { activeTab } from "../workspace/workspace.js";
5
+ import { transactDoc, mutateUpdateProperty } from "../tabs/transact.js";
4
6
 
5
7
  export const EVENT_NAMES = [
6
8
  "onclick",
@@ -15,17 +17,17 @@ export const EVENT_NAMES = [
15
17
  "onmouseleave",
16
18
  ];
17
19
 
18
- /**
19
- * @param {any} S - Studio state
20
- * @param {{ isCustomElementDoc: () => boolean; renderCanvas: () => void }} helpers
21
- */
22
- export function eventsSidebarTemplate(S, helpers) {
23
- const { isCustomElementDoc, renderCanvas } = helpers;
24
- if (!S.selection) return html`<div class="empty-state">Select an element to edit events</div>`;
25
- const node = getNodeAtPath(S.document, S.selection);
20
+ /** @param {{ isCustomElementDoc: () => boolean }} helpers */
21
+ export function eventsSidebarTemplate(helpers) {
22
+ const { isCustomElementDoc } = helpers;
23
+ const tab = activeTab.value;
24
+ const selection = tab?.session.selection;
25
+ const document = tab?.doc.document;
26
+ if (!selection) return html`<div class="empty-state">Select an element to edit events</div>`;
27
+ const node = getNodeAtPath(document, selection);
26
28
  if (!node) return html`<div class="empty-state">Node not found</div>`;
27
29
 
28
- const defs = S.document.state || {};
30
+ const defs = document.state || {};
29
31
  const functionDefs = Object.entries(defs).filter(
30
32
  ([, d]) => d.$prototype === "Function" || d.$handler,
31
33
  );
@@ -87,9 +89,10 @@ export function eventsSidebarTemplate(S, helpers) {
87
89
  @change=${(/** @type {any} */ e) => {
88
90
  const newKey = e.target.value;
89
91
  if (newKey && newKey !== evKey) {
90
- let s = updateProperty(S, S.selection, evKey, undefined);
91
- s = updateProperty(s, S.selection, newKey, node[evKey]);
92
- update(s);
92
+ transactDoc(activeTab.value, (t) => {
93
+ mutateUpdateProperty(t, selection, evKey, undefined);
94
+ mutateUpdateProperty(t, selection, newKey, node[evKey]);
95
+ });
93
96
  }
94
97
  }}
95
98
  >
@@ -103,8 +106,8 @@ export function eventsSidebarTemplate(S, helpers) {
103
106
  .value=${live(isInline ? "inline" : "ref")}
104
107
  @change=${(/** @type {any} */ e) => {
105
108
  if (e.target.value === "inline") {
106
- update(
107
- updateProperty(S, S.selection, evKey, {
109
+ transactDoc(activeTab.value, (t) =>
110
+ mutateUpdateProperty(t, selection, evKey, {
108
111
  $prototype: "Function",
109
112
  body: "",
110
113
  parameters: [],
@@ -112,10 +115,10 @@ export function eventsSidebarTemplate(S, helpers) {
112
115
  );
113
116
  } else {
114
117
  const firstFn = functionDefs[0];
115
- update(
116
- updateProperty(
117
- S,
118
- S.selection,
118
+ transactDoc(activeTab.value, (t) =>
119
+ mutateUpdateProperty(
120
+ t,
121
+ selection,
119
122
  evKey,
120
123
  firstFn ? { $ref: `#/state/${firstFn[0]}` } : { $ref: "" },
121
124
  ),
@@ -129,7 +132,10 @@ export function eventsSidebarTemplate(S, helpers) {
129
132
  <sp-action-button
130
133
  size="xs"
131
134
  quiet
132
- @click=${() => update(updateProperty(S, S.selection, evKey, undefined))}
135
+ @click=${() =>
136
+ transactDoc(activeTab.value, (t) =>
137
+ mutateUpdateProperty(t, selection, evKey, undefined),
138
+ )}
133
139
  >
134
140
  <sp-icon-delete slot="icon"></sp-icon-delete>
135
141
  </sp-action-button>
@@ -144,8 +150,8 @@ export function eventsSidebarTemplate(S, helpers) {
144
150
  placeholder="// handler body"
145
151
  .value=${live(evVal.body || "")}
146
152
  @input=${(/** @type {any} */ e) => {
147
- update(
148
- updateProperty(S, S.selection, evKey, {
153
+ transactDoc(activeTab.value, (t) =>
154
+ mutateUpdateProperty(t, selection, evKey, {
149
155
  $prototype: "Function",
150
156
  body: e.target.value,
151
157
  parameters: evVal.parameters || [],
@@ -159,18 +165,11 @@ export function eventsSidebarTemplate(S, helpers) {
159
165
  quiet
160
166
  title="Open in editor"
161
167
  @click=${() => {
162
- S = {
163
- ...S,
164
- ui: {
165
- ...S.ui,
166
- editingFunction: {
167
- type: "event",
168
- path: S.selection,
169
- eventKey: evKey,
170
- },
171
- },
168
+ tab.session.ui.editingFunction = {
169
+ type: "event",
170
+ path: selection,
171
+ eventKey: evKey,
172
172
  };
173
- renderCanvas();
174
173
  }}
175
174
  >
176
175
  <sp-icon-code slot="icon"></sp-icon-code>
@@ -184,9 +183,13 @@ export function eventsSidebarTemplate(S, helpers) {
184
183
  .value=${live(evVal.$ref || "__none__")}
185
184
  @change=${(/** @type {any} */ e) => {
186
185
  if (e.target.value && e.target.value !== "__none__") {
187
- update(updateProperty(S, S.selection, evKey, { $ref: e.target.value }));
186
+ transactDoc(activeTab.value, (t) =>
187
+ mutateUpdateProperty(t, selection, evKey, { $ref: e.target.value }),
188
+ );
188
189
  } else {
189
- update(updateProperty(S, S.selection, evKey, undefined));
190
+ transactDoc(activeTab.value, (t) =>
191
+ mutateUpdateProperty(t, selection, evKey, undefined),
192
+ );
190
193
  }
191
194
  }}
192
195
  >
@@ -212,12 +215,14 @@ export function eventsSidebarTemplate(S, helpers) {
212
215
  }
213
216
  }
214
217
  if (functionDefs.length > 0) {
215
- update(
216
- updateProperty(S, S.selection, evName, { $ref: `#/state/${functionDefs[0][0]}` }),
218
+ transactDoc(activeTab.value, (t) =>
219
+ mutateUpdateProperty(t, selection, evName, {
220
+ $ref: `#/state/${functionDefs[0][0]}`,
221
+ }),
217
222
  );
218
223
  } else {
219
- update(
220
- updateProperty(S, S.selection, evName, {
224
+ transactDoc(activeTab.value, (t) =>
225
+ mutateUpdateProperty(t, selection, evName, {
221
226
  $prototype: "Function",
222
227
  body: "",
223
228
  parameters: [],
@@ -44,8 +44,11 @@ async function gitAction(action, body) {
44
44
 
45
45
  let _pollTimer = /** @type {any} */ (null);
46
46
 
47
- /** @param {any} S */
48
- export function renderGitPanel(S) {
47
+ /**
48
+ * @param {any} S
49
+ * @param {any} ctx
50
+ */
51
+ export function renderGitPanel(S, ctx) {
49
52
  const status = S.ui.gitStatus;
50
53
  const branches = S.ui.gitBranches;
51
54
  const loading = S.ui.gitLoading;
@@ -156,9 +159,48 @@ export function renderGitPanel(S) {
156
159
  const parts = file.path.split("/");
157
160
  const name = parts.pop();
158
161
  const dir = parts.join("/");
162
+
163
+ const onFileClick = async () => {
164
+ // Only support diff for modified/added files
165
+ if (file.status !== "M" && file.status !== "A") return;
166
+ if (!file.path.endsWith(".md") && !file.path.endsWith(".json")) return;
167
+
168
+ try {
169
+ const plat = getPlatform();
170
+ updateUi("gitLoading", true);
171
+ const originalContent = await plat.gitShow({ path: file.path, ref: "HEAD" });
172
+
173
+ // Determine if it's markdown or JSON
174
+ const isMarkdown = file.path.endsWith(".md");
175
+
176
+ // For now, we'll need to pass the original content to the canvas
177
+ // This requires state management which we'll add to studio.js
178
+ updateUi("gitDiffState", {
179
+ filePath: file.path,
180
+ originalContent,
181
+ isMarkdown,
182
+ fileStatus: file.status,
183
+ });
184
+
185
+ // Trigger diff mode via context
186
+ if (ctx?.setCanvasMode) {
187
+ ctx.setCanvasMode("git-diff");
188
+ }
189
+ } catch (/** @type {any} */ e) {
190
+ updateUi("gitError", `Failed to load diff: ${e.message}`);
191
+ } finally {
192
+ updateUi("gitLoading", false);
193
+ }
194
+ };
195
+
159
196
  return html`
160
197
  <div class="git-file-row">
161
- <span class="git-file-info">
198
+ <span
199
+ class="git-file-info"
200
+ style="cursor: pointer; flex: 1;"
201
+ title="Click to view diff"
202
+ @click=${onFileClick}
203
+ >
162
204
  <span class="git-file-name" title=${file.path}>${name}</span>
163
205
  ${dir ? html`<span class="git-file-dir">${dir}</span>` : nothing}
164
206
  </span>
@@ -6,7 +6,6 @@
6
6
  import { html, nothing } from "lit-html";
7
7
  import {
8
8
  getState,
9
- update,
10
9
  flattenTree,
11
10
  getNodeAtPath,
12
11
  pathKey,
@@ -14,11 +13,10 @@ import {
14
13
  parentElementPath,
15
14
  childIndex,
16
15
  nodeLabel,
17
- selectNode,
18
- moveNode,
19
- removeNode,
20
16
  VOID_ELEMENTS,
21
17
  } from "../store.js";
18
+ import { activeTab } from "../workspace/workspace.js";
19
+ import { transactDoc, mutateMoveNode, mutateRemoveNode } from "../tabs/transact.js";
22
20
  import { view } from "../view.js";
23
21
  import { isInlineElement } from "../editor/inline-edit.js";
24
22
  import { showContextMenu } from "../editor/context-menu.js";
@@ -34,7 +32,8 @@ export function renderLayersTemplate(ctx) {
34
32
  view.dndCleanups = [];
35
33
 
36
34
  const rows = flattenTree(S.document);
37
- const collapsed = S._collapsed || (S._collapsed = new Set());
35
+ const _S = /** @type {any} */ (S);
36
+ const collapsed = _S._collapsed || (_S._collapsed = new Set());
38
37
 
39
38
  /** @type {any[]} */
40
39
  const layerRows = [];
@@ -142,7 +141,7 @@ export function renderLayersTemplate(ctx) {
142
141
  data-dnd-row=${isElement ? key : nothing}
143
142
  data-dnd-depth=${isElement ? depth : nothing}
144
143
  data-dnd-void=${isElement && isVoidEl ? "" : nothing}
145
- @click=${() => update(selectNode(getState(), path))}
144
+ @click=${() => (activeTab.value.session.selection = path)}
146
145
  @contextmenu=${isElement
147
146
  ? (/** @type {any} */ e) =>
148
147
  showContextMenu(e, path, getState(), {
@@ -175,7 +174,9 @@ export function renderLayersTemplate(ctx) {
175
174
  @click=${(/** @type {any} */ e) => {
176
175
  e.stopPropagation();
177
176
  /** @type {HTMLElement} */ (e.currentTarget).blur();
178
- update(moveNode(getState(), path, parentPath, idx - 1));
177
+ transactDoc(activeTab.value, (t) =>
178
+ mutateMoveNode(t, path, parentPath, idx - 1),
179
+ );
179
180
  }}
180
181
  >
181
182
  <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
@@ -189,7 +190,9 @@ export function renderLayersTemplate(ctx) {
189
190
  @click=${(/** @type {any} */ e) => {
190
191
  e.stopPropagation();
191
192
  /** @type {HTMLElement} */ (e.currentTarget).blur();
192
- update(moveNode(getState(), path, parentPath, idx + 2));
193
+ transactDoc(activeTab.value, (t) =>
194
+ mutateMoveNode(t, path, parentPath, idx + 2),
195
+ );
193
196
  }}
194
197
  >
195
198
  <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
@@ -206,7 +209,7 @@ export function renderLayersTemplate(ctx) {
206
209
  const prevPath = [...parentPath, idx - 1];
207
210
  const prev = getNodeAtPath(getState().document, prevPath);
208
211
  const len = prev?.children?.length || 0;
209
- update(moveNode(getState(), path, prevPath, len));
212
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, path, prevPath, len));
210
213
  }}
211
214
  >
212
215
  <sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
@@ -221,7 +224,9 @@ export function renderLayersTemplate(ctx) {
221
224
  e.stopPropagation();
222
225
  /** @type {HTMLElement} */ (e.currentTarget).blur();
223
226
  const parentIdx = /** @type {number} */ (childIndex(parentPath));
224
- update(moveNode(getState(), path, grandparentPath, parentIdx + 1));
227
+ transactDoc(activeTab.value, (t) =>
228
+ mutateMoveNode(t, path, grandparentPath, parentIdx + 1),
229
+ );
225
230
  }}
226
231
  >
227
232
  <sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
@@ -234,7 +239,7 @@ export function renderLayersTemplate(ctx) {
234
239
  title="Delete"
235
240
  @click=${(/** @type {any} */ e) => {
236
241
  e.stopPropagation();
237
- update(removeNode(getState(), path));
242
+ transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path));
238
243
  }}
239
244
  >
240
245
  <sp-icon-close slot="icon"></sp-icon-close>
@@ -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 { transact, mutateUpdateFrontmatter } from "../tabs/transact.js";
14
+
19
15
  import { renderLayersTemplate } from "./layers-panel.js";
20
16
  import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
21
17
  import { renderElementsTemplate } from "./elements-panel.js";
22
18
  import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
23
19
 
24
- /** @type {any} */
20
+ /** @typedef {import("lit-html").TemplateResult} TemplateResult */
21
+
22
+ /**
23
+ * @typedef {{
24
+ * getCanvasMode: () => string;
25
+ * setCanvasMode: (mode: string) => void;
26
+ * renderImportsTemplate: (...args: any[]) => TemplateResult;
27
+ * renderFilesTemplate: () => TemplateResult;
28
+ * renderSignalsTemplate: (...args: any[]) => TemplateResult;
29
+ * renderDataExplorerTemplate: (...args: any[]) => TemplateResult;
30
+ * renderHeadTemplate: (...args: any[]) => TemplateResult;
31
+ * renderGitPanel: (...args: any[]) => TemplateResult;
32
+ * renderCanvas: () => void;
33
+ * defCategory: (tag: string) => string;
34
+ * defBadgeLabel: (tag: string) => string;
35
+ * navigateToComponent: (path: string) => void;
36
+ * webdata: object;
37
+ * defaultDef: (tag: string) => object;
38
+ * registerLayersDnD: () => void;
39
+ * registerElementsDnD: () => void;
40
+ * registerComponentsDnD: () => void;
41
+ * setupTreeKeyboard: (tree: HTMLElement) => void;
42
+ * }} LeftPanelCtx
43
+ */
44
+
45
+ /** @type {LeftPanelCtx | null} */
25
46
  let _ctx = null;
26
47
 
48
+ /** @type {import("@vue/reactivity").EffectScope | null} */
49
+ let _scope = null;
50
+
51
+ let _rendering = false;
52
+ let _scheduled = false;
53
+
27
54
  /**
28
55
  * Mount the left panel orchestrator.
29
56
  *
30
- * @param {any} ctx — callbacks and references that avoid circular dependencies
57
+ * @param {LeftPanelCtx} ctx
31
58
  */
32
59
  export function mount(ctx) {
33
60
  _ctx = ctx;
61
+ _scope = effectScope();
62
+ _scope.run(() => {
63
+ effect(() => {
64
+ const tab = activeTab.value;
65
+ if (!tab) return;
66
+ // Track properties the left panel reads
67
+ void tab.doc.document;
68
+ void tab.doc.mode;
69
+ void tab.session.selection;
70
+ void tab.session.ui.leftTab;
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();
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
+ });
60
124
  const tab = S.ui.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
  }