@jxsuite/studio 0.28.2 → 0.28.4
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 +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/** Elements panel — block/component palette with categorized accordion and search filter. */
|
|
3
3
|
|
|
4
4
|
import { html, nothing } from "lit-html";
|
|
5
|
-
import { getNodeAtPath } from "../store";
|
|
5
|
+
import { childList, getNodeAtPath } from "../store";
|
|
6
6
|
import { activeTab } from "../workspace/workspace";
|
|
7
|
-
import {
|
|
7
|
+
import { mutateInsertNode, transactDoc } from "../tabs/transact";
|
|
8
8
|
import { view } from "../view";
|
|
9
9
|
import { getEffectiveElements } from "../site-context";
|
|
10
10
|
import { componentRegistry } from "../files/components";
|
|
@@ -32,16 +32,20 @@ export function renderElementsTemplate(ctx: {
|
|
|
32
32
|
const filtered = view.elementsFilter
|
|
33
33
|
? elements.filter((/** @type {{ tag: string }} */ e) => e.tag.includes(view.elementsFilter))
|
|
34
34
|
: elements;
|
|
35
|
-
if (filtered.length === 0)
|
|
35
|
+
if (filtered.length === 0) {
|
|
36
|
+
return nothing;
|
|
37
|
+
}
|
|
36
38
|
|
|
37
39
|
return html`
|
|
38
40
|
<sp-accordion-item
|
|
39
41
|
label=${category}
|
|
40
42
|
?open=${!view.elementsCollapsed.has(category)}
|
|
41
43
|
@sp-accordion-item-toggle=${(e: Event) => {
|
|
42
|
-
if ((e.target as HTMLElement & { open: boolean }).open)
|
|
44
|
+
if ((e.target as HTMLElement & { open: boolean }).open) {
|
|
43
45
|
view.elementsCollapsed.delete(category);
|
|
44
|
-
else
|
|
46
|
+
} else {
|
|
47
|
+
view.elementsCollapsed.add(category);
|
|
48
|
+
}
|
|
45
49
|
}}
|
|
46
50
|
>
|
|
47
51
|
${filtered.map((/** @type {{ tag: string }} */ { tag }) => {
|
|
@@ -54,7 +58,7 @@ export function renderElementsTemplate(ctx: {
|
|
|
54
58
|
const t = activeTab.value;
|
|
55
59
|
const parentPath = t?.session.selection || [];
|
|
56
60
|
const parent = getNodeAtPath(t!.doc.document, parentPath);
|
|
57
|
-
const idx = parent
|
|
61
|
+
const idx = childList(parent).length;
|
|
58
62
|
transactDoc(t!, (tr) =>
|
|
59
63
|
mutateInsertNode(tr, parentPath, idx, structuredClone(def)),
|
|
60
64
|
);
|
|
@@ -73,9 +77,11 @@ export function renderElementsTemplate(ctx: {
|
|
|
73
77
|
const effectiveEls = getEffectiveElements(
|
|
74
78
|
tab?.doc.document?.$elements as (string | JxElement)[] | undefined,
|
|
75
79
|
);
|
|
76
|
-
const enabledTags
|
|
80
|
+
const enabledTags = new Set<string>();
|
|
77
81
|
for (const entry of effectiveEls) {
|
|
78
|
-
if (typeof entry !== "string")
|
|
82
|
+
if (typeof entry !== "string") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
79
85
|
const comp = componentRegistry.find(
|
|
80
86
|
(c: ComponentEntry) =>
|
|
81
87
|
c.source === "npm" && c.modulePath && entry === `${c.package}/${c.modulePath}`,
|
|
@@ -84,7 +90,9 @@ export function renderElementsTemplate(ctx: {
|
|
|
84
90
|
enabledTags.add(comp.tagName);
|
|
85
91
|
} else {
|
|
86
92
|
for (const c of componentRegistry) {
|
|
87
|
-
if (c.source === "npm" && c.package === entry)
|
|
93
|
+
if (c.source === "npm" && c.package === entry) {
|
|
94
|
+
enabledTags.add(c.tagName);
|
|
95
|
+
}
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
}
|
|
@@ -105,9 +113,11 @@ export function renderElementsTemplate(ctx: {
|
|
|
105
113
|
label="Components"
|
|
106
114
|
?open=${!view.elementsCollapsed.has("Components")}
|
|
107
115
|
@sp-accordion-item-toggle=${(e: Event) => {
|
|
108
|
-
if ((e.target as HTMLElement & { open: boolean }).open)
|
|
116
|
+
if ((e.target as HTMLElement & { open: boolean }).open) {
|
|
109
117
|
view.elementsCollapsed.delete("Components");
|
|
110
|
-
else
|
|
118
|
+
} else {
|
|
119
|
+
view.elementsCollapsed.add("Components");
|
|
120
|
+
}
|
|
111
121
|
}}
|
|
112
122
|
>
|
|
113
123
|
<div class="components-section">
|
|
@@ -123,9 +133,8 @@ export function renderElementsTemplate(ctx: {
|
|
|
123
133
|
const t = activeTab.value;
|
|
124
134
|
const parentPath = t?.session.selection || [];
|
|
125
135
|
const parent = getNodeAtPath(t!.doc.document, parentPath);
|
|
126
|
-
const idx = parent
|
|
136
|
+
const idx = childList(parent).length;
|
|
127
137
|
const instanceDef = {
|
|
128
|
-
tagName: comp.tagName,
|
|
129
138
|
$props: Object.fromEntries(
|
|
130
139
|
(comp.props || []).map(
|
|
131
140
|
(/** @type {{ name: string; default?: unknown }} */ p) => [
|
|
@@ -134,6 +143,7 @@ export function renderElementsTemplate(ctx: {
|
|
|
134
143
|
],
|
|
135
144
|
),
|
|
136
145
|
),
|
|
146
|
+
tagName: comp.tagName,
|
|
137
147
|
};
|
|
138
148
|
transactDoc(t!, (tr) =>
|
|
139
149
|
mutateInsertNode(tr, parentPath, idx, structuredClone(instanceDef)),
|
|
@@ -3,10 +3,18 @@ import { getNodeAtPath } from "../store";
|
|
|
3
3
|
import { html, nothing } from "lit-html";
|
|
4
4
|
import { live } from "lit-html/directives/live.js";
|
|
5
5
|
import { activeTab } from "../workspace/workspace";
|
|
6
|
-
import {
|
|
6
|
+
import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
7
7
|
import { renderExpressionEditor } from "../ui/expression-editor";
|
|
8
|
+
import {
|
|
9
|
+
getEventBinding,
|
|
10
|
+
isExpressionDef,
|
|
11
|
+
isFunctionDef,
|
|
12
|
+
isJsonObject,
|
|
13
|
+
isRef,
|
|
14
|
+
} from "@jxsuite/schema/guards";
|
|
8
15
|
|
|
9
|
-
import type {
|
|
16
|
+
import type { JsonValue } from "../types";
|
|
17
|
+
import type { CemEvent, JxFunctionDef, JxPrototypeDef } from "@jxsuite/schema/types";
|
|
10
18
|
|
|
11
19
|
export const EVENT_NAMES = [
|
|
12
20
|
"onclick",
|
|
@@ -27,9 +35,13 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
27
35
|
const tab = activeTab.value;
|
|
28
36
|
const selection = tab?.session.selection;
|
|
29
37
|
const document = tab?.doc.document;
|
|
30
|
-
if (!selection)
|
|
38
|
+
if (!selection) {
|
|
39
|
+
return html`<div class="empty-state">Select an element to edit events</div>`;
|
|
40
|
+
}
|
|
31
41
|
const node = getNodeAtPath(document!, selection);
|
|
32
|
-
if (!node)
|
|
42
|
+
if (!node) {
|
|
43
|
+
return html`<div class="empty-state">Node not found</div>`;
|
|
44
|
+
}
|
|
33
45
|
|
|
34
46
|
const defs = document!.state || {};
|
|
35
47
|
const functionDefs = Object.entries(defs).filter(
|
|
@@ -40,11 +52,12 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
40
52
|
// Declared CEM events (custom element docs)
|
|
41
53
|
let declaredEventsT: unknown = nothing;
|
|
42
54
|
if (isCustomElementDoc()) {
|
|
43
|
-
const allEmits:
|
|
55
|
+
const allEmits: (CemEvent & { _fn: string })[] = [];
|
|
44
56
|
for (const [fnName, d] of Object.entries(defs)) {
|
|
45
|
-
if (Array.isArray(
|
|
46
|
-
for (const ev of
|
|
57
|
+
if (isFunctionDef(d) && Array.isArray(d.emits)) {
|
|
58
|
+
for (const ev of d.emits) {
|
|
47
59
|
allEmits.push({ ...ev, _fn: fnName });
|
|
60
|
+
}
|
|
48
61
|
}
|
|
49
62
|
}
|
|
50
63
|
if (allEmits.length > 0) {
|
|
@@ -56,10 +69,8 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
56
69
|
<div class="declared-event-row" title=${ev.description || ""}>
|
|
57
70
|
<code class="event-code">${ev.name || "(unnamed)"}</code>
|
|
58
71
|
<span class="event-source">← ${ev._fn}</span>
|
|
59
|
-
${(ev.type
|
|
60
|
-
? html`<span class="event-type"
|
|
61
|
-
>${(ev.type as Record<string, unknown>).text}</span
|
|
62
|
-
>`
|
|
72
|
+
${isJsonObject(ev.type) && typeof ev.type.text === "string"
|
|
73
|
+
? html`<span class="event-type">${ev.type.text}</span>`
|
|
63
74
|
: nothing}
|
|
64
75
|
</div>
|
|
65
76
|
`,
|
|
@@ -71,12 +82,9 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
// Find existing event bindings
|
|
74
|
-
const eventKeys = Object.keys(node).filter(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!v || typeof v !== "object") return false;
|
|
78
|
-
return v.$ref || v.$prototype === "Function" || v.$expression;
|
|
79
|
-
});
|
|
85
|
+
const eventKeys = Object.keys(node).filter(
|
|
86
|
+
(k) => k.startsWith("on") && getEventBinding(node, k) !== undefined,
|
|
87
|
+
);
|
|
80
88
|
|
|
81
89
|
return html`
|
|
82
90
|
<div class="events-panel">
|
|
@@ -86,10 +94,14 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
86
94
|
? html` <sp-field-label size="s">Event Bindings</sp-field-label> `
|
|
87
95
|
: nothing}
|
|
88
96
|
${eventKeys.map((evKey) => {
|
|
89
|
-
const evVal = node
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
const evVal = getEventBinding(node, evKey);
|
|
98
|
+
if (!evVal) {
|
|
99
|
+
return nothing;
|
|
100
|
+
}
|
|
101
|
+
const inlineFn: JxFunctionDef | null = isFunctionDef(evVal) ? evVal : null;
|
|
102
|
+
const expression = isExpressionDef(evVal) ? evVal.$expression : null;
|
|
103
|
+
const refValue = isRef(evVal) ? evVal.$ref : null;
|
|
104
|
+
const currentMode = inlineFn ? "inline" : expression ? "$expression" : "ref";
|
|
93
105
|
return html`
|
|
94
106
|
<div class="event-binding">
|
|
95
107
|
<div class="event-row">
|
|
@@ -101,8 +113,8 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
101
113
|
const newKey = (e.target as HTMLInputElement).value;
|
|
102
114
|
if (newKey && newKey !== evKey) {
|
|
103
115
|
transactDoc(activeTab.value, (t) => {
|
|
104
|
-
mutateUpdateProperty(t, selection, evKey
|
|
105
|
-
mutateUpdateProperty(t, selection, newKey,
|
|
116
|
+
mutateUpdateProperty(t, selection, evKey);
|
|
117
|
+
mutateUpdateProperty(t, selection, newKey, evVal as JsonValue);
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
120
|
}}
|
|
@@ -132,7 +144,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
132
144
|
}),
|
|
133
145
|
);
|
|
134
146
|
} else {
|
|
135
|
-
const firstFn = functionDefs
|
|
147
|
+
const [firstFn] = functionDefs;
|
|
136
148
|
transactDoc(activeTab.value, (t) =>
|
|
137
149
|
mutateUpdateProperty(
|
|
138
150
|
t,
|
|
@@ -152,14 +164,12 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
152
164
|
size="xs"
|
|
153
165
|
quiet
|
|
154
166
|
@click=${() =>
|
|
155
|
-
transactDoc(activeTab.value, (t) =>
|
|
156
|
-
mutateUpdateProperty(t, selection, evKey, undefined),
|
|
157
|
-
)}
|
|
167
|
+
transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, selection, evKey))}
|
|
158
168
|
>
|
|
159
169
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
160
170
|
</sp-action-button>
|
|
161
171
|
</div>
|
|
162
|
-
${
|
|
172
|
+
${inlineFn
|
|
163
173
|
? html`
|
|
164
174
|
<div class="event-body-row">
|
|
165
175
|
<sp-textfield
|
|
@@ -167,13 +177,13 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
167
177
|
multiline
|
|
168
178
|
grows
|
|
169
179
|
placeholder="// handler body"
|
|
170
|
-
.value=${live(
|
|
180
|
+
.value=${live(inlineFn?.body || "")}
|
|
171
181
|
@input=${(e: Event) => {
|
|
172
182
|
transactDoc(activeTab.value, (t) =>
|
|
173
183
|
mutateUpdateProperty(t, selection, evKey, {
|
|
174
184
|
$prototype: "Function",
|
|
175
185
|
body: (e.target as HTMLInputElement).value,
|
|
176
|
-
parameters:
|
|
186
|
+
parameters: inlineFn?.parameters || [],
|
|
177
187
|
}),
|
|
178
188
|
);
|
|
179
189
|
}}
|
|
@@ -185,9 +195,9 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
185
195
|
title="Open in editor"
|
|
186
196
|
@click=${() => {
|
|
187
197
|
tab.session.ui.editingFunction = {
|
|
188
|
-
type: "event",
|
|
189
|
-
path: selection,
|
|
190
198
|
eventKey: evKey,
|
|
199
|
+
path: selection,
|
|
200
|
+
type: "event",
|
|
191
201
|
};
|
|
192
202
|
}}
|
|
193
203
|
>
|
|
@@ -195,18 +205,18 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
195
205
|
</sp-action-button>
|
|
196
206
|
</div>
|
|
197
207
|
`
|
|
198
|
-
:
|
|
208
|
+
: expression
|
|
199
209
|
? html`
|
|
200
210
|
<div class="event-body-row">
|
|
201
211
|
${renderExpressionEditor(
|
|
202
|
-
|
|
203
|
-
(newNode:
|
|
212
|
+
expression,
|
|
213
|
+
(newNode: unknown) =>
|
|
204
214
|
transactDoc(activeTab.value, (t) =>
|
|
205
215
|
mutateUpdateProperty(t, selection, evKey, {
|
|
206
216
|
$expression: newNode,
|
|
207
217
|
}),
|
|
208
218
|
),
|
|
209
|
-
{ stateDefs: Object.keys(defs)
|
|
219
|
+
{ allowEventRef: true, stateDefs: Object.keys(defs) },
|
|
210
220
|
)}
|
|
211
221
|
</div>
|
|
212
222
|
`
|
|
@@ -214,7 +224,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
214
224
|
<sp-picker
|
|
215
225
|
size="s"
|
|
216
226
|
class="event-handler"
|
|
217
|
-
.value=${live(
|
|
227
|
+
.value=${live(refValue || "__none__")}
|
|
218
228
|
@change=${(e: Event) => {
|
|
219
229
|
if (
|
|
220
230
|
(e.target as HTMLInputElement).value &&
|
|
@@ -227,7 +237,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
227
237
|
);
|
|
228
238
|
} else {
|
|
229
239
|
transactDoc(activeTab.value, (t) =>
|
|
230
|
-
mutateUpdateProperty(t, selection, evKey
|
|
240
|
+
mutateUpdateProperty(t, selection, evKey),
|
|
231
241
|
);
|
|
232
242
|
}
|
|
233
243
|
}}
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -5,23 +5,24 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, nothing } from "lit-html";
|
|
8
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
9
|
+
import type {
|
|
10
|
+
GitBranchesResult,
|
|
11
|
+
GitDiffState,
|
|
12
|
+
GitFileStatus,
|
|
13
|
+
GitStatusResult,
|
|
14
|
+
StudioPlatform,
|
|
15
|
+
} from "../types";
|
|
8
16
|
import { live } from "lit-html/directives/live.js";
|
|
9
17
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
10
18
|
import { getPlatform } from "../platform";
|
|
11
19
|
import { formatForPath } from "../format/format-host";
|
|
12
|
-
import {
|
|
20
|
+
import { projectState, renderOnly, updateUi } from "../store";
|
|
13
21
|
import { activeTab } from "../workspace/workspace";
|
|
14
22
|
import { view } from "../view";
|
|
15
|
-
import {
|
|
23
|
+
import { showConfirmDialog, showDialog } from "../ui/layers";
|
|
16
24
|
import { statusMessage } from "./statusbar";
|
|
17
25
|
import { publishToGithub } from "../github/github-publish";
|
|
18
|
-
import type { StudioPlatform } from "../types";
|
|
19
|
-
|
|
20
|
-
interface GitFileEntry {
|
|
21
|
-
path: string;
|
|
22
|
-
status: string;
|
|
23
|
-
staged: boolean;
|
|
24
|
-
}
|
|
25
26
|
|
|
26
27
|
interface GitLogEntry {
|
|
27
28
|
hash: string;
|
|
@@ -30,25 +31,21 @@ interface GitLogEntry {
|
|
|
30
31
|
date: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
type GitFileEntry = GitFileStatus;
|
|
35
|
+
|
|
33
36
|
interface GitUiState {
|
|
34
|
-
gitStatus?:
|
|
35
|
-
|
|
36
|
-
branch?: string;
|
|
37
|
-
ahead?: number;
|
|
38
|
-
behind?: number;
|
|
39
|
-
isRepo?: boolean;
|
|
40
|
-
remotes?: string[];
|
|
41
|
-
} | null;
|
|
42
|
-
gitBranches?: { current?: string; branches?: string[] } | null;
|
|
37
|
+
gitStatus?: Partial<GitStatusResult> | null;
|
|
38
|
+
gitBranches?: Partial<GitBranchesResult> | null;
|
|
43
39
|
gitLoading?: boolean;
|
|
44
40
|
gitError?: string | null;
|
|
45
41
|
gitCommitMessage?: string;
|
|
46
42
|
gitLogEntries?: GitLogEntry[] | null;
|
|
47
|
-
[key: string]: unknown;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
export async function refreshGitStatus() {
|
|
51
|
-
if (!projectState)
|
|
46
|
+
if (!projectState) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
52
49
|
const plat = getPlatform();
|
|
53
50
|
updateUi("gitLoading", true);
|
|
54
51
|
updateUi("gitError", null);
|
|
@@ -57,8 +54,8 @@ export async function refreshGitStatus() {
|
|
|
57
54
|
updateUi("gitStatus", status);
|
|
58
55
|
updateUi("gitBranches", branches);
|
|
59
56
|
_lastUpdated = new Date();
|
|
60
|
-
} catch (
|
|
61
|
-
updateUi("gitError", (
|
|
57
|
+
} catch (error) {
|
|
58
|
+
updateUi("gitError", errorMessage(error));
|
|
62
59
|
} finally {
|
|
63
60
|
updateUi("gitLoading", false);
|
|
64
61
|
renderOnly("leftPanel");
|
|
@@ -104,7 +101,9 @@ export async function cloneRepository(ctx: { openRecentProject: (root: string) =
|
|
|
104
101
|
`,
|
|
105
102
|
);
|
|
106
103
|
|
|
107
|
-
if (!url)
|
|
104
|
+
if (!url) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
108
107
|
|
|
109
108
|
try {
|
|
110
109
|
statusMessage("Cloning repository...");
|
|
@@ -113,14 +112,14 @@ export async function cloneRepository(ctx: { openRecentProject: (root: string) =
|
|
|
113
112
|
statusMessage("Clone complete");
|
|
114
113
|
await ctx.openRecentProject(result.root);
|
|
115
114
|
}
|
|
116
|
-
} catch (
|
|
117
|
-
statusMessage(`Clone failed: ${(
|
|
115
|
+
} catch (error) {
|
|
116
|
+
statusMessage(`Clone failed: ${errorMessage(error)}`);
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
/** @returns {boolean} */
|
|
122
121
|
export function platformSupportsClone() {
|
|
123
|
-
return
|
|
122
|
+
return Boolean(getPlatform().gitClone);
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
/**
|
|
@@ -128,14 +127,15 @@ export function platformSupportsClone() {
|
|
|
128
127
|
* @param {unknown} [body]
|
|
129
128
|
*/
|
|
130
129
|
async function gitAction(action: string, body?: unknown) {
|
|
131
|
-
const plat = getPlatform() as Record<string,
|
|
130
|
+
const plat = getPlatform() as Record<string, (...args: unknown[]) => Promise<unknown>> &
|
|
131
|
+
StudioPlatform;
|
|
132
132
|
updateUi("gitLoading", true);
|
|
133
133
|
updateUi("gitError", null);
|
|
134
134
|
try {
|
|
135
135
|
await plat[action](body);
|
|
136
136
|
await refreshGitStatus();
|
|
137
|
-
} catch (
|
|
138
|
-
updateUi("gitError", (
|
|
137
|
+
} catch (error) {
|
|
138
|
+
updateUi("gitError", errorMessage(error));
|
|
139
139
|
updateUi("gitLoading", false);
|
|
140
140
|
renderOnly("leftPanel");
|
|
141
141
|
}
|
|
@@ -151,8 +151,8 @@ async function fetchGitLog() {
|
|
|
151
151
|
const entries = await plat.gitLog(30);
|
|
152
152
|
updateUi("gitLogEntries", entries);
|
|
153
153
|
renderOnly("leftPanel");
|
|
154
|
-
} catch (
|
|
155
|
-
updateUi("gitError", (
|
|
154
|
+
} catch (error) {
|
|
155
|
+
updateUi("gitError", errorMessage(error));
|
|
156
156
|
renderOnly("leftPanel");
|
|
157
157
|
}
|
|
158
158
|
}
|
|
@@ -161,7 +161,7 @@ async function fetchGitLog() {
|
|
|
161
161
|
* @param {{ ui: GitUiState }} S
|
|
162
162
|
* @param {{
|
|
163
163
|
* setCanvasMode?: (mode: string) => void;
|
|
164
|
-
* setGitDiffState?: (state:
|
|
164
|
+
* setGitDiffState?: (state: GitDiffState | null) => void;
|
|
165
165
|
* cloneRepository?: () => void;
|
|
166
166
|
* }} ctx
|
|
167
167
|
*/
|
|
@@ -169,7 +169,7 @@ export function renderGitPanel(
|
|
|
169
169
|
S: { ui: GitUiState },
|
|
170
170
|
ctx: {
|
|
171
171
|
setCanvasMode?: (mode: string) => void;
|
|
172
|
-
setGitDiffState?: (state:
|
|
172
|
+
setGitDiffState?: (state: GitDiffState | null) => void;
|
|
173
173
|
cloneRepository?: () => void;
|
|
174
174
|
},
|
|
175
175
|
) {
|
|
@@ -231,8 +231,10 @@ export function renderGitPanel(
|
|
|
231
231
|
|
|
232
232
|
if (!_pollTimer) {
|
|
233
233
|
_pollTimer = setInterval(() => {
|
|
234
|
-
if (view.leftTab === "git" && !S.ui.gitLoading)
|
|
235
|
-
|
|
234
|
+
if (view.leftTab === "git" && !S.ui.gitLoading) {
|
|
235
|
+
refreshGitStatus();
|
|
236
|
+
}
|
|
237
|
+
}, 30_000);
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
const stagedFiles = status?.files?.filter((f: GitFileEntry) => f.staged) || [];
|
|
@@ -242,7 +244,9 @@ export function renderGitPanel(
|
|
|
242
244
|
const doCommit = async () => {
|
|
243
245
|
const tab = activeTab.value;
|
|
244
246
|
const msg = tab?.session.ui.gitCommitMessage?.trim();
|
|
245
|
-
if (!msg)
|
|
247
|
+
if (!msg) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
246
250
|
updateUi("gitCommitMessage", "");
|
|
247
251
|
await gitAction("gitCommit", msg);
|
|
248
252
|
};
|
|
@@ -250,7 +254,9 @@ export function renderGitPanel(
|
|
|
250
254
|
const doCommitAndSync = async () => {
|
|
251
255
|
const tab = activeTab.value;
|
|
252
256
|
const msg = tab?.session.ui.gitCommitMessage?.trim();
|
|
253
|
-
if (!msg)
|
|
257
|
+
if (!msg) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
254
260
|
updateUi("gitCommitMessage", "");
|
|
255
261
|
updateUi("gitLoading", true);
|
|
256
262
|
updateUi("gitError", null);
|
|
@@ -259,8 +265,8 @@ export function renderGitPanel(
|
|
|
259
265
|
await plat.gitCommit(msg);
|
|
260
266
|
await plat.gitPush();
|
|
261
267
|
await refreshGitStatus();
|
|
262
|
-
} catch (
|
|
263
|
-
updateUi("gitError", (
|
|
268
|
+
} catch (error) {
|
|
269
|
+
updateUi("gitError", errorMessage(error));
|
|
264
270
|
updateUi("gitLoading", false);
|
|
265
271
|
renderOnly("leftPanel");
|
|
266
272
|
}
|
|
@@ -386,11 +392,16 @@ export function renderGitPanel(
|
|
|
386
392
|
const val = (e.target as HTMLInputElement).value;
|
|
387
393
|
if (val === "__new__") {
|
|
388
394
|
(e.target as HTMLInputElement).value = branches?.current || "";
|
|
395
|
+
// oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
|
|
389
396
|
const name = prompt("New branch name:");
|
|
390
|
-
if (name?.trim())
|
|
397
|
+
if (name?.trim()) {
|
|
398
|
+
await gitAction("gitCreateBranch", name.trim());
|
|
399
|
+
}
|
|
391
400
|
return;
|
|
392
401
|
}
|
|
393
|
-
if (val !== branches?.current)
|
|
402
|
+
if (val !== branches?.current) {
|
|
403
|
+
await gitAction("gitCheckout", val);
|
|
404
|
+
}
|
|
394
405
|
}}
|
|
395
406
|
>
|
|
396
407
|
${(branches?.branches || []).map(
|
|
@@ -405,7 +416,9 @@ export function renderGitPanel(
|
|
|
405
416
|
// ─── 3. Tabs: Local Changes / History ────────────────────────────────────
|
|
406
417
|
const switchTab = (tab: string) => {
|
|
407
418
|
_gitSubTab = tab;
|
|
408
|
-
if (tab === "history" && !S.ui.gitLogEntries)
|
|
419
|
+
if (tab === "history" && !S.ui.gitLogEntries) {
|
|
420
|
+
fetchGitLog();
|
|
421
|
+
}
|
|
409
422
|
renderOnly("leftPanel");
|
|
410
423
|
};
|
|
411
424
|
|
|
@@ -461,7 +474,9 @@ export function renderGitPanel(
|
|
|
461
474
|
const menu = (
|
|
462
475
|
(e.currentTarget as HTMLElement).parentElement as HTMLElement
|
|
463
476
|
).querySelector(".git-split-menu");
|
|
464
|
-
if (menu)
|
|
477
|
+
if (menu) {
|
|
478
|
+
menu.toggleAttribute("hidden");
|
|
479
|
+
}
|
|
465
480
|
}}
|
|
466
481
|
>
|
|
467
482
|
<sp-icon-chevron-down slot="icon" size="xs"></sp-icon-chevron-down>
|
|
@@ -492,8 +507,12 @@ export function renderGitPanel(
|
|
|
492
507
|
const dir = parts.join("/");
|
|
493
508
|
|
|
494
509
|
const onFileClick = async () => {
|
|
495
|
-
if (file.status !== "M" && file.status !== "A")
|
|
496
|
-
|
|
510
|
+
if (file.status !== "M" && file.status !== "A") {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!file.path.endsWith(".json") && !formatForPath(file.path)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
497
516
|
|
|
498
517
|
try {
|
|
499
518
|
const plat = getPlatform();
|
|
@@ -507,20 +526,22 @@ export function renderGitPanel(
|
|
|
507
526
|
]);
|
|
508
527
|
|
|
509
528
|
const diffState = {
|
|
510
|
-
filePath: file.path,
|
|
511
|
-
originalContent,
|
|
512
529
|
currentContent,
|
|
530
|
+
filePath: file.path,
|
|
513
531
|
fileStatus: file.status,
|
|
532
|
+
originalContent,
|
|
514
533
|
};
|
|
515
534
|
|
|
516
535
|
updateUi("gitDiffState", diffState);
|
|
517
536
|
|
|
518
537
|
if (ctx?.setCanvasMode) {
|
|
519
|
-
if (ctx.setGitDiffState)
|
|
538
|
+
if (ctx.setGitDiffState) {
|
|
539
|
+
ctx.setGitDiffState(diffState);
|
|
540
|
+
}
|
|
520
541
|
ctx.setCanvasMode("git-diff");
|
|
521
542
|
}
|
|
522
|
-
} catch (
|
|
523
|
-
updateUi("gitError", `Failed to load diff: ${(
|
|
543
|
+
} catch (error) {
|
|
544
|
+
updateUi("gitError", `Failed to load diff: ${errorMessage(error)}`);
|
|
524
545
|
} finally {
|
|
525
546
|
updateUi("gitLoading", false);
|
|
526
547
|
}
|
|
@@ -555,13 +576,17 @@ export function renderGitPanel(
|
|
|
555
576
|
quiet
|
|
556
577
|
title="Discard changes"
|
|
557
578
|
@click=${async () => {
|
|
558
|
-
if (file.status === "U")
|
|
579
|
+
if (file.status === "U") {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
559
582
|
const confirmed = await showConfirmDialog(
|
|
560
583
|
"Discard Changes",
|
|
561
584
|
`Discard changes to ${file.path}?`,
|
|
562
585
|
{ confirmLabel: "Discard", destructive: true },
|
|
563
586
|
);
|
|
564
|
-
if (!confirmed)
|
|
587
|
+
if (!confirmed) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
565
590
|
await gitAction("gitDiscard", [file.path]);
|
|
566
591
|
}}
|
|
567
592
|
?disabled=${file.status === "U"}
|
|
@@ -585,16 +610,18 @@ export function renderGitPanel(
|
|
|
585
610
|
|
|
586
611
|
/** Group files by component (parent directory for .json/.class.json, or "Other") */
|
|
587
612
|
const groupFilesByComponent = (files: GitFileEntry[]) => {
|
|
588
|
-
const groups
|
|
613
|
+
const groups = new Map<string, GitFileEntry[]>();
|
|
589
614
|
for (const f of files) {
|
|
590
615
|
const parts = f.path.split("/");
|
|
591
616
|
let component;
|
|
592
617
|
if (f.path.endsWith(".json") || f.path.endsWith(".class.json") || formatForPath(f.path)) {
|
|
593
|
-
component = parts.length > 1 ? `/${parts
|
|
618
|
+
component = parts.length > 1 ? `/${parts.at(-2)}` : `/${parts[0]}`;
|
|
594
619
|
} else {
|
|
595
620
|
component = "Other";
|
|
596
621
|
}
|
|
597
|
-
if (!groups.has(component))
|
|
622
|
+
if (!groups.has(component)) {
|
|
623
|
+
groups.set(component, []);
|
|
624
|
+
}
|
|
598
625
|
(groups.get(component) as GitFileEntry[]).push(f);
|
|
599
626
|
}
|
|
600
627
|
return groups;
|
|
@@ -708,13 +735,21 @@ function _relativeDate(iso: string) {
|
|
|
708
735
|
const d = new Date(iso);
|
|
709
736
|
const now = Date.now();
|
|
710
737
|
const diff = now - d.getTime();
|
|
711
|
-
const mins = Math.floor(diff /
|
|
712
|
-
if (mins < 1)
|
|
713
|
-
|
|
738
|
+
const mins = Math.floor(diff / 60_000);
|
|
739
|
+
if (mins < 1) {
|
|
740
|
+
return "just now";
|
|
741
|
+
}
|
|
742
|
+
if (mins < 60) {
|
|
743
|
+
return `${mins}m ago`;
|
|
744
|
+
}
|
|
714
745
|
const hours = Math.floor(mins / 60);
|
|
715
|
-
if (hours < 24)
|
|
746
|
+
if (hours < 24) {
|
|
747
|
+
return `${hours}h ago`;
|
|
748
|
+
}
|
|
716
749
|
const days = Math.floor(hours / 24);
|
|
717
|
-
if (days < 30)
|
|
750
|
+
if (days < 30) {
|
|
751
|
+
return `${days}d ago`;
|
|
752
|
+
}
|
|
718
753
|
return d.toLocaleDateString();
|
|
719
754
|
}
|
|
720
755
|
|