@jxsuite/studio 0.6.2 → 0.8.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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Git panel — Source control sidebar with status, staging, commit, push/pull, and branch
3
+ * management.
4
+ */
5
+
6
+ import { html, nothing } from "lit-html";
7
+ import { live } from "lit-html/directives/live.js";
8
+ import { getPlatform } from "../platform.js";
9
+ import { updateUi, renderOnly } from "../store.js";
10
+
11
+ async function refreshGitStatus() {
12
+ const plat = getPlatform();
13
+ updateUi("gitLoading", true);
14
+ updateUi("gitError", null);
15
+ try {
16
+ const [status, branches] = await Promise.all([plat.gitStatus(), plat.gitBranches()]);
17
+ updateUi("gitStatus", status);
18
+ updateUi("gitBranches", branches);
19
+ } catch (/** @type {any} */ e) {
20
+ updateUi("gitError", e.message);
21
+ } finally {
22
+ updateUi("gitLoading", false);
23
+ renderOnly("leftPanel");
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @param {string} action
29
+ * @param {any} [body]
30
+ */
31
+ async function gitAction(action, body) {
32
+ const plat = getPlatform();
33
+ updateUi("gitLoading", true);
34
+ updateUi("gitError", null);
35
+ try {
36
+ await plat[action](body);
37
+ await refreshGitStatus();
38
+ } catch (/** @type {any} */ e) {
39
+ updateUi("gitError", e.message);
40
+ updateUi("gitLoading", false);
41
+ renderOnly("leftPanel");
42
+ }
43
+ }
44
+
45
+ let _pollTimer = /** @type {any} */ (null);
46
+
47
+ /** @param {any} S */
48
+ export function renderGitPanel(S) {
49
+ const status = S.ui.gitStatus;
50
+ const branches = S.ui.gitBranches;
51
+ const loading = S.ui.gitLoading;
52
+
53
+ if (!status && !loading) {
54
+ refreshGitStatus();
55
+ return html`<div class="git-panel"><div class="git-loading">Loading...</div></div>`;
56
+ }
57
+
58
+ if (!_pollTimer) {
59
+ _pollTimer = setInterval(() => {
60
+ if (S.ui.leftTab === "git" && !S.ui.gitLoading) refreshGitStatus();
61
+ }, 30000);
62
+ }
63
+
64
+ const stagedFiles = status?.files?.filter((/** @type {any} */ f) => f.staged) || [];
65
+ const unstagedFiles = status?.files?.filter((/** @type {any} */ f) => !f.staged) || [];
66
+ const totalChanges = status?.files?.length || 0;
67
+
68
+ const doCommit = async () => {
69
+ const msg = S.ui.gitCommitMessage?.trim();
70
+ if (!msg) return;
71
+ updateUi("gitCommitMessage", "");
72
+ await gitAction("gitCommit", msg);
73
+ };
74
+
75
+ const branchPickerT = html`
76
+ <sp-picker
77
+ size="s"
78
+ quiet
79
+ class="git-branch-picker"
80
+ .value=${live(branches?.current || "")}
81
+ @change=${async (/** @type {any} */ e) => {
82
+ const val = e.target.value;
83
+ if (val === "__new__") {
84
+ e.target.value = branches?.current || "";
85
+ const name = prompt("New branch name:");
86
+ if (name?.trim()) await gitAction("gitCreateBranch", name.trim());
87
+ return;
88
+ }
89
+ if (val !== branches?.current) await gitAction("gitCheckout", val);
90
+ }}
91
+ >
92
+ ${(branches?.branches || []).map(
93
+ (/** @type {string} */ b) => html`<sp-menu-item value=${b}>${b}</sp-menu-item>`,
94
+ )}
95
+ <sp-menu-divider></sp-menu-divider>
96
+ <sp-menu-item value="__new__">+ New branch...</sp-menu-item>
97
+ </sp-picker>
98
+ `;
99
+
100
+ const toolbarT = html`
101
+ <div class="git-toolbar">
102
+ ${branchPickerT}
103
+ <sp-action-group size="xs" quiet>
104
+ <sp-action-button title="Fetch" @click=${() => gitAction("gitFetch")} ?disabled=${loading}>
105
+ <sp-icon-download slot="icon" size="xs"></sp-icon-download>
106
+ </sp-action-button>
107
+ <sp-action-button
108
+ title="Pull${status?.behind ? ` (${status.behind} behind)` : ""}"
109
+ @click=${() => gitAction("gitPull")}
110
+ ?disabled=${loading}
111
+ >
112
+ <sp-icon-arrow-down slot="icon" size="xs"></sp-icon-arrow-down>
113
+ </sp-action-button>
114
+ <sp-action-button
115
+ title="Push${status?.ahead ? ` (${status.ahead} ahead)` : ""}"
116
+ @click=${() => gitAction("gitPush")}
117
+ ?disabled=${loading}
118
+ >
119
+ <sp-icon-arrow-up slot="icon" size="xs"></sp-icon-arrow-up>
120
+ </sp-action-button>
121
+ <sp-action-button title="Refresh" @click=${() => refreshGitStatus()} ?disabled=${loading}>
122
+ <sp-icon-refresh slot="icon" size="xs"></sp-icon-refresh>
123
+ </sp-action-button>
124
+ </sp-action-group>
125
+ </div>
126
+ `;
127
+
128
+ const commitT = html`
129
+ <div class="git-commit-area">
130
+ <sp-textfield
131
+ size="s"
132
+ multiline
133
+ class="git-commit-input"
134
+ placeholder='Message (Ctrl+Enter to commit on "${status?.branch || ""}")'
135
+ .value=${live(S.ui.gitCommitMessage || "")}
136
+ @input=${(/** @type {any} */ e) => updateUi("gitCommitMessage", e.target.value)}
137
+ @keydown=${(/** @type {any} */ e) => {
138
+ if (e.ctrlKey && e.key === "Enter") {
139
+ e.preventDefault();
140
+ doCommit();
141
+ }
142
+ }}
143
+ ></sp-textfield>
144
+ <sp-action-button
145
+ class="git-commit-btn"
146
+ @click=${doCommit}
147
+ ?disabled=${!S.ui.gitCommitMessage?.trim() || loading}
148
+ >
149
+ <sp-icon-checkmark slot="icon" size="xs"></sp-icon-checkmark>
150
+ Commit
151
+ </sp-action-button>
152
+ </div>
153
+ `;
154
+
155
+ const fileRowT = (/** @type {any} */ file) => {
156
+ const parts = file.path.split("/");
157
+ const name = parts.pop();
158
+ const dir = parts.join("/");
159
+ return html`
160
+ <div class="git-file-row">
161
+ <span class="git-file-info">
162
+ <span class="git-file-name" title=${file.path}>${name}</span>
163
+ ${dir ? html`<span class="git-file-dir">${dir}</span>` : nothing}
164
+ </span>
165
+ <span class="git-file-actions">
166
+ ${file.staged
167
+ ? html`
168
+ <sp-action-button
169
+ size="xs"
170
+ quiet
171
+ title="Unstage"
172
+ @click=${() => gitAction("gitUnstage", [file.path])}
173
+ >
174
+ <sp-icon-remove slot="icon" size="xs"></sp-icon-remove>
175
+ </sp-action-button>
176
+ `
177
+ : html`
178
+ <sp-action-button
179
+ size="xs"
180
+ quiet
181
+ title="Discard changes"
182
+ @click=${async () => {
183
+ if (file.status === "U") return;
184
+ if (!confirm(`Discard changes to ${file.path}?`)) return;
185
+ await gitAction("gitDiscard", [file.path]);
186
+ }}
187
+ ?disabled=${file.status === "U"}
188
+ >
189
+ <sp-icon-undo slot="icon" size="xs"></sp-icon-undo>
190
+ </sp-action-button>
191
+ <sp-action-button
192
+ size="xs"
193
+ quiet
194
+ title="Stage"
195
+ @click=${() => gitAction("gitStage", [file.path])}
196
+ >
197
+ <sp-icon-add slot="icon" size="xs"></sp-icon-add>
198
+ </sp-action-button>
199
+ `}
200
+ </span>
201
+ <span class="git-file-badge git-status-${file.status}">${file.status}</span>
202
+ </div>
203
+ `;
204
+ };
205
+
206
+ const changesT = html`
207
+ ${stagedFiles.length > 0
208
+ ? html`
209
+ <div class="git-section">
210
+ <div class="git-section-header">
211
+ <span>Staged Changes</span>
212
+ <span class="git-count">${stagedFiles.length}</span>
213
+ <sp-action-button
214
+ size="xs"
215
+ quiet
216
+ title="Unstage all"
217
+ @click=${() =>
218
+ gitAction(
219
+ "gitUnstage",
220
+ stagedFiles.map((/** @type {any} */ f) => f.path),
221
+ )}
222
+ >
223
+ <sp-icon-remove slot="icon" size="xs"></sp-icon-remove>
224
+ </sp-action-button>
225
+ </div>
226
+ ${stagedFiles.map(fileRowT)}
227
+ </div>
228
+ `
229
+ : nothing}
230
+ ${unstagedFiles.length > 0
231
+ ? html`
232
+ <div class="git-section">
233
+ <div class="git-section-header">
234
+ <span>Changes</span>
235
+ <span class="git-count">${unstagedFiles.length}</span>
236
+ <sp-action-button
237
+ size="xs"
238
+ quiet
239
+ title="Stage all"
240
+ @click=${() =>
241
+ gitAction(
242
+ "gitStage",
243
+ unstagedFiles.map((/** @type {any} */ f) => f.path),
244
+ )}
245
+ >
246
+ <sp-icon-add slot="icon" size="xs"></sp-icon-add>
247
+ </sp-action-button>
248
+ </div>
249
+ ${unstagedFiles.map(fileRowT)}
250
+ </div>
251
+ `
252
+ : nothing}
253
+ ${totalChanges === 0 && !loading ? html`<div class="git-empty">No changes</div>` : nothing}
254
+ `;
255
+
256
+ const syncInfoT =
257
+ status?.ahead || status?.behind
258
+ ? html`
259
+ <div class="git-sync-info">
260
+ ${status.ahead ? html`<span title="Commits ahead">↑${status.ahead}</span>` : nothing}
261
+ ${status.behind ? html`<span title="Commits behind">↓${status.behind}</span>` : nothing}
262
+ </div>
263
+ `
264
+ : nothing;
265
+
266
+ return html`
267
+ <div class="git-panel">
268
+ ${toolbarT} ${syncInfoT} ${commitT}
269
+ ${loading ? html`<div class="git-loading">Loading...</div>` : nothing}
270
+ ${S.ui.gitError ? html`<div class="git-error">${S.ui.gitError}</div>` : nothing} ${changesT}
271
+ </div>
272
+ `;
273
+ }
274
+
275
+ export function cleanupGitPanel() {
276
+ if (_pollTimer) {
277
+ clearInterval(_pollTimer);
278
+ _pollTimer = null;
279
+ }
280
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Layers panel — document tree view showing element hierarchy with collapse, selection, move
3
+ * actions, and drag-and-drop reordering.
4
+ */
5
+
6
+ import { html, nothing } from "lit-html";
7
+ import {
8
+ getState,
9
+ update,
10
+ flattenTree,
11
+ getNodeAtPath,
12
+ pathKey,
13
+ pathsEqual,
14
+ parentElementPath,
15
+ childIndex,
16
+ nodeLabel,
17
+ selectNode,
18
+ moveNode,
19
+ removeNode,
20
+ VOID_ELEMENTS,
21
+ } from "../store.js";
22
+ import { view } from "../view.js";
23
+ import { isInlineElement } from "../editor/inline-edit.js";
24
+ import { showContextMenu } from "../editor/context-menu.js";
25
+
26
+ /**
27
+ * @param {{ navigateToComponent: any; rerender: () => void }} ctx
28
+ * @returns {import("lit-html").TemplateResult}
29
+ */
30
+ export function renderLayersTemplate(ctx) {
31
+ const S = getState();
32
+
33
+ for (const fn of view.dndCleanups) fn();
34
+ view.dndCleanups = [];
35
+
36
+ const rows = flattenTree(S.document);
37
+ const collapsed = S._collapsed || (S._collapsed = new Set());
38
+
39
+ /** @type {any[]} */
40
+ const layerRows = [];
41
+ for (const { node, path, depth, nodeType } of rows) {
42
+ let hidden = false;
43
+ for (let d = 1; d <= path.length; d++) {
44
+ const sub = path.slice(0, d);
45
+ if (d < path.length && collapsed.has(pathKey(sub))) {
46
+ hidden = true;
47
+ break;
48
+ }
49
+ }
50
+ if (hidden) continue;
51
+
52
+ if (S.mode === "content" && path.length === 0) continue;
53
+
54
+ if (nodeType === "text") {
55
+ const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
56
+ layerRows.push(html`
57
+ <div
58
+ class="layer-row"
59
+ style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
60
+ >
61
+ <span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
62
+ <span class="layer-label">${textPreview}</span>
63
+ </div>
64
+ `);
65
+ continue;
66
+ }
67
+
68
+ if (path.length >= 2 && nodeType === "element") {
69
+ const pPath = parentElementPath(path);
70
+ const parentNode = pPath ? getNodeAtPath(S.document, pPath) : null;
71
+ if (parentNode && isInlineElement(node, parentNode)) continue;
72
+ }
73
+
74
+ const key = pathKey(path);
75
+ const isSelected = pathsEqual(path, S.selection);
76
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
77
+ const hasMapChildren =
78
+ node.children && typeof node.children === "object" && node.children.$prototype === "Array";
79
+ const hasCases =
80
+ node.$switch &&
81
+ node.cases &&
82
+ typeof node.cases === "object" &&
83
+ Object.keys(node.cases).length > 0;
84
+ const isExpandable =
85
+ hasChildren || hasMapChildren || hasCases || (nodeType === "map" && node.map);
86
+ const isVoidEl = VOID_ELEMENTS.has((node.tagName || "div").toLowerCase());
87
+
88
+ /** @type {any} */
89
+ let badgeClass, badgeText, badgeTitle;
90
+ if (nodeType === "map") {
91
+ badgeClass = "layer-tag map-tag";
92
+ badgeText = "↻";
93
+ badgeTitle = "Repeater (mapped array)";
94
+ } else if (nodeType === "case" || nodeType === "case-ref") {
95
+ badgeClass = "layer-tag case-tag";
96
+ badgeText = path[path.length - 1];
97
+ badgeTitle = `$switch case: ${path[path.length - 1]}`;
98
+ } else if (node.$switch) {
99
+ badgeClass = "layer-tag switch-tag";
100
+ badgeText = "⇄";
101
+ badgeTitle = "$switch";
102
+ } else {
103
+ badgeClass = "layer-tag";
104
+ badgeText = node.tagName || "div";
105
+ badgeTitle = undefined;
106
+ }
107
+
108
+ /** @type {any} */
109
+ let labelText, labelItalic;
110
+ if (nodeType === "case-ref") {
111
+ labelText = node.$ref || "external";
112
+ labelItalic = true;
113
+ } else {
114
+ labelText = nodeLabel(node);
115
+ labelItalic = false;
116
+ }
117
+
118
+ const isElement = nodeType === "element";
119
+ const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
120
+ const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
121
+ const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
122
+ const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
123
+ const siblingCount = parentNode?.children?.length || 0;
124
+ const canMoveUp = isElement && !isRoot && idx > 0;
125
+ const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
126
+ const prevSibling = canMoveUp && parentNode ? parentNode.children[idx - 1] : null;
127
+ const canMoveIn =
128
+ isElement &&
129
+ !isRoot &&
130
+ prevSibling &&
131
+ !VOID_ELEMENTS.has((prevSibling.tagName || "div").toLowerCase());
132
+ const grandparentPath =
133
+ isElement && parentPath && parentPath.length >= 2
134
+ ? /** @type {any} */ (parentElementPath(parentPath))
135
+ : null;
136
+ const canMoveOut = isElement && !isRoot && !!grandparentPath;
137
+
138
+ layerRows.push(html`
139
+ <div
140
+ class="layer-row${isSelected ? " selected" : ""}"
141
+ data-path=${key}
142
+ data-dnd-row=${isElement ? key : nothing}
143
+ data-dnd-depth=${isElement ? depth : nothing}
144
+ data-dnd-void=${isElement && isVoidEl ? "" : nothing}
145
+ @click=${() => update(selectNode(getState(), path))}
146
+ @contextmenu=${isElement
147
+ ? (/** @type {any} */ e) =>
148
+ showContextMenu(e, path, getState(), {
149
+ onEditComponent: ctx.navigateToComponent,
150
+ })
151
+ : nothing}
152
+ >
153
+ <span class="layer-indent" style="width:${depth * 16}px"></span>
154
+ <span class="layer-toggle"
155
+ >${isExpandable
156
+ ? html`
157
+ ${collapsed.has(key)
158
+ ? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
159
+ : html`<sp-icon-chevron-down></sp-icon-chevron-down>`}
160
+ `
161
+ : nothing}</span
162
+ >
163
+ <span class=${badgeClass} title=${badgeTitle ?? nothing}>${badgeText}</span>
164
+ <span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
165
+ >${labelText}</span
166
+ >
167
+ ${isElement && !isRoot
168
+ ? html`
169
+ <span class="layer-actions">
170
+ ${canMoveUp
171
+ ? html`<sp-action-button
172
+ quiet
173
+ size="xs"
174
+ title="Move up"
175
+ @click=${(/** @type {any} */ e) => {
176
+ e.stopPropagation();
177
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
178
+ update(moveNode(getState(), path, parentPath, idx - 1));
179
+ }}
180
+ >
181
+ <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
182
+ </sp-action-button>`
183
+ : nothing}
184
+ ${canMoveDown
185
+ ? html`<sp-action-button
186
+ quiet
187
+ size="xs"
188
+ title="Move down"
189
+ @click=${(/** @type {any} */ e) => {
190
+ e.stopPropagation();
191
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
192
+ update(moveNode(getState(), path, parentPath, idx + 2));
193
+ }}
194
+ >
195
+ <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
196
+ </sp-action-button>`
197
+ : nothing}
198
+ ${canMoveIn
199
+ ? html`<sp-action-button
200
+ quiet
201
+ size="xs"
202
+ title="Move into previous sibling"
203
+ @click=${(/** @type {any} */ e) => {
204
+ e.stopPropagation();
205
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
206
+ const prevPath = [...parentPath, idx - 1];
207
+ const prev = getNodeAtPath(getState().document, prevPath);
208
+ const len = prev?.children?.length || 0;
209
+ update(moveNode(getState(), path, prevPath, len));
210
+ }}
211
+ >
212
+ <sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
213
+ </sp-action-button>`
214
+ : nothing}
215
+ ${canMoveOut
216
+ ? html`<sp-action-button
217
+ quiet
218
+ size="xs"
219
+ title="Move out of parent"
220
+ @click=${(/** @type {any} */ e) => {
221
+ e.stopPropagation();
222
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
223
+ const parentIdx = /** @type {number} */ (childIndex(parentPath));
224
+ update(moveNode(getState(), path, grandparentPath, parentIdx + 1));
225
+ }}
226
+ >
227
+ <sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
228
+ </sp-action-button>`
229
+ : nothing}
230
+ <sp-action-button
231
+ quiet
232
+ size="xs"
233
+ class="layer-delete"
234
+ title="Delete"
235
+ @click=${(/** @type {any} */ e) => {
236
+ e.stopPropagation();
237
+ update(removeNode(getState(), path));
238
+ }}
239
+ >
240
+ <sp-icon-close slot="icon"></sp-icon-close>
241
+ </sp-action-button>
242
+ </span>
243
+ `
244
+ : nothing}
245
+ </div>
246
+ `);
247
+ }
248
+
249
+ return html`
250
+ <div class="layers-container" style="position:relative">
251
+ <div
252
+ class="layers-tree"
253
+ @click=${(/** @type {any} */ e) => {
254
+ const toggle = e.target.closest(".layer-toggle");
255
+ if (!toggle) return;
256
+ e.stopPropagation();
257
+ const row = toggle.closest(".layer-row");
258
+ if (!row) return;
259
+ const key = row.dataset.path;
260
+ if (!key) return;
261
+ if (collapsed.has(key)) collapsed.delete(key);
262
+ else collapsed.add(key);
263
+ ctx.rerender();
264
+ }}
265
+ >
266
+ ${layerRows}
267
+ </div>
268
+ </div>
269
+ `;
270
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Left panel — orchestrator that delegates to per-tab render functions.
3
+ *
4
+ * Each sub-panel exports a render function that takes its dependencies as arguments and returns a
5
+ * TemplateResult — the same pattern as imports-panel, signals-panel, etc. Only this orchestrator
6
+ * uses mount/render/unmount because it owns the DOM root and error boundary.
7
+ */
8
+
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 { view } from "../view.js";
19
+ import { ensureLitState } from "./shared.js";
20
+ import { renderLayersTemplate } from "./layers-panel.js";
21
+ import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
22
+ import { renderElementsTemplate } from "./elements-panel.js";
23
+ import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
24
+
25
+ /** @type {any} */
26
+ let _ctx = null;
27
+
28
+ /**
29
+ * Mount the left panel orchestrator.
30
+ *
31
+ * @param {any} ctx — callbacks and references that avoid circular dependencies
32
+ */
33
+ export function mount(ctx) {
34
+ _ctx = ctx;
35
+ }
36
+
37
+ export function unmount() {
38
+ _ctx = null;
39
+ }
40
+
41
+ export function render() {
42
+ if (!_ctx) return;
43
+ try {
44
+ ensureLitState(leftPanel);
45
+ _render();
46
+ } catch (e) {
47
+ console.error("left-panel render error:", e);
48
+ try {
49
+ leftPanel.textContent = "";
50
+ // @ts-ignore — clear Lit's internal state to recover from marker corruption
51
+ delete leftPanel["_$litPart$"];
52
+ _render();
53
+ } catch (e2) {
54
+ console.error("left-panel retry failed:", e2);
55
+ }
56
+ }
57
+ }
58
+
59
+ function _render() {
60
+ const S = getState();
61
+ const tab = S.ui.leftTab;
62
+
63
+ /** @type {any} */
64
+ let content;
65
+ if (tab === "layers")
66
+ content =
67
+ _ctx.getCanvasMode() === "settings"
68
+ ? renderStylebookLayersTemplate({
69
+ selectStylebookTag,
70
+ stylebookMeta,
71
+ })
72
+ : renderLayersTemplate({
73
+ navigateToComponent: _ctx.navigateToComponent,
74
+ rerender: render,
75
+ });
76
+ else if (tab === "imports")
77
+ content = _ctx.renderImportsTemplate({
78
+ renderLeftPanel: render,
79
+ documentPath: S.documentPath,
80
+ documentElements: S.document.$elements || [],
81
+ applyMutation: (/** @type {any} */ fn) => {
82
+ update(applyMutation(getState(), fn));
83
+ },
84
+ });
85
+ else if (tab === "files") content = _ctx.renderFilesTemplate();
86
+ else if (tab === "blocks")
87
+ content = renderElementsTemplate({
88
+ webdata: _ctx.webdata,
89
+ defaultDef: _ctx.defaultDef,
90
+ rerender: render,
91
+ });
92
+ else if (tab === "state")
93
+ content = _ctx.renderSignalsTemplate(S, {
94
+ renderLeftPanel: render,
95
+ renderCanvas: _ctx.renderCanvas,
96
+ updateSession,
97
+ });
98
+ else if (tab === "data")
99
+ content = _ctx.renderDataExplorerTemplate(S.document.state, view.liveScope, {
100
+ renderCanvas: _ctx.renderCanvas,
101
+ renderLeftPanel: render,
102
+ defCategory: _ctx.defCategory,
103
+ defBadgeLabel: _ctx.defBadgeLabel,
104
+ });
105
+ else if (tab === "head") {
106
+ const isContent = S.mode === "content";
107
+ const fm = S.content?.frontmatter ?? {};
108
+ const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
109
+ content = _ctx.renderHeadTemplate({
110
+ document: headDoc,
111
+ applyMutation: isContent
112
+ ? (/** @type {any} */ fn) => {
113
+ const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
114
+ fn(tmp);
115
+ let s = getState();
116
+ if (tmp.title !== fm.title) s = updateFrontmatter(s, "title", tmp.title);
117
+ const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
118
+ s = updateFrontmatter(s, "$head", newHead);
119
+ update(s);
120
+ }
121
+ : (/** @type {any} */ fn) => {
122
+ update(applyMutation(getState(), fn));
123
+ },
124
+ renderLeftPanel: render,
125
+ });
126
+ } else if (tab === "git") content = _ctx.renderGitPanel(S);
127
+ else content = nothing;
128
+
129
+ litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
130
+
131
+ // Post-render side effects
132
+ if (tab === "layers" && _ctx.getCanvasMode() !== "settings") _ctx.registerLayersDnD();
133
+ else if (tab === "imports") {
134
+ /* no post-render DnD needed */
135
+ } else if (tab === "blocks") {
136
+ _ctx.registerElementsDnD();
137
+ _ctx.registerComponentsDnD();
138
+ } else if (tab === "files") {
139
+ const tree = /** @type {any} */ (leftPanel)?.querySelector(".file-tree");
140
+ if (tree) _ctx.setupTreeKeyboard(tree);
141
+ }
142
+ }