@jxsuite/studio 0.6.1 → 0.7.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 +151438 -141129
- package/dist/studio.js.map +84 -18
- package/package.json +2 -2
- package/src/markdown/md-convert.js +18 -16
- package/src/panels/activity-bar.js +22 -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 +141 -0
- package/src/panels/right-panel.js +3 -2
- 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/platforms/devserver.js +113 -0
- package/src/state.js +7 -0
- package/src/studio.js +38 -1490
- package/src/ui/spectrum.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
33
|
+
"@jxsuite/runtime": "^0.6.2",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.0",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.0",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.0",
|
|
@@ -14,6 +14,7 @@ import { unified } from "unified";
|
|
|
14
14
|
import remarkStringify from "remark-stringify";
|
|
15
15
|
import remarkDirective from "remark-directive";
|
|
16
16
|
import { MD_ALL } from "./md-allowlist.js";
|
|
17
|
+
import { htmlToJx } from "@jxsuite/parser/transpile";
|
|
17
18
|
|
|
18
19
|
// ─── mdast → Jx ──────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -40,7 +41,6 @@ const MDAST_TAG_MAP = {
|
|
|
40
41
|
table: () => "table",
|
|
41
42
|
tableRow: () => "tr",
|
|
42
43
|
tableCell: (/** @type {any} */ n) => (n.isHeader ? "th" : "td"),
|
|
43
|
-
html: () => "div",
|
|
44
44
|
break: () => "br",
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -55,7 +55,7 @@ export function mdToJx(mdast) {
|
|
|
55
55
|
return {
|
|
56
56
|
children: (mdast.children ?? [])
|
|
57
57
|
.filter((/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml")
|
|
58
|
-
.
|
|
58
|
+
.flatMap(convertMdastNode)
|
|
59
59
|
.filter(Boolean),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -78,6 +78,12 @@ function convertMdastNode(node) {
|
|
|
78
78
|
return convertDirective(node);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (node.type === "html") {
|
|
82
|
+
if (!node.value) return null;
|
|
83
|
+
const nodes = htmlToJx(node.value);
|
|
84
|
+
return nodes.length === 1 ? nodes[0] : { tagName: "div", children: nodes };
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
const tagFn = MDAST_TAG_MAP[node.type];
|
|
82
88
|
if (!tagFn) return null;
|
|
83
89
|
|
|
@@ -92,7 +98,7 @@ function convertMdastNode(node) {
|
|
|
92
98
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
93
99
|
el.textContent = node.children[0].value;
|
|
94
100
|
} else if (node.children?.length > 0) {
|
|
95
|
-
el.children = node.children.
|
|
101
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
96
102
|
}
|
|
97
103
|
break;
|
|
98
104
|
}
|
|
@@ -107,7 +113,7 @@ function convertMdastNode(node) {
|
|
|
107
113
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
108
114
|
el.textContent = node.children[0].value;
|
|
109
115
|
} else if (node.children?.length > 0) {
|
|
110
|
-
el.children = node.children.
|
|
116
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
111
117
|
}
|
|
112
118
|
break;
|
|
113
119
|
}
|
|
@@ -122,7 +128,7 @@ function convertMdastNode(node) {
|
|
|
122
128
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
123
129
|
el.textContent = node.children[0].value;
|
|
124
130
|
} else if (node.children?.length > 0) {
|
|
125
|
-
el.children = node.children.
|
|
131
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
126
132
|
}
|
|
127
133
|
break;
|
|
128
134
|
|
|
@@ -134,13 +140,13 @@ function convertMdastNode(node) {
|
|
|
134
140
|
case "blockquote":
|
|
135
141
|
case "listItem":
|
|
136
142
|
if (node.children?.length > 0) {
|
|
137
|
-
el.children = node.children.
|
|
143
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
138
144
|
}
|
|
139
145
|
break;
|
|
140
146
|
|
|
141
147
|
case "list":
|
|
142
148
|
if (node.children?.length > 0) {
|
|
143
|
-
el.children = node.children.
|
|
149
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
144
150
|
}
|
|
145
151
|
if (node.start != null && node.start !== 1) {
|
|
146
152
|
el.attributes = { start: String(node.start) };
|
|
@@ -165,7 +171,7 @@ function convertMdastNode(node) {
|
|
|
165
171
|
|
|
166
172
|
case "table": {
|
|
167
173
|
// Mdast tables have rows directly; split into thead/tbody
|
|
168
|
-
const rows = (node.children ?? []).
|
|
174
|
+
const rows = (node.children ?? []).flatMap(convertMdastNode).filter(Boolean);
|
|
169
175
|
const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
|
|
170
176
|
const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
|
|
171
177
|
el.children = [thead, tbody].filter(Boolean);
|
|
@@ -174,7 +180,7 @@ function convertMdastNode(node) {
|
|
|
174
180
|
|
|
175
181
|
case "tableRow":
|
|
176
182
|
if (node.children?.length > 0) {
|
|
177
|
-
el.children = node.children.
|
|
183
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
178
184
|
}
|
|
179
185
|
break;
|
|
180
186
|
|
|
@@ -182,13 +188,9 @@ function convertMdastNode(node) {
|
|
|
182
188
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
183
189
|
el.textContent = node.children[0].value;
|
|
184
190
|
} else if (node.children?.length > 0) {
|
|
185
|
-
el.children = node.children.
|
|
191
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
186
192
|
}
|
|
187
193
|
break;
|
|
188
|
-
|
|
189
|
-
case "html":
|
|
190
|
-
el.innerHTML = node.value;
|
|
191
|
-
break;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
return el;
|
|
@@ -209,10 +211,10 @@ function convertDirective(node) {
|
|
|
209
211
|
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
210
212
|
el.textContent = node.children[0].value;
|
|
211
213
|
} else if (node.children?.length > 0) {
|
|
212
|
-
el.children = node.children.
|
|
214
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
213
215
|
}
|
|
214
216
|
} else if (node.type === "containerDirective" && node.children?.length > 0) {
|
|
215
|
-
el.children = node.children.
|
|
217
|
+
el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
|
|
216
218
|
}
|
|
217
219
|
return el;
|
|
218
220
|
}
|
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
import { html, render as litRender, nothing } from "lit-html";
|
|
4
4
|
import { activityBar, update, getState, renderOnly } from "../store.js";
|
|
5
5
|
|
|
6
|
+
const gitBranchIcon = (/** @type {any} */ s) => html`
|
|
7
|
+
<svg
|
|
8
|
+
slot="icon"
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
width=${s === "m" ? 20 : 16}
|
|
11
|
+
height=${s === "m" ? 20 : 16}
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
stroke-width="2"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
stroke-linejoin="round"
|
|
18
|
+
>
|
|
19
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
20
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
21
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
22
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
23
|
+
</svg>
|
|
24
|
+
`;
|
|
25
|
+
|
|
6
26
|
/**
|
|
7
27
|
* @param {any} tag
|
|
8
28
|
* @param {any} size
|
|
@@ -32,6 +52,7 @@ export function tabIcon(tag, size) {
|
|
|
32
52
|
html`<sp-icon-artboard slot="icon" size=${s}></sp-icon-artboard>`,
|
|
33
53
|
"sp-icon-box": (/** @type {any} */ s) =>
|
|
34
54
|
html`<sp-icon-box slot="icon" size=${s}></sp-icon-box>`,
|
|
55
|
+
"sp-icon-git-branch": gitBranchIcon,
|
|
35
56
|
};
|
|
36
57
|
const fn = m[tag];
|
|
37
58
|
return fn ? fn(size || "s") : nothing;
|
|
@@ -47,6 +68,7 @@ export function renderActivityBar(S) {
|
|
|
47
68
|
{ value: "state", icon: "sp-icon-brackets", label: "State" },
|
|
48
69
|
{ value: "data", icon: "sp-icon-data", label: "Data" },
|
|
49
70
|
{ value: "head", icon: "sp-icon-file-single-web-page", label: "Head" },
|
|
71
|
+
{ value: "git", icon: "sp-icon-git-branch", label: "Source Control" },
|
|
50
72
|
];
|
|
51
73
|
const tpl = html`
|
|
52
74
|
<sp-tabs
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Elements panel — block/component palette with categorized accordion and search filter. */
|
|
2
|
+
|
|
3
|
+
import { html, nothing } from "lit-html";
|
|
4
|
+
import { getState, update, getNodeAtPath, insertNode } from "../store.js";
|
|
5
|
+
import { view } from "../view.js";
|
|
6
|
+
import { getEffectiveElements } from "../site-context.js";
|
|
7
|
+
import { componentRegistry } from "../files/components.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ webdata: any; defaultDef: (tag: string) => any; rerender: () => void }} ctx
|
|
11
|
+
* @returns {import("lit-html").TemplateResult}
|
|
12
|
+
*/
|
|
13
|
+
export function renderElementsTemplate(ctx) {
|
|
14
|
+
const S = getState();
|
|
15
|
+
|
|
16
|
+
const categories = Object.entries(ctx.webdata.elements).map(
|
|
17
|
+
(/** @type {any} */ [category, elements]) => {
|
|
18
|
+
const filtered = view.elementsFilter
|
|
19
|
+
? elements.filter((/** @type {any} */ e) => e.tag.includes(view.elementsFilter))
|
|
20
|
+
: elements;
|
|
21
|
+
if (filtered.length === 0) return nothing;
|
|
22
|
+
|
|
23
|
+
return html`
|
|
24
|
+
<sp-accordion-item
|
|
25
|
+
label=${category}
|
|
26
|
+
?open=${!view.elementsCollapsed.has(category)}
|
|
27
|
+
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
28
|
+
if (e.target.open) view.elementsCollapsed.delete(category);
|
|
29
|
+
else view.elementsCollapsed.add(category);
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
${filtered.map((/** @type {any} */ { tag }) => {
|
|
33
|
+
const def = ctx.defaultDef(tag);
|
|
34
|
+
return html`
|
|
35
|
+
<div
|
|
36
|
+
class="element-card"
|
|
37
|
+
data-block-tag=${tag}
|
|
38
|
+
@click=${() => {
|
|
39
|
+
const s = getState();
|
|
40
|
+
const parentPath = s.selection || [];
|
|
41
|
+
const parent = getNodeAtPath(s.document, parentPath);
|
|
42
|
+
const idx = parent?.children ? parent.children.length : 0;
|
|
43
|
+
update(insertNode(s, parentPath, idx, structuredClone(def)));
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<div class="element-card-preview"></div>
|
|
47
|
+
<div class="element-card-label"><${tag}></div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
})}
|
|
51
|
+
</sp-accordion-item>
|
|
52
|
+
`;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const effectiveEls = getEffectiveElements(S.document?.$elements);
|
|
57
|
+
/** @type {Set<string>} */
|
|
58
|
+
const enabledTags = new Set();
|
|
59
|
+
for (const entry of effectiveEls) {
|
|
60
|
+
if (typeof entry !== "string") continue;
|
|
61
|
+
const comp = componentRegistry.find(
|
|
62
|
+
(/** @type {any} */ c) =>
|
|
63
|
+
c.source === "npm" && c.modulePath && entry === `${c.package}/${c.modulePath}`,
|
|
64
|
+
);
|
|
65
|
+
if (comp) {
|
|
66
|
+
enabledTags.add(comp.tagName);
|
|
67
|
+
} else {
|
|
68
|
+
for (const c of componentRegistry) {
|
|
69
|
+
if (c.source === "npm" && c.package === entry) enabledTags.add(c.tagName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const compsFiltered =
|
|
74
|
+
componentRegistry.length > 0
|
|
75
|
+
? componentRegistry
|
|
76
|
+
.filter((/** @type {any} */ c) => c.source !== "npm" || enabledTags.has(c.tagName))
|
|
77
|
+
.filter(
|
|
78
|
+
(/** @type {any} */ c) =>
|
|
79
|
+
!view.elementsFilter || c.tagName.toLowerCase().includes(view.elementsFilter),
|
|
80
|
+
)
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
const componentsAccordion =
|
|
84
|
+
compsFiltered.length > 0
|
|
85
|
+
? html`
|
|
86
|
+
<sp-accordion-item
|
|
87
|
+
label="Components"
|
|
88
|
+
?open=${!view.elementsCollapsed.has("Components")}
|
|
89
|
+
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
90
|
+
if (e.target.open) view.elementsCollapsed.delete("Components");
|
|
91
|
+
else view.elementsCollapsed.add("Components");
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div class="components-section">
|
|
95
|
+
${compsFiltered.map(
|
|
96
|
+
(/** @type {any} */ comp) => html`
|
|
97
|
+
<div
|
|
98
|
+
class="element-card"
|
|
99
|
+
data-component-tag=${comp.tagName}
|
|
100
|
+
title=${comp.source === "npm"
|
|
101
|
+
? `${comp.package}: <${comp.tagName}>`
|
|
102
|
+
: comp.path}
|
|
103
|
+
@click=${() => {
|
|
104
|
+
const s = getState();
|
|
105
|
+
const parentPath = s.selection || [];
|
|
106
|
+
const parent = getNodeAtPath(s.document, parentPath);
|
|
107
|
+
const idx = parent?.children ? parent.children.length : 0;
|
|
108
|
+
const instanceDef = {
|
|
109
|
+
tagName: comp.tagName,
|
|
110
|
+
$props: Object.fromEntries(
|
|
111
|
+
(comp.props || []).map((/** @type {any} */ p) => [
|
|
112
|
+
p.name,
|
|
113
|
+
p.default !== undefined ? p.default : "",
|
|
114
|
+
]),
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
update(insertNode(s, parentPath, idx, structuredClone(instanceDef)));
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<div class="element-card-preview">
|
|
121
|
+
<span style="color:var(--fg-dim);font-size:11px;font-style:italic"
|
|
122
|
+
><${comp.tagName}></span
|
|
123
|
+
>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="element-card-label">${comp.tagName}</div>
|
|
126
|
+
</div>
|
|
127
|
+
`,
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</sp-accordion-item>
|
|
131
|
+
`
|
|
132
|
+
: nothing;
|
|
133
|
+
|
|
134
|
+
return html`
|
|
135
|
+
<sp-search
|
|
136
|
+
size="s"
|
|
137
|
+
placeholder="Filter elements…"
|
|
138
|
+
value=${view.elementsFilter}
|
|
139
|
+
@input=${(/** @type {any} */ e) => {
|
|
140
|
+
view.elementsFilter = e.target.value.toLowerCase();
|
|
141
|
+
ctx.rerender();
|
|
142
|
+
}}
|
|
143
|
+
></sp-search>
|
|
144
|
+
<sp-accordion class="elements-list" allow-multiple
|
|
145
|
+
>${componentsAccordion}${categories}</sp-accordion
|
|
146
|
+
>
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
@@ -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
|
+
}
|