@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
@@ -0,0 +1,131 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * On project open, compare the project's `@jxsuite/*` dependency ranges to the version this Studio
4
+ * build embeds (`VERSION`). If the project is behind, prompt the user to bump them to match and —
5
+ * on confirm — rewrite the ranges and reinstall. The target is the embedded version (not npm
6
+ * latest) so a project always lines up with the runtime/compiler the running app actually uses.
7
+ */
8
+
9
+ import { html } from "lit-html";
10
+ import { getPlatform } from "../platform";
11
+ import { VERSION } from "../version";
12
+ import { showConfirmDialog } from "../ui/layers";
13
+ import { showProgressModal } from "../ui/progress-modal";
14
+ import { statusMessage } from "../panels/statusbar";
15
+ import { isComparable, isUpgrade } from "./semver";
16
+ import type { PackageInfo } from "../types";
17
+
18
+ const JXSUITE_PREFIX = "@jxsuite/";
19
+
20
+ export interface JxsuiteUpdate {
21
+ name: string;
22
+ current: string;
23
+ dev: boolean;
24
+ }
25
+
26
+ /**
27
+ * Compare the project's @jxsuite/* deps to the embedded version. Returns the packages behind the
28
+ * target plus the target version, or null when there's nothing to do (dev build, no project, or all
29
+ * already current/ahead).
30
+ */
31
+ export async function checkJxsuiteUpdate(): Promise<{
32
+ target: string;
33
+ outdated: JxsuiteUpdate[];
34
+ } | null> {
35
+ const target = VERSION;
36
+ if (!isComparable(target)) {
37
+ return null; // "dev" / non-semver build — no reliable target
38
+ }
39
+ const platform = getPlatform();
40
+ let pkgs: PackageInfo[];
41
+ try {
42
+ pkgs = await platform.listPackages();
43
+ } catch {
44
+ return null;
45
+ }
46
+ const outdated: JxsuiteUpdate[] = [];
47
+ for (const p of pkgs) {
48
+ if (!p.name.startsWith(JXSUITE_PREFIX) || !isComparable(p.version)) {
49
+ continue;
50
+ }
51
+ if (isUpgrade(p.version, target)) {
52
+ outdated.push({ current: p.version, dev: Boolean(p.dev), name: p.name });
53
+ }
54
+ }
55
+ return outdated.length > 0 ? { outdated, target } : null;
56
+ }
57
+
58
+ function dismissKey(root: string, target: string): string {
59
+ return `jx:jxsuite-update-dismissed:${root}:${target}`;
60
+ }
61
+
62
+ function isDismissed(root: string, target: string): boolean {
63
+ try {
64
+ return localStorage.getItem(dismissKey(root, target)) === "1";
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function setDismissed(root: string, target: string): void {
71
+ try {
72
+ localStorage.setItem(dismissKey(root, target), "1");
73
+ } catch {
74
+ /* Ignore storage errors */
75
+ }
76
+ }
77
+
78
+ /** Apply a set of @jxsuite bumps to `^target` and reinstall, behind a progress modal. */
79
+ export async function applyJxsuiteUpdate(outdated: JxsuiteUpdate[], target: string): Promise<void> {
80
+ const platform = getPlatform();
81
+ if (!platform.setPackageVersions) {
82
+ return;
83
+ }
84
+ const progress = showProgressModal({
85
+ status: "Updating @jxsuite packages…",
86
+ title: "Updating dependencies",
87
+ });
88
+ try {
89
+ const result = await platform.setPackageVersions(
90
+ outdated.map((p) => ({ dev: p.dev, name: p.name, version: `^${target}` })),
91
+ );
92
+ if (result.ok) {
93
+ progress.done();
94
+ statusMessage(`Updated ${outdated.length} @jxsuite package(s) to ${target}`);
95
+ } else {
96
+ progress.fail(result.log ?? "Update failed");
97
+ }
98
+ } catch (error) {
99
+ progress.fail(error instanceof Error ? error.message : String(error));
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Prompt to update @jxsuite packages on open. Skips when there's nothing to do or the user already
105
+ * declined this exact target for this project (remembered in localStorage).
106
+ */
107
+ export async function maybePromptJxsuiteUpdate(projectRoot: string): Promise<void> {
108
+ const platform = getPlatform();
109
+ if (!platform.setPackageVersions) {
110
+ return;
111
+ }
112
+ const check = await checkJxsuiteUpdate();
113
+ if (!check || isDismissed(projectRoot, check.target)) {
114
+ return;
115
+ }
116
+ const list = check.outdated.map((p) => `${p.name} ${p.current} → ^${check.target}`).join("\n");
117
+ const confirmed = await showConfirmDialog(
118
+ "Update @jxsuite packages?",
119
+ html`This project uses older @jxsuite packages. Update them to match Studio ${check.target}?
120
+ <br /><br /><span
121
+ style="font-size:var(--spectrum-font-size-75, 12px);color:var(--fg-dim);white-space:pre-line"
122
+ >${list}</span
123
+ >`,
124
+ { cancelLabel: "Not now", confirmLabel: "Update" },
125
+ );
126
+ if (!confirmed) {
127
+ setDismissed(projectRoot, check.target);
128
+ return;
129
+ }
130
+ await applyJxsuiteUpdate(check.outdated, check.target);
131
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimal semver helpers for comparing dependency versions in the UI. Deliberately tiny — we only
3
+ * need range stripping and ordering of `x.y.z` triples, not full semver range satisfaction, so we
4
+ * avoid pulling in a dependency.
5
+ */
6
+
7
+ /** Strip a range prefix to its base version: "^0.19.0" → "0.19.0", "~1.2.3" → "1.2.3". */
8
+ export function stripRange(range: string): string {
9
+ const base = range.replace(/^[\^~>=<v\s]+/, "").trim();
10
+ return base.split(/\s+/)[0] ?? "";
11
+ }
12
+
13
+ /**
14
+ * Whether a spec is a comparable registry semver (a plain `x.y.z` range), as opposed to
15
+ * `workspace:`, `file:`, `link:`, a git/URL spec, or a wildcard like `*`/`latest`.
16
+ */
17
+ export function isComparable(spec: string): boolean {
18
+ if (spec.includes(":")) {
19
+ return false;
20
+ }
21
+ return /^\d+\.\d+/.test(stripRange(spec));
22
+ }
23
+
24
+ /** Parse a version into numeric [major, minor, patch], ignoring any pre-release/build suffix. */
25
+ function parts(version: string): [number, number, number] {
26
+ const core = stripRange(version).split(/[-+]/)[0] ?? "";
27
+ const [major = 0, minor = 0, patch = 0] = core.split(".").map((n) => Math.trunc(Number(n)) || 0);
28
+ return [major, minor, patch];
29
+ }
30
+
31
+ /**
32
+ * Compare two versions (range prefixes tolerated). Returns -1 if a < b, 1 if a > b, 0 if equal.
33
+ * Pre-release/build metadata is ignored.
34
+ */
35
+ export function compareSemver(a: string, b: string): number {
36
+ const pa = parts(a);
37
+ const pb = parts(b);
38
+ for (let i = 0; i < 3; i++) {
39
+ const da = pa[i] ?? 0;
40
+ const db = pb[i] ?? 0;
41
+ if (da !== db) {
42
+ return da < db ? -1 : 1;
43
+ }
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ /** Whether `latest` is strictly newer than the base version of `current` (both comparable). */
49
+ export function isUpgrade(current: string, latest: string): boolean {
50
+ if (!isComparable(current) || !isComparable(latest)) {
51
+ return false;
52
+ }
53
+ return compareSemver(latest, current) > 0;
54
+ }
@@ -7,6 +7,7 @@ import { effect, effectScope } from "../reactivity";
7
7
  import { activeTab } from "../workspace/workspace";
8
8
  import { applyPanelCollapse, view } from "../view";
9
9
  import { openSettingsModal } from "../settings/settings-modal";
10
+ import { openAboutModal } from "../about/about-modal";
10
11
  import { refreshGitStatus } from "./git-panel";
11
12
  import type { EffectScope } from "@vue/reactivity";
12
13
  import type { TemplateResult } from "lit-html";
@@ -129,7 +130,18 @@ export function renderActivityBar() {
129
130
  `,
130
131
  )}
131
132
  </sp-tabs>
132
- <div style="margin-top:auto;padding:8px 0;display:flex;justify-content:center">
133
+ <div
134
+ style="margin-top:auto;padding:8px 0;display:flex;flex-direction:column;align-items:center;gap:4px"
135
+ >
136
+ <sp-action-button
137
+ quiet
138
+ size="m"
139
+ title="About"
140
+ aria-label="About"
141
+ @click=${() => openAboutModal()}
142
+ >
143
+ <sp-icon-info slot="icon"></sp-icon-info>
144
+ </sp-action-button>
133
145
  <sp-action-button
134
146
  quiet
135
147
  size="m"
@@ -457,7 +457,9 @@ export function renderAiPanelTemplate() {
457
457
  <div class="ai-status-center">
458
458
  <sp-icon-artboard style="font-size:32px"></sp-icon-artboard>
459
459
  <div>Claude authentication required</div>
460
- <div style="font-size:11px;color:var(--spectrum-global-color-gray-600)">
460
+ <div
461
+ style="font-size:var(--spectrum-font-size-50, 11px);color:var(--spectrum-gray-600, #808080)"
462
+ >
461
463
  Run the following in your terminal:
462
464
  </div>
463
465
  <code class="ai-code-snippet">npx @anthropic-ai/claude-code login</code>
package/src/panels/dnd.ts CHANGED
@@ -67,8 +67,8 @@ export function registerLayersDnD() {
67
67
  for (const row of container.querySelectorAll("[data-dnd-row]") as NodeListOf<HTMLElement>) {
68
68
  const rowPath = (row.dataset.path as string)
69
69
  .split("/")
70
- .map((s: string) => (/^\d+$/.test(s) ? Number.parseInt(s, 10) : s)) as JxPath;
71
- const rowDepth = Number.parseInt(row.dataset.dndDepth as string, 10) || 0;
70
+ .map((s: string) => (/^\d+$/.test(s) ? Math.trunc(Number(s)) : s)) as JxPath;
71
+ const rowDepth = Math.trunc(Number(row.dataset.dndDepth as string)) || 0;
72
72
  const isVoid = Object.hasOwn(row.dataset, "dndVoid");
73
73
  const isExpanded = Object.hasOwn(row.dataset, "dndExpanded");
74
74
 
@@ -141,7 +141,8 @@ export function renderElementsTemplate(ctx: {
141
141
  }}
142
142
  >
143
143
  <div class="element-card-preview">
144
- <span style="color:var(--fg-dim);font-size:11px;font-style:italic"
144
+ <span
145
+ style="color:var(--fg-dim);font-size:var(--spectrum-font-size-50, 11px);font-style:italic"
145
146
  >&lt;${comp.tagName}&gt;</span
146
147
  >
147
148
  </div>
@@ -151,7 +151,11 @@ export function renderLayersTemplate(ctx: {
151
151
  class="layer-row"
152
152
  style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
153
153
  >
154
- <span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
154
+ <span
155
+ class="layer-tag"
156
+ style="background: var(--spectrum-gray-500, #64748b); font-size: 0.65rem;"
157
+ >text</span
158
+ >
155
159
  <span class="layer-label">${textPreview}</span>
156
160
  </div>
157
161
  `,
@@ -122,7 +122,7 @@ export function renderCanvasNode(
122
122
  const placeholder = document.createElement("div");
123
123
  placeholder.textContent = `[$switch: ${keys.join(" | ")}]`;
124
124
  placeholder.style.cssText =
125
- "font-family:monospace;font-size:11px;padding:6px 10px;background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px dashed color-mix(in srgb, var(--danger) 40%, transparent);border-radius:4px;color:var(--danger);font-style:italic";
125
+ "font-family:var(--font-mono);font-size:var(--spectrum-font-size-50,11px);padding:6px 10px;background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px dashed color-mix(in srgb, var(--danger) 40%, transparent);border-radius:var(--radius);color:var(--danger);font-style:italic";
126
126
  el.append(placeholder);
127
127
  }
128
128
 
@@ -331,7 +331,7 @@ function renderSwitchFieldsTemplate(
331
331
  mapSignals,
332
332
  )}
333
333
  <div
334
- style="font-size:11px;font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
334
+ 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"
335
335
  >
336
336
  Cases
337
337
  </div>
@@ -369,7 +369,7 @@ function renderSwitchFieldsTemplate(
369
369
  >→</span
370
370
  >
371
371
  <span
372
- style="cursor:pointer;color:var(--danger);font-size:11px"
372
+ style="cursor:pointer;color:var(--danger);font-size:var(--spectrum-font-size-50, 11px)"
373
373
  @click=${(e: Event) => {
374
374
  e.stopPropagation();
375
375
  transactDoc(activeTab.value!, (t) => mutateRemoveSwitchCase(t, path, caseName));
@@ -657,7 +657,7 @@ function renderMediaFieldsTemplate(node: JxMutableNode) {
657
657
  }}
658
658
  />
659
659
  <span
660
- style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
660
+ style="font-size:10px;color:var(--fg-dim);font-family:var(--font-mono);white-space:nowrap"
661
661
  >${view.addBreakpointPreview}</span
662
662
  >
663
663
  </div>
@@ -714,7 +714,7 @@ function mediaBreakpointRowTemplate(name: string, query: string) {
714
714
  <input
715
715
  class="field-input"
716
716
  .value=${live(mediaDisplayName(name))}
717
- style="flex:1;font-weight:600;font-size:12px"
717
+ style="flex:1;font-weight:600;font-size:var(--spectrum-font-size-75, 12px)"
718
718
  @input=${(e: Event) => {
719
719
  const newKey = friendlyNameToMedia((e.target as HTMLInputElement).value);
720
720
  currentRawLabel = newKey || "";
@@ -738,7 +738,7 @@ function mediaBreakpointRowTemplate(name: string, query: string) {
738
738
  />
739
739
  <span
740
740
  class="bp-raw-label"
741
- style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
741
+ style="font-size:10px;color:var(--fg-dim);font-family:var(--font-mono);white-space:nowrap"
742
742
  >${name}</span
743
743
  >
744
744
  <span
@@ -892,14 +892,18 @@ function renderLayoutSelectionPanel(ctx: { navigateToComponent: (path: string) =
892
892
  style="font-size:9px;padding:2px 6px;background:var(--spectrum-purple-600);color:white;border-radius:3px;text-transform:uppercase;letter-spacing:0.5px"
893
893
  >Layout</span
894
894
  >
895
- <code style="font-size:12px;font-family:monospace">&lt;${tagName}&gt;</code>
895
+ <code
896
+ style="font-size:var(--spectrum-font-size-75, 12px);font-family:var(--font-mono)"
897
+ >&lt;${tagName}&gt;</code
898
+ >
896
899
  </div>
897
900
  ${className
898
901
  ? html`<div class="style-row">
899
902
  <div class="style-row-label">
900
903
  <sp-field-label size="s">Class</sp-field-label>
901
904
  </div>
902
- <span style="font-size:11px;color:var(--fg-dim);word-break:break-all"
905
+ <span
906
+ style="font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim);word-break:break-all"
903
907
  >${className}</span
904
908
  >
905
909
  </div>`
@@ -1234,9 +1238,9 @@ export function renderPropertiesPanelTemplate(ctx: {
1234
1238
  const def = d as Record<string, unknown>;
1235
1239
  return html`
1236
1240
  <div
1237
- style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
1241
+ style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
1238
1242
  >
1239
- <code style="font-family:monospace;color:var(--accent)"
1243
+ <code style="font-family:var(--font-mono);color:var(--accent)"
1240
1244
  >${def.attribute}</code
1241
1245
  >
1242
1246
  <span style="color:var(--fg-dim)"> → </span>
@@ -1248,7 +1252,7 @@ export function renderPropertiesPanelTemplate(ctx: {
1248
1252
  : nothing}
1249
1253
  ${def.reflects
1250
1254
  ? html`<span
1251
- style="font-size:9px;background:var(--bg-hover);padding:1px 4px;border-radius:3px"
1255
+ style="font-size:9px;background:var(--hover-bg);padding:1px 4px;border-radius:var(--radius)"
1252
1256
  >reflects</span
1253
1257
  >`
1254
1258
  : nothing}
@@ -1344,9 +1348,9 @@ export function renderPropertiesPanelTemplate(ctx: {
1344
1348
  ${cssProps.map(
1345
1349
  ([prop, val]) => html`
1346
1350
  <div
1347
- style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
1351
+ style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
1348
1352
  >
1349
- <code style="font-family:monospace;color:var(--accent)">${prop}</code>
1353
+ <code style="font-family:var(--font-mono);color:var(--accent)">${prop}</code>
1350
1354
  <span style="margin-left:auto;color:var(--fg-dim)">${String(val)}</span>
1351
1355
  </div>
1352
1356
  `,
@@ -1374,9 +1378,11 @@ export function renderPropertiesPanelTemplate(ctx: {
1374
1378
  ${parts.map(
1375
1379
  (p) => html`
1376
1380
  <div
1377
- style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
1381
+ style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:var(--spectrum-font-size-50, 11px)"
1378
1382
  >
1379
- <code style="font-family:monospace;color:var(--accent)">${p.name}</code>
1383
+ <code style="font-family:var(--font-mono);color:var(--accent)"
1384
+ >${p.name}</code
1385
+ >
1380
1386
  <span style="color:var(--fg-dim)">&lt;${p.tag}&gt;</span>
1381
1387
  </div>
1382
1388
  `,
@@ -706,9 +706,9 @@ function renderSignalEditorTemplate(
706
706
  : signalFieldRow("Default", defaultVal, (v: string) => {
707
707
  let parsed: unknown = v;
708
708
  if (def.type === "integer") {
709
- parsed = Number.parseInt(v, 10) || 0;
709
+ parsed = Math.trunc(Number(v)) || 0;
710
710
  } else if (def.type === "number") {
711
- parsed = Number.parseFloat(v) || 0;
711
+ parsed = Number(v) || 0;
712
712
  } else if (def.type === "boolean") {
713
713
  parsed = v === "true";
714
714
  } else if (def.type === "array" || def.type === "object") {
@@ -860,7 +860,7 @@ function renderDataSourceFields(
860
860
  )}
861
861
  ${signalFieldRow("Version", String(def.version || 1), (v: string) =>
862
862
  transactDoc(activeTab.value, (t) =>
863
- mutateUpdateDef(t, name, { version: Number.parseInt(v, 10) || 1 }),
863
+ mutateUpdateDef(t, name, { version: Math.trunc(Number(v)) || 1 }),
864
864
  ),
865
865
  )}
866
866
  `;
@@ -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>
@@ -1279,6 +1279,14 @@ function resolveSchemaEnum(
1279
1279
  * @param {(val: unknown) => void} onChange
1280
1280
  * @param {Record<string, unknown>} [parentDef] - Parent def for resolving dependent enum refs
1281
1281
  */
1282
+ /** Parse a numeric field value, returning NaN for blank input (so callers can treat it as unset). */
1283
+ function parseNumericField(raw: string, integer: boolean): number {
1284
+ if (raw.trim() === "") {
1285
+ return Number.NaN;
1286
+ }
1287
+ return integer ? Math.trunc(Number(raw)) : Number(raw);
1288
+ }
1289
+
1282
1290
  function renderInlineField(
1283
1291
  key: string,
1284
1292
  schema: Record<string, unknown>,
@@ -1319,10 +1327,10 @@ function renderInlineField(
1319
1327
  .value=${value !== undefined ? value : nothing}
1320
1328
  step=${schema.type === "integer" ? "1" : nothing}
1321
1329
  @change=${(e: Event) => {
1322
- const parsed =
1323
- schema.type === "integer"
1324
- ? Number.parseInt((e.target as HTMLInputElement).value, 10)
1325
- : Number.parseFloat((e.target as HTMLInputElement).value);
1330
+ const parsed = parseNumericField(
1331
+ (e.target as HTMLInputElement).value,
1332
+ schema.type === "integer",
1333
+ );
1326
1334
  onChange(Number.isNaN(parsed) ? undefined : parsed);
1327
1335
  }}
1328
1336
  ></sp-number-field>`;
@@ -1439,10 +1447,10 @@ export function renderSchemaFieldsTemplate(
1439
1447
  @change=${(e: Event) => {
1440
1448
  clearTimeout(debounce);
1441
1449
  debounce = setTimeout(() => {
1442
- const parsed =
1443
- ps.type === "integer"
1444
- ? Number.parseInt((e.target as HTMLInputElement).value, 10)
1445
- : Number.parseFloat((e.target as HTMLInputElement).value);
1450
+ const parsed = parseNumericField(
1451
+ (e.target as HTMLInputElement).value,
1452
+ ps.type === "integer",
1453
+ );
1446
1454
  transactDoc(activeTab.value, (t) =>
1447
1455
  mutateUpdateDef(t, name, {
1448
1456
  [prop]: Number.isNaN(parsed) ? undefined : parsed,
@@ -1466,7 +1474,7 @@ export function renderSchemaFieldsTemplate(
1466
1474
  ${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
1467
1475
  ([k, v]) => html`
1468
1476
  <span
1469
- 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)"
1470
1478
  >${k}: ${v.type ?? "any"}</span
1471
1479
  >
1472
1480
  `,
@@ -1634,7 +1642,7 @@ export function renderExternalPrototypeEditorTemplate(
1634
1642
  } else {
1635
1643
  // Trigger async load — will re-render when cached
1636
1644
  schemaContent = html`<div
1637
- 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"
1638
1646
  >
1639
1647
  Loading schema…
1640
1648
  </div>`;
@@ -1650,7 +1658,10 @@ export function renderExternalPrototypeEditorTemplate(
1650
1658
 
1651
1659
  return html`
1652
1660
  ${importedPath
1653
- ? 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
+ >
1654
1665
  ${def.$prototype}
1655
1666
  </div>`
1656
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
  }