@jxsuite/studio 0.30.1 → 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.
@@ -943,7 +943,7 @@ function renderFunctionFields(
943
943
  </div>
944
944
  <textarea
945
945
  class="field-input"
946
- style="min-height:60px;font-family:monospace;font-size:11px"
946
+ style="min-height:60px;font-family:var(--font-mono);font-size:var(--spectrum-font-size-50, 11px)"
947
947
  .value=${def.body || ""}
948
948
  @input=${(e: Event) => {
949
949
  const v = (e.target as HTMLInputElement).value;
@@ -981,7 +981,7 @@ function renderParameterEditorTemplate(
981
981
  ${params.map(
982
982
  (p: CemParameter, i: number) => html`
983
983
  <span
984
- style="display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:3px;background:var(--bg-hover);font-size:11px;font-family:monospace"
984
+ style="display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:var(--radius);background:var(--hover-bg);font-size:var(--spectrum-font-size-50, 11px);font-family:var(--font-mono)"
985
985
  >
986
986
  ${p.name || "?"}
987
987
  <span
@@ -1002,7 +1002,7 @@ function renderParameterEditorTemplate(
1002
1002
  )}
1003
1003
  <input
1004
1004
  class="field-input"
1005
- style="width:60px;flex:0 0 auto;font-size:11px"
1005
+ style="width:60px;flex:0 0 auto;font-size:var(--spectrum-font-size-50, 11px)"
1006
1006
  placeholder="+"
1007
1007
  @keydown=${(e: KeyboardEvent) => {
1008
1008
  if (e.key === "Enter" && (e.target as HTMLInputElement).value.trim()) {
@@ -1145,7 +1145,7 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
1145
1145
 
1146
1146
  return html`
1147
1147
  <div
1148
- style="font-size:11px;font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
1148
+ style="font-size:var(--spectrum-font-size-50, 11px);font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
1149
1149
  >
1150
1150
  Emits
1151
1151
  </div>
@@ -1474,7 +1474,7 @@ export function renderSchemaFieldsTemplate(
1474
1474
  ${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
1475
1475
  ([k, v]) => html`
1476
1476
  <span
1477
- style="background:var(--bg-alt);padding:1px 6px;border-radius:3px;font-size:10px;color:var(--fg-dim)"
1477
+ style="background:var(--bg);padding:1px 6px;border-radius:var(--radius);font-size:10px;color:var(--fg-dim)"
1478
1478
  >${k}: ${v.type ?? "any"}</span
1479
1479
  >
1480
1480
  `,
@@ -1642,7 +1642,7 @@ export function renderExternalPrototypeEditorTemplate(
1642
1642
  } else {
1643
1643
  // Trigger async load — will re-render when cached
1644
1644
  schemaContent = html`<div
1645
- style="padding:4px 0;font-size:11px;color:var(--fg-dim);font-style:italic"
1645
+ style="padding:4px 0;font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim);font-style:italic"
1646
1646
  >
1647
1647
  Loading schema…
1648
1648
  </div>`;
@@ -1658,7 +1658,10 @@ export function renderExternalPrototypeEditorTemplate(
1658
1658
 
1659
1659
  return html`
1660
1660
  ${importedPath
1661
- ? html`<div class="signal-hint" style="padding:4px 0 2px;font-size:11px;color:var(--fg-dim)">
1661
+ ? html`<div
1662
+ class="signal-hint"
1663
+ style="padding:4px 0 2px;font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim)"
1664
+ >
1662
1665
  ${def.$prototype}
1663
1666
  </div>`
1664
1667
  : html`
@@ -804,7 +804,7 @@ function styleSidebarTemplate(
804
804
  (rule) => html`
805
805
  <div style="display:flex;align-items:center;gap:4px">
806
806
  <button
807
- style="flex:1;text-align:left;padding:6px 10px;background:var(--spectrum-gray-200, #1a1a1a);border:none;border-radius:4px;color:var(--spectrum-gray-900, #fafafa);font-size:12px;cursor:pointer"
807
+ style="flex:1;text-align:left;padding:6px 10px;background:var(--spectrum-gray-200, #1a1a1a);border:none;border-radius:var(--radius);color:var(--spectrum-gray-900, #fafafa);font-size:var(--spectrum-font-size-75, 12px);cursor:pointer"
808
808
  @click=${() => {
809
809
  const newSelector = activeSelector ? `${activeSelector} ${rule}` : rule;
810
810
  selectStylebookTag(newSelector, undefined, {
@@ -821,7 +821,7 @@ function styleSidebarTemplate(
821
821
  `,
822
822
  )}
823
823
  <button
824
- style="padding:6px 10px;background:none;border:1px dashed var(--spectrum-gray-400, #333);border-radius:4px;color:var(--spectrum-gray-700, #a1a1aa);font-size:12px;cursor:pointer"
824
+ style="padding:6px 10px;background:none;border:1px dashed var(--spectrum-gray-400, #333);border-radius:var(--radius);color:var(--spectrum-gray-700, #a1a1aa);font-size:var(--spectrum-font-size-75, 12px);cursor:pointer"
825
825
  @click=${() => {
826
826
  // oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
827
827
  const name = prompt("Selector name (e.g. th, :hover, .active):");
@@ -111,19 +111,19 @@ export function renderStylebookLayersTemplate(ctx: {
111
111
  const style = rootStyle;
112
112
  const vars = Object.entries(style).filter(([k]) => k.startsWith("--"));
113
113
  if (vars.length === 0) {
114
- return html`<div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:12px">
114
+ return html`<div
115
+ style="padding:16px;text-align:center;color:var(--fg-dim);font-size:var(--spectrum-font-size-75, 12px)"
116
+ >
115
117
  No variables defined
116
118
  </div>`;
117
119
  }
118
120
  return html`${vars.map(
119
121
  ([k, v]) => html`
120
122
  <div class="layer-row">
121
- <span class="layer-tag" style="font-size:10px;font-family:'SF Mono','Fira Code',monospace"
122
- >var</span
123
- >
123
+ <span class="layer-tag" style="font-size:10px;font-family:var(--font-mono)">var</span>
124
124
  <span class="layer-label">${k}</span>
125
125
  <span
126
- style="font-size:11px;color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px"
126
+ style="font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px"
127
127
  >${String(v)}</span
128
128
  >
129
129
  </div>
@@ -592,7 +592,7 @@ export async function renderComponentPreview(comp: ComponentEntry) {
592
592
  function _componentFallback(tagName: string) {
593
593
  const fallback = document.createElement("div");
594
594
  fallback.style.cssText =
595
- "padding:12px;border:1px dashed var(--border);border-radius:4px;color:var(--fg-dim)";
595
+ "padding:12px;border:1px dashed var(--border);border-radius:var(--radius);color:var(--fg-dim)";
596
596
  fallback.textContent = `<${tagName}>`;
597
597
  return fallback;
598
598
  }
@@ -421,6 +421,44 @@ export function createDevServerPlatform() {
421
421
  return await res.json();
422
422
  },
423
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
+
424
462
  // ─── Code services (optional) ─────────────────────────────────────────
425
463
 
426
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
@@ -74,6 +74,8 @@ import { mountResizeEdges } from "./resize-edges";
74
74
  import { codeService } from "./services/code-services";
75
75
  import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/signals-panel";
76
76
  import { loadComponentRegistry } from "./files/components";
77
+ import { ensureDependenciesInstalled } from "./packages/ensure-deps";
78
+ import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
77
79
 
78
80
  import { html, render as litRender } from "lit-html";
79
81
 
@@ -560,6 +562,7 @@ if (_projectParam) {
560
562
  selectedPath: siteCtx.fileRelPath || null,
561
563
  });
562
564
 
565
+ await ensureDependenciesInstalled();
563
566
  await loadComponentRegistry();
564
567
 
565
568
  // Load directory tree and populate projectDirs from conventional dirs found
@@ -584,6 +587,7 @@ if (_projectParam) {
584
587
  }
585
588
  }
586
589
  requireProjectState().projectDirs = foundDirs;
590
+ void maybePromptJxsuiteUpdate(siteCtx.sitePath);
587
591
  }
588
592
 
589
593
  // Read and open the file
@@ -713,6 +717,7 @@ async function openRecentProject(root: string) {
713
717
  selectedPath: null,
714
718
  });
715
719
 
720
+ await ensureDependenciesInstalled();
716
721
  await loadDirectory(".");
717
722
  await loadComponentRegistry();
718
723
 
@@ -741,6 +746,7 @@ async function openRecentProject(root: string) {
741
746
 
742
747
  await openHomePage();
743
748
  ensureFsSync();
749
+ void maybePromptJxsuiteUpdate(root);
744
750
  } catch (error) {
745
751
  statusMessage(`Error: ${errorMessage(error)}`);
746
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 {
@@ -125,6 +155,28 @@ export interface StudioPlatform {
125
155
  addPackage: (name: string) => Promise<unknown>;
126
156
  removePackage: (name: string) => Promise<unknown>;
127
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>;
128
180
  codeService: (action: string, payload: unknown) => Promise<CodeServiceResult | null>;
129
181
  resolveSiteContext: (filePath: string) => Promise<{
130
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`
@@ -223,7 +223,7 @@ export function rawTextArea(
223
223
  const { onInput, onCommit } = makeHandlers(key, ms, commit, opts.commitMode);
224
224
  const style = [
225
225
  `min-height:${opts.minHeight ?? "40px"}`,
226
- opts.mono ? "font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:11px" : "",
226
+ opts.mono ? "font-family:var(--font-mono);font-size:var(--spectrum-font-size-50,11px)" : "",
227
227
  opts.style ?? "",
228
228
  ]
229
229
  .filter(Boolean)
@@ -208,7 +208,7 @@ function renderMediaPickerPopover() {
208
208
  slot="icon"
209
209
  src=${m.path}
210
210
  alt=""
211
- style="width:24px;height:24px;object-fit:cover;border-radius:2px"
211
+ style="width:24px;height:24px;object-fit:cover;border-radius:var(--spectrum-corner-radius-75, 2px)"
212
212
  />`
213
213
  : nothing}
214
214
  ${m.name}