@jxsuite/studio 0.19.0 → 0.20.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.css +98 -98
- package/dist/studio.js +12577 -12183
- package/dist/studio.js.map +146 -144
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +17 -6
- package/src/browse/browse.js +25 -19
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +94 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +38 -15
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +1 -1
- package/src/editor/convert-to-component.js +12 -10
- package/src/editor/inline-edit.js +66 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +4 -2
- package/src/editor/slash-menu.js +6 -4
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +78 -52
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +115 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +22 -20
- package/src/panels/ai-panel.js +399 -0
- package/src/panels/block-action-bar.js +5 -5
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +469 -98
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +88 -64
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +85 -47
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +365 -249
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +7 -5
- package/src/panels/right-panel.js +26 -2
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +295 -166
- package/src/panels/statusbar.js +4 -4
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +233 -132
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +7 -7
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +21 -26
- package/src/studio.js +103 -81
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +31 -20
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +7 -7
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +10 -7
package/src/panels/git-panel.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git panel — Source control sidebar with status,
|
|
3
|
-
*
|
|
2
|
+
* Git panel — Source control sidebar with sync status, branch selector, Local Changes / History
|
|
3
|
+
* tabs, commit form, and changed-components view.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { html, nothing } from "lit-html";
|
|
7
7
|
import { live } from "lit-html/directives/live.js";
|
|
8
8
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
9
9
|
import { getPlatform } from "../platform.js";
|
|
10
|
-
import { updateUi, renderOnly } from "../store.js";
|
|
10
|
+
import { updateUi, renderOnly, projectState } from "../store.js";
|
|
11
11
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
12
|
import { view } from "../view.js";
|
|
13
|
+
import { showDialog } from "../ui/layers.js";
|
|
14
|
+
import { statusMessage } from "./statusbar.js";
|
|
15
|
+
import { publishToGithub } from "../github/github-publish.js";
|
|
13
16
|
|
|
14
17
|
export async function refreshGitStatus() {
|
|
18
|
+
if (!projectState) return;
|
|
15
19
|
const plat = getPlatform();
|
|
16
20
|
updateUi("gitLoading", true);
|
|
17
21
|
updateUi("gitError", null);
|
|
@@ -19,39 +23,153 @@ export async function refreshGitStatus() {
|
|
|
19
23
|
const [status, branches] = await Promise.all([plat.gitStatus(), plat.gitBranches()]);
|
|
20
24
|
updateUi("gitStatus", status);
|
|
21
25
|
updateUi("gitBranches", branches);
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
_lastUpdated = new Date();
|
|
27
|
+
} catch (/** @type {unknown} */ e) {
|
|
28
|
+
updateUi("gitError", /** @type {Error} */ (e).message);
|
|
24
29
|
} finally {
|
|
25
30
|
updateUi("gitLoading", false);
|
|
26
31
|
renderOnly("leftPanel");
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Show a dialog to clone a git repository. Returns the cloned project root on success, or null.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ openRecentProject: (root: string) => Promise<void> }} ctx
|
|
39
|
+
*/
|
|
40
|
+
export async function cloneRepository(ctx) {
|
|
41
|
+
const platform = getPlatform();
|
|
42
|
+
if (!platform.gitClone) {
|
|
43
|
+
statusMessage("Clone not supported on this platform");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const url = await showDialog(
|
|
48
|
+
(done) => html`
|
|
49
|
+
<sp-underlay open @close=${() => done(null)}></sp-underlay>
|
|
50
|
+
<sp-dialog-wrapper
|
|
51
|
+
headline="Clone Git Repository"
|
|
52
|
+
confirmLabel="Clone"
|
|
53
|
+
cancelLabel="Cancel"
|
|
54
|
+
open
|
|
55
|
+
@confirm=${(/** @type {Event} */ e) => {
|
|
56
|
+
const input = /** @type {HTMLInputElement | null} */ (
|
|
57
|
+
/** @type {HTMLElement} */ (
|
|
58
|
+
/** @type {HTMLElement} */ (e.target).parentElement
|
|
59
|
+
).querySelector("sp-textfield")
|
|
60
|
+
);
|
|
61
|
+
done(input?.value?.trim() || null);
|
|
62
|
+
}}
|
|
63
|
+
@cancel=${() => done(null)}
|
|
64
|
+
@close=${() => done(null)}
|
|
65
|
+
>
|
|
66
|
+
<sp-textfield
|
|
67
|
+
label="Repository URL"
|
|
68
|
+
placeholder="https://github.com/user/repo.git"
|
|
69
|
+
style="width: 100%"
|
|
70
|
+
autofocus
|
|
71
|
+
></sp-textfield>
|
|
72
|
+
</sp-dialog-wrapper>
|
|
73
|
+
`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!url) return;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
statusMessage("Cloning repository...");
|
|
80
|
+
const result = await platform.gitClone(url);
|
|
81
|
+
if (result?.root) {
|
|
82
|
+
statusMessage("Clone complete");
|
|
83
|
+
await ctx.openRecentProject(result.root);
|
|
84
|
+
}
|
|
85
|
+
} catch (/** @type {unknown} */ e) {
|
|
86
|
+
statusMessage(`Clone failed: ${/** @type {Error} */ (e).message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @returns {boolean} */
|
|
91
|
+
export function platformSupportsClone() {
|
|
92
|
+
return !!getPlatform().gitClone;
|
|
93
|
+
}
|
|
94
|
+
|
|
30
95
|
/**
|
|
31
96
|
* @param {string} action
|
|
32
|
-
* @param {
|
|
97
|
+
* @param {unknown} [body]
|
|
33
98
|
*/
|
|
34
99
|
async function gitAction(action, body) {
|
|
35
|
-
const plat = getPlatform();
|
|
100
|
+
const plat = /** @type {Record<string, Function> & StudioPlatform} */ (getPlatform());
|
|
36
101
|
updateUi("gitLoading", true);
|
|
37
102
|
updateUi("gitError", null);
|
|
38
103
|
try {
|
|
39
104
|
await plat[action](body);
|
|
40
105
|
await refreshGitStatus();
|
|
41
|
-
} catch (/** @type {
|
|
42
|
-
updateUi("gitError", e.message);
|
|
106
|
+
} catch (/** @type {unknown} */ e) {
|
|
107
|
+
updateUi("gitError", /** @type {Error} */ (e).message);
|
|
43
108
|
updateUi("gitLoading", false);
|
|
44
109
|
renderOnly("leftPanel");
|
|
45
110
|
}
|
|
46
111
|
}
|
|
47
112
|
|
|
48
|
-
let _pollTimer = /** @type {
|
|
113
|
+
let _pollTimer = /** @type {ReturnType<typeof setInterval> | null} */ (null);
|
|
114
|
+
let _lastUpdated = /** @type {Date | null} */ (null);
|
|
115
|
+
let _gitSubTab = "changes";
|
|
116
|
+
|
|
117
|
+
/** @typedef {{ path: string; status: string; staged: boolean }} GitFileEntry */
|
|
118
|
+
/** @typedef {{ hash: string; message: string; author: string; date: string }} GitLogEntry */
|
|
49
119
|
|
|
50
120
|
/**
|
|
51
|
-
* @
|
|
52
|
-
*
|
|
121
|
+
* @typedef {{
|
|
122
|
+
* gitStatus?: {
|
|
123
|
+
* files?: GitFileEntry[];
|
|
124
|
+
* branch?: string;
|
|
125
|
+
* ahead?: number;
|
|
126
|
+
* behind?: number;
|
|
127
|
+
* isRepo?: boolean;
|
|
128
|
+
* remotes?: string[];
|
|
129
|
+
* } | null;
|
|
130
|
+
* gitBranches?: { current?: string; branches?: string[] } | null;
|
|
131
|
+
* gitLoading?: boolean;
|
|
132
|
+
* gitError?: string | null;
|
|
133
|
+
* gitCommitMessage?: string;
|
|
134
|
+
* gitLogEntries?: GitLogEntry[] | null;
|
|
135
|
+
* [key: string]: unknown;
|
|
136
|
+
* }} GitUiState
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
async function fetchGitLog() {
|
|
140
|
+
const plat = getPlatform();
|
|
141
|
+
try {
|
|
142
|
+
const entries = await plat.gitLog(30);
|
|
143
|
+
updateUi("gitLogEntries", entries);
|
|
144
|
+
renderOnly("leftPanel");
|
|
145
|
+
} catch (/** @type {unknown} */ e) {
|
|
146
|
+
updateUi("gitError", /** @type {Error} */ (e).message);
|
|
147
|
+
renderOnly("leftPanel");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {{ ui: GitUiState }} S
|
|
153
|
+
* @param {{
|
|
154
|
+
* setCanvasMode?: (mode: string) => void;
|
|
155
|
+
* setGitDiffState?: (state: unknown) => void;
|
|
156
|
+
* cloneRepository?: () => void;
|
|
157
|
+
* }} ctx
|
|
53
158
|
*/
|
|
54
159
|
export function renderGitPanel(S, ctx) {
|
|
160
|
+
if (!projectState) {
|
|
161
|
+
return html`<div class="git-panel git-panel-empty">
|
|
162
|
+
<div class="git-empty-state">
|
|
163
|
+
<p>Open a project to use source control.</p>
|
|
164
|
+
${platformSupportsClone()
|
|
165
|
+
? html`<sp-action-button size="m" @click=${() => ctx.cloneRepository?.()}>
|
|
166
|
+
<sp-icon-download slot="icon"></sp-icon-download>
|
|
167
|
+
Clone Git Repository
|
|
168
|
+
</sp-action-button>`
|
|
169
|
+
: nothing}
|
|
170
|
+
</div>
|
|
171
|
+
</div>`;
|
|
172
|
+
}
|
|
55
173
|
const status = S.ui.gitStatus;
|
|
56
174
|
const branches = S.ui.gitBranches;
|
|
57
175
|
const loading = S.ui.gitLoading;
|
|
@@ -61,14 +179,43 @@ export function renderGitPanel(S, ctx) {
|
|
|
61
179
|
return html`<div class="git-panel"><div class="git-loading">Loading...</div></div>`;
|
|
62
180
|
}
|
|
63
181
|
|
|
182
|
+
if (status && !status.isRepo) {
|
|
183
|
+
return html`<div class="git-panel git-panel-empty">
|
|
184
|
+
<div class="git-empty-state">
|
|
185
|
+
<p>This project is not yet a git repository.</p>
|
|
186
|
+
<sp-action-button
|
|
187
|
+
size="m"
|
|
188
|
+
@click=${async () => {
|
|
189
|
+
statusMessage("Initializing repository…");
|
|
190
|
+
await getPlatform().gitInit();
|
|
191
|
+
statusMessage("Repository initialized");
|
|
192
|
+
await refreshGitStatus();
|
|
193
|
+
}}
|
|
194
|
+
?disabled=${loading}
|
|
195
|
+
>
|
|
196
|
+
<sp-icon-add slot="icon"></sp-icon-add>
|
|
197
|
+
Initialize Repository
|
|
198
|
+
</sp-action-button>
|
|
199
|
+
<sp-action-button
|
|
200
|
+
size="m"
|
|
201
|
+
@click=${() => publishToGithub({ projectName: projectState?.name || "my-project" })}
|
|
202
|
+
?disabled=${loading}
|
|
203
|
+
>
|
|
204
|
+
<sp-icon-share slot="icon"></sp-icon-share>
|
|
205
|
+
Publish to GitHub
|
|
206
|
+
</sp-action-button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
64
211
|
if (!_pollTimer) {
|
|
65
212
|
_pollTimer = setInterval(() => {
|
|
66
213
|
if (view.leftTab === "git" && !S.ui.gitLoading) refreshGitStatus();
|
|
67
214
|
}, 30000);
|
|
68
215
|
}
|
|
69
216
|
|
|
70
|
-
const stagedFiles = status?.files?.filter((/** @type {
|
|
71
|
-
const unstagedFiles = status?.files?.filter((/** @type {
|
|
217
|
+
const stagedFiles = status?.files?.filter((/** @type {GitFileEntry} */ f) => f.staged) || [];
|
|
218
|
+
const unstagedFiles = status?.files?.filter((/** @type {GitFileEntry} */ f) => !f.staged) || [];
|
|
72
219
|
const totalChanges = status?.files?.length || 0;
|
|
73
220
|
|
|
74
221
|
const doCommit = async () => {
|
|
@@ -79,83 +226,240 @@ export function renderGitPanel(S, ctx) {
|
|
|
79
226
|
await gitAction("gitCommit", msg);
|
|
80
227
|
};
|
|
81
228
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
229
|
+
const doCommitAndSync = async () => {
|
|
230
|
+
const tab = activeTab.value;
|
|
231
|
+
const msg = tab?.session.ui.gitCommitMessage?.trim();
|
|
232
|
+
if (!msg) return;
|
|
233
|
+
updateUi("gitCommitMessage", "");
|
|
234
|
+
updateUi("gitLoading", true);
|
|
235
|
+
updateUi("gitError", null);
|
|
236
|
+
const plat = getPlatform();
|
|
237
|
+
try {
|
|
238
|
+
await plat.gitCommit(msg);
|
|
239
|
+
await plat.gitPush();
|
|
240
|
+
await refreshGitStatus();
|
|
241
|
+
} catch (/** @type {unknown} */ e) {
|
|
242
|
+
updateUi("gitError", /** @type {Error} */ (e).message);
|
|
243
|
+
updateUi("gitLoading", false);
|
|
244
|
+
renderOnly("leftPanel");
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ─── 1. Sync status bar ──────────────────────────────────────────────────
|
|
249
|
+
const isUpToDate = !status?.ahead && !status?.behind;
|
|
250
|
+
const syncLabel = isUpToDate
|
|
251
|
+
? "Up to date"
|
|
252
|
+
: `${status?.ahead ? `${status.ahead} ahead` : ""}${status?.ahead && status?.behind ? ", " : ""}${status?.behind ? `${status.behind} behind` : ""}`;
|
|
253
|
+
const lastUpdatedStr = _lastUpdated
|
|
254
|
+
? _lastUpdated.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
255
|
+
: "";
|
|
256
|
+
|
|
257
|
+
const hasRemotes = (status?.remotes?.length ?? 0) > 0;
|
|
258
|
+
|
|
259
|
+
const syncBarT = hasRemotes
|
|
260
|
+
? html`
|
|
261
|
+
<div class="git-sync-bar">
|
|
262
|
+
<sp-action-button
|
|
263
|
+
size="s"
|
|
264
|
+
quiet
|
|
265
|
+
class="git-sync-icon"
|
|
266
|
+
title="Refresh"
|
|
267
|
+
@click=${() => refreshGitStatus()}
|
|
268
|
+
?disabled=${loading}
|
|
269
|
+
>
|
|
270
|
+
<sp-icon-refresh slot="icon"></sp-icon-refresh>
|
|
271
|
+
</sp-action-button>
|
|
272
|
+
<div class="git-sync-text">
|
|
273
|
+
<span class="git-sync-label">${syncLabel}</span>
|
|
274
|
+
${lastUpdatedStr
|
|
275
|
+
? html`<span class="git-sync-time">Last updated ${lastUpdatedStr}</span>`
|
|
276
|
+
: nothing}
|
|
277
|
+
</div>
|
|
278
|
+
<sp-action-group size="xs" quiet class="git-sync-actions">
|
|
279
|
+
<sp-action-button
|
|
280
|
+
title="Fetch"
|
|
281
|
+
@click=${() => gitAction("gitFetch")}
|
|
282
|
+
?disabled=${loading}
|
|
283
|
+
>
|
|
284
|
+
<sp-icon-download slot="icon" size="xs"></sp-icon-download>
|
|
285
|
+
</sp-action-button>
|
|
286
|
+
<sp-action-button
|
|
287
|
+
title="Pull${status?.behind ? ` (${status.behind} behind)` : ""}"
|
|
288
|
+
@click=${() => gitAction("gitPull")}
|
|
289
|
+
?disabled=${loading}
|
|
290
|
+
>
|
|
291
|
+
<sp-icon-arrow-down slot="icon" size="xs"></sp-icon-arrow-down>
|
|
292
|
+
</sp-action-button>
|
|
293
|
+
<sp-action-button
|
|
294
|
+
title="Push${status?.ahead ? ` (${status.ahead} ahead)` : ""}"
|
|
295
|
+
@click=${() => gitAction("gitPush")}
|
|
296
|
+
?disabled=${loading}
|
|
297
|
+
>
|
|
298
|
+
<sp-icon-arrow-up slot="icon" size="xs"></sp-icon-arrow-up>
|
|
299
|
+
</sp-action-button>
|
|
300
|
+
</sp-action-group>
|
|
301
|
+
</div>
|
|
302
|
+
`
|
|
303
|
+
: html`
|
|
304
|
+
<div class="git-sync-bar git-sync-bar--no-remote">
|
|
305
|
+
<sp-action-button
|
|
306
|
+
size="s"
|
|
307
|
+
quiet
|
|
308
|
+
class="git-sync-icon"
|
|
309
|
+
title="Refresh"
|
|
310
|
+
@click=${() => refreshGitStatus()}
|
|
311
|
+
?disabled=${loading}
|
|
312
|
+
>
|
|
313
|
+
<sp-icon-refresh slot="icon"></sp-icon-refresh>
|
|
314
|
+
</sp-action-button>
|
|
315
|
+
<div class="git-sync-text">
|
|
316
|
+
<span class="git-sync-label">Local only (no remote)</span>
|
|
317
|
+
</div>
|
|
318
|
+
<sp-action-button
|
|
319
|
+
size="s"
|
|
320
|
+
@click=${() => publishToGithub({ projectName: projectState?.name || "my-project" })}
|
|
321
|
+
?disabled=${loading}
|
|
322
|
+
>
|
|
323
|
+
<sp-icon-share slot="icon"></sp-icon-share>
|
|
324
|
+
Publish to GitHub
|
|
325
|
+
</sp-action-button>
|
|
326
|
+
</div>
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
// ─── 2. Branch selector ──────────────────────────────────────────────────
|
|
330
|
+
const branchSelectorT = html`
|
|
331
|
+
<div class="git-branch-row">
|
|
332
|
+
<svg
|
|
333
|
+
class="git-branch-icon"
|
|
334
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
335
|
+
width="18"
|
|
336
|
+
height="18"
|
|
337
|
+
viewBox="0 0 24 24"
|
|
338
|
+
fill="none"
|
|
339
|
+
stroke="currentColor"
|
|
340
|
+
stroke-width="2"
|
|
341
|
+
stroke-linecap="round"
|
|
342
|
+
stroke-linejoin="round"
|
|
343
|
+
>
|
|
344
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
345
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
346
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
347
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
348
|
+
</svg>
|
|
349
|
+
<div class="git-branch-text">
|
|
350
|
+
<span class="git-branch-label">Active branch</span>
|
|
351
|
+
<span class="git-branch-name">${branches?.current || status?.branch || "—"}</span>
|
|
352
|
+
</div>
|
|
353
|
+
<sp-picker
|
|
354
|
+
size="s"
|
|
355
|
+
quiet
|
|
356
|
+
class="git-branch-picker"
|
|
357
|
+
.value=${live(branches?.current || "")}
|
|
358
|
+
@change=${async (/** @type {Event} */ e) => {
|
|
359
|
+
const val = /** @type {HTMLInputElement} */ (e.target).value;
|
|
360
|
+
if (val === "__new__") {
|
|
361
|
+
/** @type {HTMLInputElement} */ (e.target).value = branches?.current || "";
|
|
362
|
+
const name = prompt("New branch name:");
|
|
363
|
+
if (name?.trim()) await gitAction("gitCreateBranch", name.trim());
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (val !== branches?.current) await gitAction("gitCheckout", val);
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
${(branches?.branches || []).map(
|
|
370
|
+
(/** @type {string} */ b) => html`<sp-menu-item value=${b}>${b}</sp-menu-item>`,
|
|
371
|
+
)}
|
|
372
|
+
<sp-menu-divider></sp-menu-divider>
|
|
373
|
+
<sp-menu-item value="__new__">+ New branch...</sp-menu-item>
|
|
374
|
+
</sp-picker>
|
|
375
|
+
</div>
|
|
105
376
|
`;
|
|
106
377
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<sp-action-button title="Refresh" @click=${() => refreshGitStatus()} ?disabled=${loading}>
|
|
129
|
-
<sp-icon-refresh slot="icon" size="xs"></sp-icon-refresh>
|
|
130
|
-
</sp-action-button>
|
|
131
|
-
</sp-action-group>
|
|
378
|
+
// ─── 3. Tabs: Local Changes / History ────────────────────────────────────
|
|
379
|
+
const switchTab = (/** @type {string} */ tab) => {
|
|
380
|
+
_gitSubTab = tab;
|
|
381
|
+
if (tab === "history" && !S.ui.gitLogEntries) fetchGitLog();
|
|
382
|
+
renderOnly("leftPanel");
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const tabsT = html`
|
|
386
|
+
<div class="git-tabs">
|
|
387
|
+
<button
|
|
388
|
+
class="git-tab ${_gitSubTab === "changes" ? "active" : ""}"
|
|
389
|
+
@click=${() => switchTab("changes")}
|
|
390
|
+
>
|
|
391
|
+
Local Changes${totalChanges > 0 ? ` (${totalChanges})` : ""}
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
class="git-tab ${_gitSubTab === "history" ? "active" : ""}"
|
|
395
|
+
@click=${() => switchTab("history")}
|
|
396
|
+
>
|
|
397
|
+
History
|
|
398
|
+
</button>
|
|
132
399
|
</div>
|
|
133
400
|
`;
|
|
134
401
|
|
|
402
|
+
// ─── 4. Commit form ──────────────────────────────────────────────────────
|
|
135
403
|
const commitT = html`
|
|
136
404
|
<div class="git-commit-area">
|
|
405
|
+
<label class="git-commit-label">Please write a commit message</label>
|
|
137
406
|
<sp-textfield
|
|
138
407
|
size="s"
|
|
139
408
|
multiline
|
|
140
409
|
class="git-commit-input"
|
|
141
|
-
placeholder=
|
|
410
|
+
placeholder="Describe your changes"
|
|
142
411
|
.value=${live(S.ui.gitCommitMessage || "")}
|
|
143
|
-
@input=${(/** @type {
|
|
144
|
-
|
|
412
|
+
@input=${(/** @type {Event} */ e) =>
|
|
413
|
+
updateUi("gitCommitMessage", /** @type {HTMLInputElement} */ (e.target).value)}
|
|
414
|
+
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
145
415
|
if (e.ctrlKey && e.key === "Enter") {
|
|
146
416
|
e.preventDefault();
|
|
147
417
|
doCommit();
|
|
148
418
|
}
|
|
149
419
|
}}
|
|
150
420
|
></sp-textfield>
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
421
|
+
<div class="git-commit-actions">
|
|
422
|
+
<div class="git-split-btn">
|
|
423
|
+
<sp-action-button
|
|
424
|
+
class="git-commit-btn"
|
|
425
|
+
size="s"
|
|
426
|
+
@click=${doCommitAndSync}
|
|
427
|
+
?disabled=${loading}
|
|
428
|
+
>
|
|
429
|
+
Commit and sync
|
|
430
|
+
</sp-action-button>
|
|
431
|
+
<sp-action-button
|
|
432
|
+
class="git-split-trigger"
|
|
433
|
+
size="s"
|
|
434
|
+
@click=${(/** @type {Event} */ e) => {
|
|
435
|
+
const menu = /** @type {HTMLElement} */ (
|
|
436
|
+
/** @type {HTMLElement} */ (e.currentTarget).parentElement
|
|
437
|
+
).querySelector(".git-split-menu");
|
|
438
|
+
if (menu) menu.toggleAttribute("hidden");
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
<sp-icon-chevron-down slot="icon" size="xs"></sp-icon-chevron-down>
|
|
442
|
+
</sp-action-button>
|
|
443
|
+
<div class="git-split-menu" hidden>
|
|
444
|
+
<button
|
|
445
|
+
class="git-split-menu-item"
|
|
446
|
+
@click=${(/** @type {Event} */ e) => {
|
|
447
|
+
/** @type {HTMLElement} */ (
|
|
448
|
+
/** @type {HTMLElement} */ (e.currentTarget).parentElement
|
|
449
|
+
).setAttribute("hidden", "");
|
|
450
|
+
doCommit();
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
Commit (don't sync)
|
|
454
|
+
</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
155
458
|
</div>
|
|
156
459
|
`;
|
|
157
460
|
|
|
158
|
-
|
|
461
|
+
// ─── 5. Changed Components ───────────────────────────────────────────────
|
|
462
|
+
const fileRowT = (/** @type {GitFileEntry} */ file) => {
|
|
159
463
|
const parts = file.path.split("/");
|
|
160
464
|
const name = parts.pop();
|
|
161
465
|
const dir = parts.join("/");
|
|
@@ -190,8 +494,8 @@ export function renderGitPanel(S, ctx) {
|
|
|
190
494
|
if (ctx.setGitDiffState) ctx.setGitDiffState(diffState);
|
|
191
495
|
ctx.setCanvasMode("git-diff");
|
|
192
496
|
}
|
|
193
|
-
} catch (/** @type {
|
|
194
|
-
updateUi("gitError", `Failed to load diff: ${e.message}`);
|
|
497
|
+
} catch (/** @type {unknown} */ e) {
|
|
498
|
+
updateUi("gitError", `Failed to load diff: ${/** @type {Error} */ (e).message}`);
|
|
195
499
|
} finally {
|
|
196
500
|
updateUi("gitLoading", false);
|
|
197
501
|
}
|
|
@@ -249,6 +553,27 @@ export function renderGitPanel(S, ctx) {
|
|
|
249
553
|
`;
|
|
250
554
|
};
|
|
251
555
|
|
|
556
|
+
/** Group files by component (parent directory for .json/.class.json, or "Other") */
|
|
557
|
+
const groupFilesByComponent = (/** @type {GitFileEntry[]} */ files) => {
|
|
558
|
+
/** @type {Map<string, GitFileEntry[]>} */
|
|
559
|
+
const groups = new Map();
|
|
560
|
+
for (const f of files) {
|
|
561
|
+
const parts = f.path.split("/");
|
|
562
|
+
let component;
|
|
563
|
+
if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || f.path.endsWith(".md")) {
|
|
564
|
+
component = parts.length > 1 ? `/${parts[parts.length - 2]}` : `/${parts[0]}`;
|
|
565
|
+
} else {
|
|
566
|
+
component = "Other";
|
|
567
|
+
}
|
|
568
|
+
if (!groups.has(component)) groups.set(component, []);
|
|
569
|
+
/** @type {GitFileEntry[]} */ (groups.get(component)).push(f);
|
|
570
|
+
}
|
|
571
|
+
return groups;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const allFiles = [...stagedFiles, ...unstagedFiles];
|
|
575
|
+
const componentGroups = groupFilesByComponent(allFiles);
|
|
576
|
+
|
|
252
577
|
const changesT = html`
|
|
253
578
|
${stagedFiles.length > 0
|
|
254
579
|
? html`
|
|
@@ -263,22 +588,22 @@ export function renderGitPanel(S, ctx) {
|
|
|
263
588
|
@click=${() =>
|
|
264
589
|
gitAction(
|
|
265
590
|
"gitUnstage",
|
|
266
|
-
stagedFiles.map((/** @type {
|
|
591
|
+
stagedFiles.map((/** @type {GitFileEntry} */ f) => f.path),
|
|
267
592
|
)}
|
|
268
593
|
>
|
|
269
594
|
<sp-icon-remove slot="icon" size="xs"></sp-icon-remove>
|
|
270
595
|
</sp-action-button>
|
|
271
596
|
</div>
|
|
272
|
-
${repeat(stagedFiles, (/** @type {
|
|
597
|
+
${repeat(stagedFiles, (/** @type {GitFileEntry} */ f) => f.path, fileRowT)}
|
|
273
598
|
</div>
|
|
274
599
|
`
|
|
275
600
|
: nothing}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
601
|
+
<div class="git-section">
|
|
602
|
+
<div class="git-section-header">
|
|
603
|
+
<span>Changed Components</span>
|
|
604
|
+
<span class="git-count">${allFiles.length}</span>
|
|
605
|
+
${unstagedFiles.length > 0
|
|
606
|
+
? html`
|
|
282
607
|
<sp-action-button
|
|
283
608
|
size="xs"
|
|
284
609
|
quiet
|
|
@@ -286,38 +611,84 @@ export function renderGitPanel(S, ctx) {
|
|
|
286
611
|
@click=${() =>
|
|
287
612
|
gitAction(
|
|
288
613
|
"gitStage",
|
|
289
|
-
unstagedFiles.map((/** @type {
|
|
614
|
+
unstagedFiles.map((/** @type {GitFileEntry} */ f) => f.path),
|
|
290
615
|
)}
|
|
291
616
|
>
|
|
292
617
|
<sp-icon-add slot="icon" size="xs"></sp-icon-add>
|
|
293
618
|
</sp-action-button>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
619
|
+
`
|
|
620
|
+
: nothing}
|
|
621
|
+
</div>
|
|
622
|
+
${allFiles.length > 0
|
|
623
|
+
? html`
|
|
624
|
+
${[...componentGroups.entries()].map(
|
|
625
|
+
([comp, files]) => html`
|
|
626
|
+
<div class="git-component-group">
|
|
627
|
+
<div class="git-component-header">
|
|
628
|
+
<sp-action-button
|
|
629
|
+
size="xs"
|
|
630
|
+
quiet
|
|
631
|
+
class="git-component-overflow"
|
|
632
|
+
title="Actions"
|
|
633
|
+
>
|
|
634
|
+
<sp-icon-more slot="icon" size="xs"></sp-icon-more>
|
|
635
|
+
</sp-action-button>
|
|
636
|
+
<span class="git-component-name">${comp}</span>
|
|
637
|
+
</div>
|
|
638
|
+
${repeat(files, (/** @type {GitFileEntry} */ f) => f.path, fileRowT)}
|
|
639
|
+
</div>
|
|
640
|
+
`,
|
|
641
|
+
)}
|
|
642
|
+
`
|
|
643
|
+
: html`<div class="git-empty">No changes</div>`}
|
|
644
|
+
</div>
|
|
300
645
|
`;
|
|
301
646
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
647
|
+
// ─── 6. History tab content ──────────────────────────────────────────────
|
|
648
|
+
const logEntries = S.ui.gitLogEntries || [];
|
|
649
|
+
const historyT = html`
|
|
650
|
+
<div class="git-history">
|
|
651
|
+
${logEntries.length === 0
|
|
652
|
+
? html`<div class="git-empty">No history</div>`
|
|
653
|
+
: repeat(
|
|
654
|
+
logEntries,
|
|
655
|
+
(/** @type {GitLogEntry} */ e) => e.hash,
|
|
656
|
+
(/** @type {GitLogEntry} */ entry) => html`
|
|
657
|
+
<div class="git-history-entry">
|
|
658
|
+
<span class="git-history-hash">${entry.hash.slice(0, 7)}</span>
|
|
659
|
+
<span class="git-history-message">${entry.message}</span>
|
|
660
|
+
<span class="git-history-meta">${entry.author} · ${_relativeDate(entry.date)}</span>
|
|
661
|
+
</div>
|
|
662
|
+
`,
|
|
663
|
+
)}
|
|
664
|
+
</div>
|
|
665
|
+
`;
|
|
311
666
|
|
|
312
667
|
return html`
|
|
313
668
|
<div class="git-panel">
|
|
314
|
-
${
|
|
669
|
+
${syncBarT} ${branchSelectorT} ${tabsT}
|
|
670
|
+
${_gitSubTab === "changes" ? html`${commitT}${changesT}` : historyT}
|
|
315
671
|
${loading ? html`<div class="git-loading">Loading...</div>` : nothing}
|
|
316
|
-
${S.ui.gitError ? html`<div class="git-error">${S.ui.gitError}</div>` : nothing}
|
|
672
|
+
${S.ui.gitError ? html`<div class="git-error">${S.ui.gitError}</div>` : nothing}
|
|
317
673
|
</div>
|
|
318
674
|
`;
|
|
319
675
|
}
|
|
320
676
|
|
|
677
|
+
/** @param {string} iso */
|
|
678
|
+
function _relativeDate(iso) {
|
|
679
|
+
const d = new Date(iso);
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
const diff = now - d.getTime();
|
|
682
|
+
const mins = Math.floor(diff / 60000);
|
|
683
|
+
if (mins < 1) return "just now";
|
|
684
|
+
if (mins < 60) return `${mins}m ago`;
|
|
685
|
+
const hours = Math.floor(mins / 60);
|
|
686
|
+
if (hours < 24) return `${hours}h ago`;
|
|
687
|
+
const days = Math.floor(hours / 24);
|
|
688
|
+
if (days < 30) return `${days}d ago`;
|
|
689
|
+
return d.toLocaleDateString();
|
|
690
|
+
}
|
|
691
|
+
|
|
321
692
|
export function cleanupGitPanel() {
|
|
322
693
|
if (_pollTimer) {
|
|
323
694
|
clearInterval(_pollTimer);
|