@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.
Files changed (41) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +4628 -2543
  3. package/dist/studio.js.map +58 -32
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +13 -11
  8. package/src/about/about-modal.ts +156 -0
  9. package/src/editor/context-menu.ts +4 -2
  10. package/src/files/files.ts +50 -18
  11. package/src/files/fs-events.ts +162 -0
  12. package/src/packages/ensure-deps.ts +39 -0
  13. package/src/packages/jxsuite-update.ts +131 -0
  14. package/src/packages/semver.ts +54 -0
  15. package/src/panels/activity-bar.ts +13 -1
  16. package/src/panels/ai-panel.ts +3 -1
  17. package/src/panels/dnd.ts +2 -2
  18. package/src/panels/elements-panel.ts +2 -1
  19. package/src/panels/layers-panel.ts +5 -1
  20. package/src/panels/preview-render.ts +1 -1
  21. package/src/panels/properties-panel.ts +20 -14
  22. package/src/panels/signals-panel.ts +29 -18
  23. package/src/panels/style-panel.ts +2 -2
  24. package/src/panels/stylebook-layers-panel.ts +5 -5
  25. package/src/panels/stylebook-panel.ts +1 -1
  26. package/src/platforms/devserver.ts +92 -2
  27. package/src/settings/dependencies-editor.ts +262 -0
  28. package/src/settings/general-settings.ts +5 -3
  29. package/src/settings/settings-modal.ts +6 -0
  30. package/src/studio.ts +21 -2
  31. package/src/tabs/tab.ts +10 -4
  32. package/src/types.ts +82 -1
  33. package/src/ui/expression-editor.ts +4 -3
  34. package/src/ui/field-input.ts +1 -1
  35. package/src/ui/media-picker.ts +1 -1
  36. package/src/ui/panel-resize.ts +8 -7
  37. package/src/ui/progress-modal.ts +94 -0
  38. package/src/ui/spectrum.ts +4 -0
  39. package/src/ui/unit-selector.ts +3 -0
  40. package/src/utils/canvas-media.ts +3 -3
  41. 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:4px;padding:2px"
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:4px;display:flex;align-items:center;justify-content:center;color:var(--fg-dim);font-size:11px"
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)">${currentFavicon}</span>`
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
- return _openProject({
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
- /** @returns {TabUi} */
94
- function createDefaultUi() {
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: "edit",
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<void>;
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 style="font-size:12px;color:var(--spectrum-global-color-gray-600)"
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-global-color-gray-600);min-width:30px">
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-global-color-gray-300);margin-left:8px;padding-left:8px;"
500
+ ? "border-left:2px solid var(--spectrum-gray-300, #3c3c3c);margin-left:8px;padding-left:8px;"
500
501
  : "";
501
502
 
502
503
  return html`