@jxsuite/studio 0.29.0 → 0.31.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 +4628 -2543
- package/dist/studio.js.map +58 -32
- 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 +13 -11
- package/src/about/about-modal.ts +156 -0
- package/src/editor/context-menu.ts +4 -2
- package/src/files/files.ts +50 -18
- package/src/files/fs-events.ts +162 -0
- package/src/packages/ensure-deps.ts +39 -0
- package/src/packages/jxsuite-update.ts +131 -0
- package/src/packages/semver.ts +54 -0
- package/src/panels/activity-bar.ts +13 -1
- package/src/panels/ai-panel.ts +3 -1
- package/src/panels/dnd.ts +2 -2
- package/src/panels/elements-panel.ts +2 -1
- package/src/panels/layers-panel.ts +5 -1
- package/src/panels/preview-render.ts +1 -1
- package/src/panels/properties-panel.ts +20 -14
- package/src/panels/signals-panel.ts +29 -18
- package/src/panels/style-panel.ts +2 -2
- package/src/panels/stylebook-layers-panel.ts +5 -5
- package/src/panels/stylebook-panel.ts +1 -1
- package/src/platforms/devserver.ts +92 -2
- package/src/settings/dependencies-editor.ts +262 -0
- package/src/settings/general-settings.ts +5 -3
- package/src/settings/settings-modal.ts +6 -0
- package/src/studio.ts +21 -2
- package/src/tabs/tab.ts +10 -4
- package/src/types.ts +82 -1
- package/src/ui/expression-editor.ts +4 -3
- package/src/ui/field-input.ts +1 -1
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +8 -7
- package/src/ui/progress-modal.ts +94 -0
- package/src/ui/spectrum.ts +4 -0
- package/src/ui/unit-selector.ts +3 -0
- package/src/utils/canvas-media.ts +3 -3
- package/src/version.ts +34 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
12
|
-
import type { DirEntry } from "../types";
|
|
12
|
+
import type { DirEntry, FsEvent, RenameResult } from "../types";
|
|
13
13
|
|
|
14
14
|
/** A directory entry from the server, tolerating extra wire fields. */
|
|
15
15
|
type WireDirEntry = DirEntry & Record<string, unknown>;
|
|
@@ -298,7 +298,7 @@ export function createDevServerPlatform() {
|
|
|
298
298
|
* @param {string} from
|
|
299
299
|
* @param {string} to
|
|
300
300
|
*/
|
|
301
|
-
async renameFile(from: string, to: string) {
|
|
301
|
+
async renameFile(from: string, to: string): Promise<RenameResult> {
|
|
302
302
|
const res = await fetch("/__studio/file/rename", {
|
|
303
303
|
body: JSON.stringify({ from: serverPath(from), to: serverPath(to) }),
|
|
304
304
|
headers: { "Content-Type": "application/json" },
|
|
@@ -307,6 +307,58 @@ export function createDevServerPlatform() {
|
|
|
307
307
|
if (!res.ok) {
|
|
308
308
|
throw new Error(`Failed to rename: ${from} → ${to}`);
|
|
309
309
|
}
|
|
310
|
+
const report = await readJson<RenameResult>(res);
|
|
311
|
+
// Map server-root-relative report paths back to project-relative for the studio.
|
|
312
|
+
if (typeof report.from === "string") {
|
|
313
|
+
report.from = stripRoot(report.from);
|
|
314
|
+
}
|
|
315
|
+
if (typeof report.to === "string") {
|
|
316
|
+
report.to = stripRoot(report.to);
|
|
317
|
+
}
|
|
318
|
+
for (const f of report.references?.files ?? []) {
|
|
319
|
+
f.path = stripRoot(f.path);
|
|
320
|
+
}
|
|
321
|
+
for (const e of report.errors ?? []) {
|
|
322
|
+
e.path = stripRoot(e.path);
|
|
323
|
+
}
|
|
324
|
+
return report;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Subscribe to filesystem change events over the dev server's SSE stream. Listens for the named
|
|
329
|
+
* "fs" event (the preview iframe's default `onmessage` ignores it), strips paths to
|
|
330
|
+
* project-relative, and drops events for sibling projects outside the active root.
|
|
331
|
+
*/
|
|
332
|
+
subscribeFileEvents(handler: (events: FsEvent[]) => void) {
|
|
333
|
+
if (typeof EventSource === "undefined") {
|
|
334
|
+
return () => {};
|
|
335
|
+
}
|
|
336
|
+
const es = new EventSource("/__reload");
|
|
337
|
+
es.addEventListener("fs", (ev: MessageEvent) => {
|
|
338
|
+
let payload: { events?: FsEvent[] };
|
|
339
|
+
try {
|
|
340
|
+
payload = JSON.parse(ev.data as string) as { events?: FsEvent[] };
|
|
341
|
+
} catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const events: FsEvent[] = [];
|
|
345
|
+
for (const e of payload.events ?? []) {
|
|
346
|
+
const raw = e.path.replaceAll("\\", "/");
|
|
347
|
+
if (_projectRoot && raw !== _projectRoot && !raw.startsWith(`${_projectRoot}/`)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const path = stripRoot(raw);
|
|
351
|
+
if (path && !path.startsWith("..")) {
|
|
352
|
+
events.push({ isDir: e.isDir, path, type: e.type });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (events.length > 0) {
|
|
356
|
+
handler(events);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return () => {
|
|
360
|
+
es.close();
|
|
361
|
+
};
|
|
310
362
|
},
|
|
311
363
|
|
|
312
364
|
/** @param {string} _path */
|
|
@@ -369,6 +421,44 @@ export function createDevServerPlatform() {
|
|
|
369
421
|
return await res.json();
|
|
370
422
|
},
|
|
371
423
|
|
|
424
|
+
async installDependencies() {
|
|
425
|
+
const res = await fetch("/__studio/packages/install", { method: "POST" });
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
return { log: await res.text(), ok: false };
|
|
428
|
+
}
|
|
429
|
+
return await res.json();
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
async dependenciesNeedInstall() {
|
|
433
|
+
const res = await fetch("/__studio/packages/needs-install");
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
const data = (await res.json()) as { needsInstall?: boolean };
|
|
438
|
+
return Boolean(data.needsInstall);
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
async outdatedPackages() {
|
|
442
|
+
const res = await fetch("/__studio/packages/outdated");
|
|
443
|
+
if (!res.ok) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
return await res.json();
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/** @param {{ name: string; version: string; dev?: boolean }[]} updates */
|
|
450
|
+
async setPackageVersions(updates: { name: string; version: string; dev?: boolean }[]) {
|
|
451
|
+
const res = await fetch("/__studio/packages/set-versions", {
|
|
452
|
+
body: JSON.stringify({ updates }),
|
|
453
|
+
headers: { "Content-Type": "application/json" },
|
|
454
|
+
method: "POST",
|
|
455
|
+
});
|
|
456
|
+
if (!res.ok) {
|
|
457
|
+
return { log: await res.text(), ok: false };
|
|
458
|
+
}
|
|
459
|
+
return await res.json();
|
|
460
|
+
},
|
|
461
|
+
|
|
372
462
|
// ─── Code services (optional) ─────────────────────────────────────────
|
|
373
463
|
|
|
374
464
|
/**
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Dependencies settings section — an outdated-aware table of the project's npm dependencies. Shows
|
|
4
|
+
* current vs latest version, with per-row update/remove, an add-package field, "update all", and
|
|
5
|
+
* reinstall. @jxsuite/* rows target the version this Studio build embeds; other packages target the
|
|
6
|
+
* newest published version reported by the backend.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { html, render as litRender } from "lit-html";
|
|
10
|
+
import { getPlatform } from "../platform";
|
|
11
|
+
import { VERSION } from "../version";
|
|
12
|
+
import { statusMessage } from "../panels/statusbar";
|
|
13
|
+
import { showProgressModal } from "../ui/progress-modal";
|
|
14
|
+
import { isComparable, isUpgrade, stripRange } from "../packages/semver";
|
|
15
|
+
import type { PackageInfo } from "../types";
|
|
16
|
+
|
|
17
|
+
const JXSUITE_PREFIX = "@jxsuite/";
|
|
18
|
+
|
|
19
|
+
interface Update {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
dev: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _container: HTMLElement | null = null;
|
|
26
|
+
let _packages: PackageInfo[] | null = null;
|
|
27
|
+
let _latest = new Map<string, string>();
|
|
28
|
+
let _busy = false;
|
|
29
|
+
let _addName = "";
|
|
30
|
+
|
|
31
|
+
/** The version a package can be updated to, or null if it is already current. */
|
|
32
|
+
function latestFor(p: PackageInfo): string | null {
|
|
33
|
+
if (p.name.startsWith(JXSUITE_PREFIX) && isComparable(VERSION) && isUpgrade(p.version, VERSION)) {
|
|
34
|
+
return VERSION;
|
|
35
|
+
}
|
|
36
|
+
const registry = _latest.get(p.name);
|
|
37
|
+
return registry && isUpgrade(p.version, registry) ? registry : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function load() {
|
|
41
|
+
const platform = getPlatform();
|
|
42
|
+
try {
|
|
43
|
+
_packages = await platform.listPackages();
|
|
44
|
+
} catch {
|
|
45
|
+
_packages = [];
|
|
46
|
+
}
|
|
47
|
+
_latest = new Map();
|
|
48
|
+
if (platform.outdatedPackages) {
|
|
49
|
+
try {
|
|
50
|
+
for (const o of await platform.outdatedPackages()) {
|
|
51
|
+
_latest.set(o.name, o.latest);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
/* Registry lookups are best-effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
render();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function withBusy(fn: () => Promise<void>) {
|
|
61
|
+
if (_busy) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
_busy = true;
|
|
65
|
+
render();
|
|
66
|
+
const progress = showProgressModal({ status: "Running bun…", title: "Updating dependencies" });
|
|
67
|
+
try {
|
|
68
|
+
await fn();
|
|
69
|
+
progress.done();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
progress.fail(error instanceof Error ? error.message : String(error));
|
|
72
|
+
} finally {
|
|
73
|
+
_busy = false;
|
|
74
|
+
await load();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function onAdd() {
|
|
79
|
+
const name = _addName.trim();
|
|
80
|
+
if (!name) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await withBusy(async () => {
|
|
84
|
+
await getPlatform().addPackage(name);
|
|
85
|
+
_addName = "";
|
|
86
|
+
statusMessage(`Added ${name}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function onRemove(p: PackageInfo) {
|
|
91
|
+
await withBusy(async () => {
|
|
92
|
+
await getPlatform().removePackage(p.name);
|
|
93
|
+
statusMessage(`Removed ${p.name}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function onUpdate(p: PackageInfo, latest: string) {
|
|
98
|
+
const platform = getPlatform();
|
|
99
|
+
if (!platform.setPackageVersions) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await withBusy(async () => {
|
|
103
|
+
const target = stripRange(latest);
|
|
104
|
+
const res = await platform.setPackageVersions!([
|
|
105
|
+
{ dev: Boolean(p.dev), name: p.name, version: `^${target}` },
|
|
106
|
+
]);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(res.log ?? "Update failed");
|
|
109
|
+
}
|
|
110
|
+
statusMessage(`Updated ${p.name} to ${target}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function onUpdateAll() {
|
|
115
|
+
const platform = getPlatform();
|
|
116
|
+
if (!platform.setPackageVersions) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const updates = (_packages ?? [])
|
|
120
|
+
.map((p): Update | null => {
|
|
121
|
+
const latest = latestFor(p);
|
|
122
|
+
return latest
|
|
123
|
+
? { dev: Boolean(p.dev), name: p.name, version: `^${stripRange(latest)}` }
|
|
124
|
+
: null;
|
|
125
|
+
})
|
|
126
|
+
.filter((u): u is Update => u !== null);
|
|
127
|
+
if (updates.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await withBusy(async () => {
|
|
131
|
+
const res = await platform.setPackageVersions!(updates);
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(res.log ?? "Update failed");
|
|
134
|
+
}
|
|
135
|
+
statusMessage(`Updated ${updates.length} package(s)`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function onReinstall() {
|
|
140
|
+
const platform = getPlatform();
|
|
141
|
+
if (!platform.installDependencies) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await withBusy(async () => {
|
|
145
|
+
const res = await platform.installDependencies!();
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
throw new Error(res.log ?? "Install failed");
|
|
148
|
+
}
|
|
149
|
+
statusMessage("Dependencies reinstalled");
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function row(p: PackageInfo) {
|
|
154
|
+
const latest = latestFor(p);
|
|
155
|
+
return html`
|
|
156
|
+
<sp-table-row>
|
|
157
|
+
<sp-table-cell>
|
|
158
|
+
${p.name}${p.dev
|
|
159
|
+
? html`<span style="color:var(--fg-dim);font-size:10px"> · dev</span>`
|
|
160
|
+
: ""}
|
|
161
|
+
</sp-table-cell>
|
|
162
|
+
<sp-table-cell>${p.version}</sp-table-cell>
|
|
163
|
+
<sp-table-cell>${latest ?? "—"}</sp-table-cell>
|
|
164
|
+
<sp-table-cell>
|
|
165
|
+
${latest
|
|
166
|
+
? html`<sp-action-button
|
|
167
|
+
size="s"
|
|
168
|
+
quiet
|
|
169
|
+
?disabled=${_busy}
|
|
170
|
+
title="Update to ${latest}"
|
|
171
|
+
@click=${() => onUpdate(p, latest)}
|
|
172
|
+
>
|
|
173
|
+
<sp-icon-refresh slot="icon"></sp-icon-refresh>
|
|
174
|
+
</sp-action-button>`
|
|
175
|
+
: ""}
|
|
176
|
+
<sp-action-button
|
|
177
|
+
size="s"
|
|
178
|
+
quiet
|
|
179
|
+
?disabled=${_busy}
|
|
180
|
+
title="Remove"
|
|
181
|
+
@click=${() => onRemove(p)}
|
|
182
|
+
>
|
|
183
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
184
|
+
</sp-action-button>
|
|
185
|
+
</sp-table-cell>
|
|
186
|
+
</sp-table-row>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function render() {
|
|
191
|
+
if (!_container) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const pkgs = _packages ?? [];
|
|
195
|
+
const hasUpdates = pkgs.some((p) => latestFor(p) !== null);
|
|
196
|
+
|
|
197
|
+
const tpl = html`
|
|
198
|
+
<div class="settings-section">
|
|
199
|
+
<h3 class="settings-section-title">Dependencies</h3>
|
|
200
|
+
<p class="settings-field-desc">Manage this project's npm dependencies.</p>
|
|
201
|
+
|
|
202
|
+
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
|
|
203
|
+
<sp-textfield
|
|
204
|
+
size="s"
|
|
205
|
+
placeholder="package-name"
|
|
206
|
+
.value=${_addName}
|
|
207
|
+
?disabled=${_busy}
|
|
208
|
+
@input=${(e: Event) => {
|
|
209
|
+
_addName = (e.target as HTMLInputElement).value;
|
|
210
|
+
}}
|
|
211
|
+
></sp-textfield>
|
|
212
|
+
<sp-action-button size="s" ?disabled=${_busy} @click=${onAdd}>
|
|
213
|
+
<sp-icon-add slot="icon"></sp-icon-add>
|
|
214
|
+
Add
|
|
215
|
+
</sp-action-button>
|
|
216
|
+
<span style="flex:1"></span>
|
|
217
|
+
${hasUpdates
|
|
218
|
+
? html`<sp-action-button size="s" ?disabled=${_busy} @click=${onUpdateAll}>
|
|
219
|
+
Update all
|
|
220
|
+
</sp-action-button>`
|
|
221
|
+
: ""}
|
|
222
|
+
<sp-action-button
|
|
223
|
+
size="s"
|
|
224
|
+
quiet
|
|
225
|
+
?disabled=${_busy}
|
|
226
|
+
title="Reinstall (bun install)"
|
|
227
|
+
@click=${onReinstall}
|
|
228
|
+
>
|
|
229
|
+
<sp-icon-refresh slot="icon"></sp-icon-refresh>
|
|
230
|
+
Reinstall
|
|
231
|
+
</sp-action-button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
${_packages === null
|
|
235
|
+
? html`<p class="about-muted">Loading…</p>`
|
|
236
|
+
: pkgs.length === 0
|
|
237
|
+
? html`<p class="about-muted">No dependencies.</p>`
|
|
238
|
+
: html`
|
|
239
|
+
<sp-table size="s">
|
|
240
|
+
<sp-table-head>
|
|
241
|
+
<sp-table-head-cell>Package</sp-table-head-cell>
|
|
242
|
+
<sp-table-head-cell>Current</sp-table-head-cell>
|
|
243
|
+
<sp-table-head-cell>Latest</sp-table-head-cell>
|
|
244
|
+
<sp-table-head-cell></sp-table-head-cell>
|
|
245
|
+
</sp-table-head>
|
|
246
|
+
<sp-table-body> ${pkgs.map((p) => row(p))} </sp-table-body>
|
|
247
|
+
</sp-table>
|
|
248
|
+
`}
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
litRender(tpl, _container);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {HTMLElement} container */
|
|
256
|
+
export function renderDependenciesEditor(container: HTMLElement) {
|
|
257
|
+
_container = container;
|
|
258
|
+
_packages = null;
|
|
259
|
+
_addName = "";
|
|
260
|
+
render();
|
|
261
|
+
void load();
|
|
262
|
+
}
|
|
@@ -94,16 +94,18 @@ export function renderGeneralSettings(container: HTMLElement) {
|
|
|
94
94
|
? html`<img
|
|
95
95
|
src=${currentFavicon}
|
|
96
96
|
alt="Current favicon"
|
|
97
|
-
style="width:32px;height:32px;object-fit:contain;border:1px solid var(--border);border-radius:
|
|
97
|
+
style="width:32px;height:32px;object-fit:contain;border:1px solid var(--border);border-radius:var(--radius);padding:2px"
|
|
98
98
|
/>`
|
|
99
99
|
: html`<div
|
|
100
|
-
style="width:32px;height:32px;border:1px dashed var(--border);border-radius:
|
|
100
|
+
style="width:32px;height:32px;border:1px dashed var(--border);border-radius:var(--radius);display:flex;align-items:center;justify-content:center;color:var(--fg-dim);font-size:var(--spectrum-font-size-50, 11px)"
|
|
101
101
|
>
|
|
102
102
|
—
|
|
103
103
|
</div>`}
|
|
104
104
|
<sp-action-button size="s" @click=${onFaviconUpload}> Upload Favicon </sp-action-button>
|
|
105
105
|
${currentFavicon
|
|
106
|
-
? html`<span style="font-size:11px;color:var(--fg-dim)"
|
|
106
|
+
? html`<span style="font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim)"
|
|
107
|
+
>${currentFavicon}</span
|
|
108
|
+
>`
|
|
107
109
|
: nothing}
|
|
108
110
|
</div>
|
|
109
111
|
</div>
|
|
@@ -13,6 +13,7 @@ import { renderContentTypesEditor } from "./content-types-editor";
|
|
|
13
13
|
import { renderCssVarsEditor } from "./css-vars-editor";
|
|
14
14
|
import { renderHeadEditor } from "./head-editor";
|
|
15
15
|
import { renderGeneralSettings } from "./general-settings";
|
|
16
|
+
import { renderDependenciesEditor } from "./dependencies-editor";
|
|
16
17
|
import { openModal } from "../ui/layers";
|
|
17
18
|
|
|
18
19
|
let _handle: ReturnType<typeof openModal> | null = null;
|
|
@@ -27,6 +28,7 @@ const sections = [
|
|
|
27
28
|
{ icon: "sp-icon-brush", key: "cssVars", label: "CSS Variables" },
|
|
28
29
|
{ icon: "sp-icon-data", key: "definitions", label: "Definitions" },
|
|
29
30
|
{ icon: "sp-icon-view-grid", key: "contentTypes", label: "Content Types" },
|
|
31
|
+
{ icon: "sp-icon-box", key: "dependencies", label: "Dependencies" },
|
|
30
32
|
];
|
|
31
33
|
|
|
32
34
|
export function openSettingsModal() {
|
|
@@ -131,6 +133,10 @@ function renderActiveSection() {
|
|
|
131
133
|
renderContentTypesEditor(_contentEl);
|
|
132
134
|
break;
|
|
133
135
|
}
|
|
136
|
+
case "dependencies": {
|
|
137
|
+
renderDependenciesEditor(_contentEl);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
134
140
|
default: {
|
|
135
141
|
break;
|
|
136
142
|
}
|
package/src/studio.ts
CHANGED
|
@@ -59,8 +59,10 @@ import {
|
|
|
59
59
|
openFileInTab,
|
|
60
60
|
openHomePage,
|
|
61
61
|
registerFileTreeDnD,
|
|
62
|
+
reloadCleanTab,
|
|
62
63
|
setupTreeKeyboard,
|
|
63
64
|
} from "./files/files";
|
|
65
|
+
import { startFsSync } from "./files/fs-events";
|
|
64
66
|
import { renderImportsTemplate } from "./panels/imports-panel";
|
|
65
67
|
import { renderHeadTemplate } from "./panels/head-panel";
|
|
66
68
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
|
|
@@ -72,6 +74,8 @@ import { mountResizeEdges } from "./resize-edges";
|
|
|
72
74
|
import { codeService } from "./services/code-services";
|
|
73
75
|
import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/signals-panel";
|
|
74
76
|
import { loadComponentRegistry } from "./files/components";
|
|
77
|
+
import { ensureDependenciesInstalled } from "./packages/ensure-deps";
|
|
78
|
+
import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
|
|
75
79
|
|
|
76
80
|
import { html, render as litRender } from "lit-html";
|
|
77
81
|
|
|
@@ -506,6 +510,13 @@ function safeRenderRightPanel() {
|
|
|
506
510
|
// Now that renderers are registered, bootstrap
|
|
507
511
|
registerFunctionCompletions();
|
|
508
512
|
|
|
513
|
+
let fsUnsub: (() => void) | null = null;
|
|
514
|
+
/** (Re)subscribe the sidebar to backend filesystem events for the active project. */
|
|
515
|
+
function ensureFsSync() {
|
|
516
|
+
fsUnsub?.();
|
|
517
|
+
fsUnsub = startFsSync({ onContentChange: reloadCleanTab, renderLeftPanel });
|
|
518
|
+
}
|
|
519
|
+
|
|
509
520
|
const _urlParams = new URLSearchParams(location.search);
|
|
510
521
|
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
511
522
|
|
|
@@ -551,6 +562,7 @@ if (_projectParam) {
|
|
|
551
562
|
selectedPath: siteCtx.fileRelPath || null,
|
|
552
563
|
});
|
|
553
564
|
|
|
565
|
+
await ensureDependenciesInstalled();
|
|
554
566
|
await loadComponentRegistry();
|
|
555
567
|
|
|
556
568
|
// Load directory tree and populate projectDirs from conventional dirs found
|
|
@@ -575,6 +587,7 @@ if (_projectParam) {
|
|
|
575
587
|
}
|
|
576
588
|
}
|
|
577
589
|
requireProjectState().projectDirs = foundDirs;
|
|
590
|
+
void maybePromptJxsuiteUpdate(siteCtx.sitePath);
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
// Read and open the file
|
|
@@ -646,6 +659,7 @@ if (_projectParam) {
|
|
|
646
659
|
// Normal mode: probe for project at server root
|
|
647
660
|
void loadProject();
|
|
648
661
|
render();
|
|
662
|
+
ensureFsSync();
|
|
649
663
|
}
|
|
650
664
|
|
|
651
665
|
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
@@ -657,11 +671,13 @@ function renderLeftPanel() {
|
|
|
657
671
|
function loadProject() {
|
|
658
672
|
return _loadProject();
|
|
659
673
|
}
|
|
660
|
-
function openProject() {
|
|
661
|
-
|
|
674
|
+
async function openProject() {
|
|
675
|
+
const result = await _openProject({
|
|
662
676
|
renderActivityBar: () => renderActivityBar(),
|
|
663
677
|
renderLeftPanel,
|
|
664
678
|
});
|
|
679
|
+
ensureFsSync();
|
|
680
|
+
return result;
|
|
665
681
|
}
|
|
666
682
|
async function openRecentProject(root: string) {
|
|
667
683
|
try {
|
|
@@ -701,6 +717,7 @@ async function openRecentProject(root: string) {
|
|
|
701
717
|
selectedPath: null,
|
|
702
718
|
});
|
|
703
719
|
|
|
720
|
+
await ensureDependenciesInstalled();
|
|
704
721
|
await loadDirectory(".");
|
|
705
722
|
await loadComponentRegistry();
|
|
706
723
|
|
|
@@ -728,6 +745,8 @@ async function openRecentProject(root: string) {
|
|
|
728
745
|
statusMessage(`Opened project: ${requireProjectState().name}`);
|
|
729
746
|
|
|
730
747
|
await openHomePage();
|
|
748
|
+
ensureFsSync();
|
|
749
|
+
void maybePromptJxsuiteUpdate(root);
|
|
731
750
|
} catch (error) {
|
|
732
751
|
statusMessage(`Error: ${errorMessage(error)}`);
|
|
733
752
|
}
|
package/src/tabs/tab.ts
CHANGED
|
@@ -90,12 +90,15 @@ export interface Tab {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} canvasMode — initial canvas mode (the tab's first allowed mode)
|
|
95
|
+
* @returns {TabUi}
|
|
96
|
+
*/
|
|
97
|
+
function createDefaultUi(canvasMode: string) {
|
|
95
98
|
return {
|
|
96
99
|
activeMedia: null,
|
|
97
100
|
activeSelector: null,
|
|
98
|
-
canvasMode
|
|
101
|
+
canvasMode,
|
|
99
102
|
editingFunction: null,
|
|
100
103
|
featureToggles: {},
|
|
101
104
|
gitBranches: null,
|
|
@@ -160,6 +163,9 @@ export function createTab({
|
|
|
160
163
|
normalizeArrayChildren(document);
|
|
161
164
|
|
|
162
165
|
const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
|
|
166
|
+
// A tab opens in its first allowed mode — never one the toolbar would disable.
|
|
167
|
+
// Formats author mode order so the default comes first (edit, stylebook, etc.).
|
|
168
|
+
const initialCanvasMode = resolvedModes[0] ?? "edit";
|
|
163
169
|
|
|
164
170
|
const tab = scope.run(() => ({
|
|
165
171
|
capabilities: { modes: resolvedModes },
|
|
@@ -190,7 +196,7 @@ export function createTab({
|
|
|
190
196
|
documentStack: [],
|
|
191
197
|
hover: null,
|
|
192
198
|
selection: null,
|
|
193
|
-
ui: createDefaultUi(),
|
|
199
|
+
ui: createDefaultUi(initialCanvasMode),
|
|
194
200
|
}),
|
|
195
201
|
})) as unknown as Tab;
|
|
196
202
|
|
package/src/types.ts
CHANGED
|
@@ -53,6 +53,36 @@ export interface ComponentMeta {
|
|
|
53
53
|
export interface PackageInfo {
|
|
54
54
|
name: string;
|
|
55
55
|
version: string;
|
|
56
|
+
/** True when the dependency lives in `devDependencies` rather than `dependencies`. */
|
|
57
|
+
dev?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
|
|
61
|
+
export interface OutdatedInfo {
|
|
62
|
+
name: string;
|
|
63
|
+
/** The version range pinned in package.json (e.g. "^0.19.0"). */
|
|
64
|
+
current: string;
|
|
65
|
+
/** The newest published version. */
|
|
66
|
+
latest: string;
|
|
67
|
+
/** The newest version satisfying the current range, if known. */
|
|
68
|
+
wanted?: string;
|
|
69
|
+
dev?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Result of a package mutation that runs `bun install` (install / set-versions). */
|
|
73
|
+
export interface PackageOpResult {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
/** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
|
|
76
|
+
log?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Desktop app/build info surfaced in the About screen. */
|
|
80
|
+
export interface AppInfo {
|
|
81
|
+
version: string;
|
|
82
|
+
channel: string;
|
|
83
|
+
hash: string;
|
|
84
|
+
/** Human-readable update status (e.g. "Up to date", "Update available"), if known. */
|
|
85
|
+
updateStatus?: string;
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
export interface CodeServiceResult {
|
|
@@ -69,6 +99,30 @@ export interface DirEntry {
|
|
|
69
99
|
modified?: string;
|
|
70
100
|
}
|
|
71
101
|
|
|
102
|
+
/** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
|
|
103
|
+
export interface FsEvent {
|
|
104
|
+
type: "add" | "change" | "unlink" | "addDir" | "unlinkDir";
|
|
105
|
+
path: string;
|
|
106
|
+
isDir: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Result of a rename, including the references rewritten across the project (refactor report). */
|
|
110
|
+
export interface RenameResult {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
from: string;
|
|
113
|
+
to: string;
|
|
114
|
+
isDir?: boolean;
|
|
115
|
+
references?: {
|
|
116
|
+
filesChanged: number;
|
|
117
|
+
refsUpdated: number;
|
|
118
|
+
files: { path: string; count: number }[];
|
|
119
|
+
};
|
|
120
|
+
errors?: { path: string; error: string }[];
|
|
121
|
+
tag?: { from: string; to: string; filesChanged: number; refsUpdated: number };
|
|
122
|
+
tagSkipped?: string;
|
|
123
|
+
error?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
72
126
|
export interface StudioPlatform {
|
|
73
127
|
id: string;
|
|
74
128
|
projectRoot: string;
|
|
@@ -90,12 +144,39 @@ export interface StudioPlatform {
|
|
|
90
144
|
writeFile: (path: string, content: string) => Promise<void>;
|
|
91
145
|
uploadFile: (path: string, data: string | File | Blob | ArrayBuffer) => Promise<unknown>;
|
|
92
146
|
deleteFile: (path: string) => Promise<void>;
|
|
93
|
-
renameFile: (from: string, to: string) => Promise<
|
|
147
|
+
renameFile: (from: string, to: string) => Promise<RenameResult>;
|
|
94
148
|
createDirectory: (path: string) => Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Subscribe to backend filesystem change events for the active project. Returns an unsubscribe
|
|
151
|
+
* function. Optional: platforms without a watcher omit it and the sidebar stays manual-refresh.
|
|
152
|
+
*/
|
|
153
|
+
subscribeFileEvents?: (handler: (events: FsEvent[]) => void) => () => void;
|
|
95
154
|
discoverComponents: (dir?: string) => Promise<ComponentMeta[]>;
|
|
96
155
|
addPackage: (name: string) => Promise<unknown>;
|
|
97
156
|
removePackage: (name: string) => Promise<unknown>;
|
|
98
157
|
listPackages: () => Promise<PackageInfo[]>;
|
|
158
|
+
/**
|
|
159
|
+
* Run `bun install` in the project root. Optional: platforms without a Bun-capable backend omit
|
|
160
|
+
* it and the install-on-open / reinstall affordances are skipped.
|
|
161
|
+
*/
|
|
162
|
+
installDependencies?: () => Promise<PackageOpResult>;
|
|
163
|
+
/** Whether the project has uninstalled dependencies (node_modules missing). */
|
|
164
|
+
dependenciesNeedInstall?: () => Promise<boolean>;
|
|
165
|
+
/** List dependencies that have a newer version available. */
|
|
166
|
+
outdatedPackages?: () => Promise<OutdatedInfo[]>;
|
|
167
|
+
/**
|
|
168
|
+
* Rewrite the version range of each named package (preserving its dependencies/devDependencies
|
|
169
|
+
* placement) and run `bun install`. Used by the @jxsuite bump and per-dependency updates.
|
|
170
|
+
*/
|
|
171
|
+
setPackageVersions?: (
|
|
172
|
+
updates: { name: string; version: string; dev?: boolean }[],
|
|
173
|
+
) => Promise<PackageOpResult>;
|
|
174
|
+
/**
|
|
175
|
+
* Desktop-only app/build info (release channel, commit hash, update status). Platforms without a
|
|
176
|
+
* native shell (e.g. the dev server) omit it, and the About screen hides the corresponding
|
|
177
|
+
* section.
|
|
178
|
+
*/
|
|
179
|
+
getAppInfo?: () => Promise<AppInfo>;
|
|
99
180
|
codeService: (action: string, payload: unknown) => Promise<CodeServiceResult | null>;
|
|
100
181
|
resolveSiteContext: (filePath: string) => Promise<{
|
|
101
182
|
sitePath: string | null;
|
|
@@ -332,7 +332,8 @@ function renderLiteralEditor(operand: unknown, onChange: (newVal: JxExpressionOp
|
|
|
332
332
|
@change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
|
|
333
333
|
>true</sp-checkbox
|
|
334
334
|
>`
|
|
335
|
-
: html`<span
|
|
335
|
+
: html`<span
|
|
336
|
+
style="font-size:var(--spectrum-font-size-75, 12px);color:var(--spectrum-gray-600, #808080)"
|
|
336
337
|
>null</span
|
|
337
338
|
>`}
|
|
338
339
|
</div>
|
|
@@ -425,7 +426,7 @@ function renderSpliceArgsEditor(
|
|
|
425
426
|
class="array-object-row"
|
|
426
427
|
style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
|
|
427
428
|
>
|
|
428
|
-
<span style="font-size:10px;color:var(--spectrum-
|
|
429
|
+
<span style="font-size:10px;color:var(--spectrum-gray-600, #808080);min-width:30px">
|
|
429
430
|
${labels[idx] ?? "item"}
|
|
430
431
|
</span>
|
|
431
432
|
${renderOperandEditor(
|
|
@@ -496,7 +497,7 @@ export function renderExpressionEditor(
|
|
|
496
497
|
|
|
497
498
|
const nestStyle =
|
|
498
499
|
depth > 0
|
|
499
|
-
? "border-left:2px solid var(--spectrum-
|
|
500
|
+
? "border-left:2px solid var(--spectrum-gray-300, #3c3c3c);margin-left:8px;padding-left:8px;"
|
|
500
501
|
: "";
|
|
501
502
|
|
|
502
503
|
return html`
|