@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.
- package/dist/studio.js +162163 -151534
- package/dist/studio.js.map +99 -27
- package/package.json +2 -2
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/markdown/md-convert.js +18 -16
- package/src/panels/activity-bar.js +22 -0
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/elements-panel.js +148 -0
- package/src/panels/git-panel.js +280 -0
- package/src/panels/layers-panel.js +270 -0
- package/src/panels/left-panel.js +142 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/right-panel.js +5 -3
- package/src/panels/shared.js +74 -0
- package/src/panels/style-inputs.js +176 -0
- package/src/panels/style-panel.js +651 -0
- package/src/panels/style-utils.js +193 -0
- package/src/panels/stylebook-layers-panel.js +103 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/platforms/devserver.js +113 -0
- package/src/state.js +7 -0
- package/src/studio.js +92 -4820
- package/src/ui/spectrum.js +4 -0
|
@@ -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
|
+
}
|