@jxsuite/studio 0.28.4 → 0.29.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 (81) hide show
  1. package/dist/studio.js +6769 -5399
  2. package/dist/studio.js.map +86 -79
  3. package/package.json +5 -4
  4. package/src/browse/browse-modal.ts +2 -2
  5. package/src/browse/browse.ts +20 -19
  6. package/src/canvas/canvas-diff.ts +3 -3
  7. package/src/canvas/canvas-helpers.ts +15 -2
  8. package/src/canvas/canvas-live-render.ts +168 -86
  9. package/src/canvas/canvas-patcher.ts +656 -0
  10. package/src/canvas/canvas-perf.ts +68 -0
  11. package/src/canvas/canvas-render.ts +135 -23
  12. package/src/canvas/canvas-subtree-render.ts +113 -0
  13. package/src/canvas/canvas-utils.ts +4 -0
  14. package/src/editor/component-inline-edit.ts +4 -35
  15. package/src/editor/context-menu.ts +92 -74
  16. package/src/editor/convert-to-component.ts +11 -2
  17. package/src/editor/convert-to-repeater.ts +4 -5
  18. package/src/editor/inline-edit.ts +9 -9
  19. package/src/editor/inline-format.ts +11 -11
  20. package/src/editor/merge-tags.ts +120 -0
  21. package/src/editor/shortcuts.ts +4 -4
  22. package/src/files/components.ts +37 -0
  23. package/src/files/file-ops.ts +28 -7
  24. package/src/files/files.ts +25 -21
  25. package/src/github/github-auth.ts +14 -2
  26. package/src/github/github-publish.ts +12 -2
  27. package/src/panels/activity-bar.ts +1 -1
  28. package/src/panels/ai-panel.ts +67 -39
  29. package/src/panels/block-action-bar.ts +72 -1
  30. package/src/panels/canvas-dnd.ts +55 -26
  31. package/src/panels/data-explorer.ts +5 -3
  32. package/src/panels/dnd.ts +12 -15
  33. package/src/panels/editors.ts +5 -23
  34. package/src/panels/elements-panel.ts +2 -12
  35. package/src/panels/events-panel.ts +1 -1
  36. package/src/panels/git-panel.ts +6 -6
  37. package/src/panels/head-panel.ts +1 -1
  38. package/src/panels/layers-panel.ts +177 -147
  39. package/src/panels/preview-render.ts +15 -0
  40. package/src/panels/properties-panel.ts +16 -27
  41. package/src/panels/quick-search.ts +2 -2
  42. package/src/panels/right-panel.ts +2 -6
  43. package/src/panels/shared.ts +3 -0
  44. package/src/panels/signals-panel.ts +24 -22
  45. package/src/panels/statusbar.ts +15 -6
  46. package/src/panels/style-panel.ts +3 -3
  47. package/src/panels/style-utils.ts +3 -3
  48. package/src/panels/stylebook-panel.ts +4 -4
  49. package/src/panels/tab-bar.ts +198 -0
  50. package/src/panels/tab-strip.ts +2 -2
  51. package/src/panels/toolbar.ts +6 -75
  52. package/src/platforms/devserver.ts +60 -34
  53. package/src/reactivity.ts +2 -0
  54. package/src/services/cem-export.ts +23 -2
  55. package/src/services/code-services.ts +16 -2
  56. package/src/services/monaco-setup.ts +2 -1
  57. package/src/settings/content-types-editor.ts +16 -16
  58. package/src/settings/css-vars-editor.ts +2 -2
  59. package/src/settings/defs-editor.ts +14 -14
  60. package/src/settings/general-settings.ts +6 -6
  61. package/src/settings/head-editor.ts +2 -2
  62. package/src/site-context.ts +1 -1
  63. package/src/state.ts +66 -12
  64. package/src/store.ts +15 -3
  65. package/src/studio.ts +73 -33
  66. package/src/tabs/patch-ops.ts +143 -0
  67. package/src/tabs/tab.ts +15 -1
  68. package/src/tabs/transact.ts +454 -27
  69. package/src/types.ts +41 -0
  70. package/src/ui/color-selector.ts +7 -7
  71. package/src/ui/icons.ts +0 -30
  72. package/src/ui/media-picker.ts +2 -2
  73. package/src/ui/panel-resize.ts +6 -1
  74. package/src/ui/unit-selector.ts +2 -2
  75. package/src/ui/value-selector.ts +3 -3
  76. package/src/utils/canvas-media.ts +6 -6
  77. package/src/utils/edit-display.ts +125 -83
  78. package/src/utils/google-fonts.ts +1 -1
  79. package/src/utils/inherited-style.ts +3 -2
  80. package/src/utils/studio-utils.ts +1 -1
  81. package/src/view.ts +4 -1
@@ -214,7 +214,7 @@ export function defBadgeLabel(def: SignalDef | unknown) {
214
214
  * @param {string} name
215
215
  * @param {SignalDef | null | undefined} def
216
216
  */
217
- export function defHint(name: string, def: SignalDef | null | undefined) {
217
+ export function defHint(_name: string, def: SignalDef | null | undefined) {
218
218
  if (!def) {
219
219
  return "";
220
220
  }
@@ -413,15 +413,15 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
413
413
  state: [],
414
414
  } as Record<string, [string, SignalDef][]>;
415
415
  for (const [name, def] of entries) {
416
- groups[defCategory(def)].push([name, asSignalDef(def)]);
416
+ groups[defCategory(def)]!.push([name, asSignalDef(def)]);
417
417
  }
418
418
 
419
419
  const categories = [
420
- { items: groups.state, key: "state", label: "State" },
421
- { items: groups.computed, key: "computed", label: "Computed" },
422
- { items: groups.data, key: "data", label: "Data" },
423
- { items: groups.expression, key: "expression", label: "Expressions" },
424
- { items: groups.function, key: "function", label: "Functions" },
420
+ { items: groups.state!, key: "state", label: "State" },
421
+ { items: groups.computed!, key: "computed", label: "Computed" },
422
+ { items: groups.data!, key: "data", label: "Data" },
423
+ { items: groups.expression!, key: "expression", label: "Expressions" },
424
+ { items: groups.function!, key: "function", label: "Functions" },
425
425
  ];
426
426
 
427
427
  const collapsedCats = (S._collapsedSignalCats ||= new Set());
@@ -515,7 +515,7 @@ export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx
515
515
  );
516
516
  expandedSignal = n;
517
517
  if (src) {
518
- fetchPluginSchema(
518
+ void fetchPluginSchema(
519
519
  { $prototype: protoName, $src: src },
520
520
  {
521
521
  ...(S.documentPath != null && {
@@ -887,7 +887,7 @@ function renderDataSourceFields(
887
887
  return textareaRow(fieldLabel, defaultStr, (v: string) => {
888
888
  try {
889
889
  transactDoc(activeTab.value, (t) =>
890
- mutateUpdateDef(t, name, { [fieldName]: JSON.parse(v) }),
890
+ mutateUpdateDef(t, name, { [fieldName]: JSON.parse(v) as unknown }),
891
891
  );
892
892
  } catch {}
893
893
  });
@@ -901,7 +901,7 @@ function renderFunctionFields(
901
901
  S: SignalsPanelState,
902
902
  name: string,
903
903
  def: SignalDef,
904
- textareaRow: (
904
+ _textareaRow: (
905
905
  label: string,
906
906
  value: string,
907
907
  onChange: (value: string) => void,
@@ -962,7 +962,7 @@ function renderFunctionFields(
962
962
 
963
963
  /** Render CEM parameter editor with basic/advanced toggle. */
964
964
  function renderParameterEditorTemplate(
965
- S: SignalsPanelState,
965
+ _S: SignalsPanelState,
966
966
  name: string,
967
967
  def: SignalDef,
968
968
  ctx: SignalsPanelCtx,
@@ -1061,7 +1061,7 @@ function renderParameterEditorTemplate(
1061
1061
  @change=${(e: Event) => {
1062
1062
  const next = [...params];
1063
1063
  const val = (e.target as HTMLInputElement).value;
1064
- const { type: _t, ...rest } = next[i];
1064
+ const { type: _t, ...rest } = next[i]!;
1065
1065
  next[i] = val ? { ...rest, type: { text: val } } : rest;
1066
1066
  transactDoc(activeTab.value, (t) =>
1067
1067
  mutateUpdateDef(t, name, { parameters: next }),
@@ -1076,7 +1076,7 @@ function renderParameterEditorTemplate(
1076
1076
  @change=${(e: Event) => {
1077
1077
  const next = [...params];
1078
1078
  const val = (e.target as HTMLInputElement).value;
1079
- const { description: _d, ...rest } = next[i];
1079
+ const { description: _d, ...rest } = next[i]!;
1080
1080
  next[i] = val ? { ...rest, description: val } : rest;
1081
1081
  transactDoc(activeTab.value, (t) =>
1082
1082
  mutateUpdateDef(t, name, { parameters: next }),
@@ -1090,7 +1090,7 @@ function renderParameterEditorTemplate(
1090
1090
  @change=${(e: Event) => {
1091
1091
  const next = [...params];
1092
1092
  const { checked } = e.target as HTMLInputElement;
1093
- const { optional: _o, ...rest } = next[i];
1093
+ const { optional: _o, ...rest } = next[i]!;
1094
1094
  next[i] = checked ? { ...rest, optional: true } : rest;
1095
1095
  transactDoc(activeTab.value, (t) =>
1096
1096
  mutateUpdateDef(t, name, { parameters: next }),
@@ -1174,7 +1174,7 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
1174
1174
  @change=${(e: Event) => {
1175
1175
  const next = [...emits];
1176
1176
  const val = (e.target as HTMLInputElement).value;
1177
- const { type: _t, ...rest } = next[i];
1177
+ const { type: _t, ...rest } = next[i]!;
1178
1178
  next[i] = val ? { ...rest, type: { text: val } } : rest;
1179
1179
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1180
1180
  }}
@@ -1187,7 +1187,7 @@ function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: Sign
1187
1187
  @change=${(e: Event) => {
1188
1188
  const next = [...emits];
1189
1189
  const val = (e.target as HTMLInputElement).value;
1190
- const { description: _d, ...rest } = next[i];
1190
+ const { description: _d, ...rest } = next[i]!;
1191
1191
  next[i] = val ? { ...rest, description: val } : rest;
1192
1192
  transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1193
1193
  }}
@@ -1248,7 +1248,7 @@ function resolveSchemaEnum(
1248
1248
  const match = ref.match(/#\/\$context\/contentTypes\/\{@(\w+)\}\/schema\/properties/);
1249
1249
  if (match && parentDef) {
1250
1250
  const [, paramName] = match;
1251
- const typeName = parentDef[paramName] as string | undefined;
1251
+ const typeName = parentDef[paramName!] as string | undefined;
1252
1252
  if (typeName) {
1253
1253
  const ct = projectState?.projectConfig?.contentTypes?.[typeName] as
1254
1254
  | Record<string, unknown>
@@ -1357,7 +1357,7 @@ function renderJsonTextField(
1357
1357
  try {
1358
1358
  transactDoc(activeTab.value, (t) =>
1359
1359
  mutateUpdateDef(t, name, {
1360
- [prop]: JSON.parse((e.target as HTMLInputElement).value),
1360
+ [prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
1361
1361
  }),
1362
1362
  );
1363
1363
  } catch {}
@@ -1490,7 +1490,7 @@ export function renderSchemaFieldsTemplate(
1490
1490
  try {
1491
1491
  transactDoc(activeTab.value, (t) =>
1492
1492
  mutateUpdateDef(t, name, {
1493
- [prop]: JSON.parse((e.target as HTMLInputElement).value),
1493
+ [prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
1494
1494
  }),
1495
1495
  );
1496
1496
  } catch {}
@@ -1501,7 +1501,9 @@ export function renderSchemaFieldsTemplate(
1501
1501
  `;
1502
1502
  } else if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
1503
1503
  // Array of objects with defined schema → multi-row inline form
1504
- const rows = Array.isArray(currentValue) ? currentValue : [];
1504
+ const rows: Record<string, unknown>[] = Array.isArray(currentValue)
1505
+ ? (currentValue as Record<string, unknown>[])
1506
+ : [];
1505
1507
  const itemProps = ps.items.properties as Record<string, Record<string, unknown>>;
1506
1508
  control = html`
1507
1509
  <div class="array-object-field">
@@ -1626,7 +1628,7 @@ export function renderExternalPrototypeEditorTemplate(
1626
1628
  ${schema.description
1627
1629
  ? html`<div class="signal-hint" style="padding:4px 0 8px">${schema.description}</div>`
1628
1630
  : nothing}
1629
- ${renderSchemaFieldsTemplate(schema, def, name, S, ctx)}
1631
+ ${renderSchemaFieldsTemplate(schema as JsonSchema, def, name, S, ctx)}
1630
1632
  `;
1631
1633
  }
1632
1634
  } else {
@@ -1636,7 +1638,7 @@ export function renderExternalPrototypeEditorTemplate(
1636
1638
  >
1637
1639
  Loading schema…
1638
1640
  </div>`;
1639
- fetchPluginSchema(def, {
1641
+ void fetchPluginSchema(def, {
1640
1642
  ...(S.documentPath != null && { documentPath: S.documentPath }),
1641
1643
  }).then((schema) => {
1642
1644
  if (schema) {
@@ -70,19 +70,28 @@ export function renderStatusbar() {
70
70
  const node = getNodeAtPath(tab.doc.document, sel);
71
71
  parts.push(`Selected: ${esc(nodeLabel(node))}`);
72
72
 
73
+ // Walk the path one structural step at a time. Most steps are `["children", index]` or
74
+ // `["cases", name]` pairs, but a repeater template is reached by a lone `"map"` segment — so the
75
+ // Step width varies. Emitting a crumb per node keeps the array pseudo-element ("Repeater") and
76
+ // Its template both visible instead of collapsing the array into a bare `[index]`.
73
77
  const pathSegments = [];
74
- for (let i = 0; i < sel.length; i += 2) {
75
- const subPath = sel.slice(0, i + 2);
78
+ for (let i = 0; i < sel.length; ) {
79
+ const seg = sel[i];
80
+ const step = seg === "map" ? 1 : 2;
81
+ const subPath = sel.slice(0, i + step);
76
82
  const childNode = getNodeAtPath(tab.doc.document, subPath);
77
83
  const fallbackTag = childNode?.tag;
78
84
  const label =
79
- childNode?.tagName ||
80
- (typeof fallbackTag === "string" ? fallbackTag : "") ||
81
- `[${sel[i + 1]}]`;
85
+ childNode?.$prototype === "Array"
86
+ ? "Repeater"
87
+ : childNode?.tagName ||
88
+ (typeof fallbackTag === "string" ? fallbackTag : "") ||
89
+ (seg === "cases" ? String(sel[i + 1]) : `[${sel[i + 1]}]`);
82
90
  const dataPath = JSON.stringify(subPath);
83
91
  pathSegments.push(
84
92
  `<span class="sb-path-seg" data-path='${esc(dataPath)}'>${esc(label)}</span>`,
85
93
  );
94
+ i += step;
86
95
  }
87
96
  parts.push(`Path: ${pathSegments.join(' <span class="sb-path-sep">&gt;</span> ')}`);
88
97
  } else if (tab?.session.ui.stylebookSelection) {
@@ -106,7 +115,7 @@ function _onStatusbarClick(e: Event) {
106
115
  return;
107
116
  }
108
117
  try {
109
- const path = JSON.parse(pathStr);
118
+ const path = JSON.parse(pathStr) as JxPath;
110
119
  updateSession({ selection: path });
111
120
  renderOnly("leftPanel", "rightPanel", "canvas");
112
121
  } catch {
@@ -505,10 +505,10 @@ function styleSidebarTemplate(
505
505
  continue;
506
506
  }
507
507
  const sec = ((entry as Record<string, unknown>).$section as string) || "other";
508
- sectionProps[sec].push({ entry, prop });
508
+ sectionProps[sec]!.push({ entry, prop });
509
509
  }
510
510
  for (const sec of cssMeta.$sections) {
511
- sectionProps[sec.key].sort(
511
+ sectionProps[sec.key]!.sort(
512
512
  (
513
513
  a: { prop: string; entry: CssPropertyEntry },
514
514
  b: { prop: string; entry: CssPropertyEntry },
@@ -545,7 +545,7 @@ function styleSidebarTemplate(
545
545
  const sectionTemplates = cssMeta.$sections
546
546
  .filter((sec) => sec.key !== "other")
547
547
  .map((sec) => {
548
- const entries = sectionProps[sec.key];
548
+ const entries = sectionProps[sec.key]!;
549
549
 
550
550
  const sectionActiveProps = entries.filter(
551
551
  ({ prop, entry }: { prop: string; entry: CssPropertyEntry }) => {
@@ -4,7 +4,6 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
4
4
 
5
5
  import { getNodeAtPath } from "../store";
6
6
  import { activeTab } from "../workspace/workspace";
7
- import { camelToKebab } from "../utils/studio-utils";
8
7
  import cssMeta from "../../data/css-meta.json";
9
8
 
10
9
  let cssInitialMap = new Map<string, string>();
@@ -246,9 +245,10 @@ export function currentFontFamily() {
246
245
  }
247
246
  const m = typeof raw === "string" && raw.match(/^var\((--[^)]+)\)$/);
248
247
  if (m) {
249
- return tab?.doc.document?.style?.[m[1]] || "";
248
+ return tab?.doc.document?.style?.[m[1]!] || "";
250
249
  }
251
250
  return raw;
252
251
  }
253
252
 
254
- export { cssMeta, camelToKebab };
253
+ export { default as cssMeta } from "../../data/css-meta.json";
254
+ export { camelToKebab } from "../utils/studio-utils";
@@ -78,7 +78,7 @@ interface StylebookCtx {
78
78
  effectiveZoom: () => number;
79
79
  }
80
80
 
81
- export { stylebookMeta };
81
+ export { default as stylebookMeta } from "../../data/stylebook-meta.json";
82
82
 
83
83
  /**
84
84
  * Resolve a nested tag path in a style object. e.g., "table th" → style["table"]["th"]
@@ -102,7 +102,7 @@ function _resolveNestedStyle(style: Record<string, unknown>, tagPath: string) {
102
102
  let _ctx: StylebookCtx | null = null;
103
103
 
104
104
  /** Lookup: tag → entry from stylebookMeta (built once) */
105
- const _entryByTag = new Map();
105
+ const _entryByTag = new Map<string, StylebookEntry>();
106
106
  for (const section of stylebookMeta.$sections) {
107
107
  for (const entry of section.elements as StylebookEntry[]) {
108
108
  _entryByTag.set(entry.tag, entry);
@@ -289,7 +289,7 @@ export function refreshStylebookStyles() {
289
289
  const htmlEl = el as HTMLElement;
290
290
  // Determine if it's a compound selector (e.g. "ul li") or simple tag
291
291
  const parts = tag.split(" ");
292
- const leafTag = parts.at(-1);
292
+ const leafTag = parts.at(-1)!;
293
293
  const entry = _entryByTag.get(leafTag);
294
294
 
295
295
  // Reset to base style
@@ -379,7 +379,7 @@ export function selectStylebookTag(tag: string, media?: string | null, { panCanv
379
379
  renderStylebookOverlays();
380
380
 
381
381
  if (tag && panCanvas && canvasPanels.length > 0) {
382
- const el = findStylebookEl(canvasPanels[0].canvas, tag);
382
+ const el = findStylebookEl(canvasPanels[0]!.canvas, tag);
383
383
  if (el) {
384
384
  panToCanvasEl(el);
385
385
  }
@@ -0,0 +1,198 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Tab bar — a per-tab contextual action bar rendered between the tab strip and the edit content.
4
+ * Standardizes Back/breadcrumb navigation, media feature toggles, and mode actions (Code-mode
5
+ * Export) into a single bar shared identically by every edit mode.
6
+ *
7
+ * Follows the same module shape as tab-strip.ts: mount(host, ctx) → effectScope/effect → render().
8
+ * The bar collapses (renders `nothing`, so `#tab-bar:empty` hides the row) when there is nothing
9
+ * contextual to show.
10
+ */
11
+
12
+ import { html, render as litRender, nothing } from "lit-html";
13
+ import { updateUi } from "../store";
14
+ import { effect, effectScope } from "../reactivity";
15
+ import { activeTab } from "../workspace/workspace";
16
+ import { getEffectiveMedia } from "../site-context";
17
+ import { mediaDisplayName } from "./shared";
18
+ import type { DocumentStackEntry, FunctionEditDef } from "../types";
19
+ import type { EffectScope } from "@vue/reactivity";
20
+ import type { TemplateResult } from "lit-html";
21
+
22
+ interface TabBarCtx {
23
+ navigateBack: () => void;
24
+ navigateToLevel: (level: number) => void;
25
+ closeFunctionEditor: () => void;
26
+ exportFile: () => void;
27
+ getCanvasMode: () => string;
28
+ parseMediaEntries: (media: Record<string, string> | null | undefined) => {
29
+ sizeBreakpoints: {
30
+ name: string;
31
+ query: string;
32
+ width: number;
33
+ type: string;
34
+ }[];
35
+ featureQueries: { name: string; query: string }[];
36
+ baseWidth: number;
37
+ };
38
+ }
39
+
40
+ let _host: HTMLElement | null = null;
41
+
42
+ let _ctx: TabBarCtx | null = null;
43
+
44
+ let _scope: EffectScope | null = null;
45
+
46
+ /**
47
+ * Mount the tab bar into the given host element.
48
+ *
49
+ * @param {HTMLElement} host
50
+ * @param {TabBarCtx} ctx
51
+ */
52
+ export function mount(host: HTMLElement, ctx: TabBarCtx) {
53
+ _host = host;
54
+ _ctx = ctx;
55
+ _scope = effectScope();
56
+ _scope.run(() => {
57
+ effect(() => {
58
+ const tab = activeTab.value;
59
+ if (tab) {
60
+ // Read reactive properties to establish tracking — mirrors the toolbar's subset
61
+ void tab.doc.document;
62
+ void tab.doc.mode;
63
+ void tab.documentPath;
64
+ void tab.session.documentStack.length;
65
+ void tab.session.ui.canvasMode;
66
+ void tab.session.ui.editingFunction;
67
+ void tab.session.ui.featureToggles;
68
+ }
69
+ render();
70
+ });
71
+ });
72
+ }
73
+
74
+ export function unmount() {
75
+ _scope?.stop();
76
+ _scope = null;
77
+ _host = null;
78
+ _ctx = null;
79
+ }
80
+
81
+ export function render() {
82
+ if (!_host || !_ctx) {
83
+ return;
84
+ }
85
+ try {
86
+ litRender(tabBarTemplate(_ctx), _host);
87
+ } catch (error) {
88
+ console.error("tab-bar render error:", error);
89
+ }
90
+ }
91
+
92
+ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
93
+ const tab = activeTab.value;
94
+ if (!tab) {
95
+ return nothing;
96
+ }
97
+
98
+ const S = {
99
+ document: tab.doc.document,
100
+ documentPath: tab.documentPath,
101
+ documentStack: tab.session.documentStack,
102
+ ui: tab.session.ui,
103
+ };
104
+ const canvasMode = ctx.getCanvasMode();
105
+ const editing = S.ui.editingFunction as FunctionEditDef | null;
106
+ const hasStack = S.documentStack && S.documentStack.length > 0;
107
+
108
+ // ── Left region: navigation context (function editor takes precedence over the stack) ──
109
+ let navTpl: TemplateResult | typeof nothing = nothing;
110
+ if (editing) {
111
+ const docName = S.documentPath?.split("/").pop() || S.document?.tagName || "document";
112
+ const funcLabel = editing.type === "def" ? `ƒ ${editing.defName}` : `ƒ ${editing.eventKey}`;
113
+ navTpl = html`
114
+ <div class="breadcrumb">
115
+ <sp-action-button size="s" title="Close editor" @click=${ctx.closeFunctionEditor}>
116
+ <sp-icon-back slot="icon"></sp-icon-back>
117
+ Back
118
+ </sp-action-button>
119
+ <span class="breadcrumb-item">${docName}</span>
120
+ <span class="breadcrumb-sep"> › </span>
121
+ <span class="breadcrumb-item current">${funcLabel}</span>
122
+ </div>
123
+ `;
124
+ } else if (hasStack) {
125
+ navTpl = html`
126
+ <div class="breadcrumb">
127
+ <sp-action-button size="s" title="Return to parent document" @click=${ctx.navigateBack}>
128
+ <sp-icon-back slot="icon"></sp-icon-back>
129
+ Back
130
+ </sp-action-button>
131
+ ${S.documentStack.map(
132
+ (frame: DocumentStackEntry, i: number) => html`
133
+ <span class="breadcrumb-item clickable" @click=${() => ctx.navigateToLevel(i)}
134
+ >${frame.documentPath?.split("/").pop() || "untitled"}</span
135
+ >
136
+ <span class="breadcrumb-sep"> › </span>
137
+ `,
138
+ )}
139
+ <span class="breadcrumb-item current">
140
+ ${S.documentPath?.split("/").pop() || S.document?.tagName || "document"}
141
+ </span>
142
+ </div>
143
+ `;
144
+ }
145
+
146
+ // ── Right region: media feature toggles ──
147
+ const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
148
+ const togglesTpl =
149
+ featureQueries.length > 0
150
+ ? html`
151
+ <sp-action-group compact size="s">
152
+ ${featureQueries.map(
153
+ ({ name, query }: { name: string; query: string }) => html`
154
+ <sp-action-button
155
+ toggles
156
+ size="s"
157
+ title=${query}
158
+ ?selected=${Boolean(S.ui.featureToggles[name])}
159
+ @click=${() => {
160
+ const newToggles = {
161
+ ...S.ui.featureToggles,
162
+ [name]: !S.ui.featureToggles[name],
163
+ };
164
+ updateUi("featureToggles", newToggles);
165
+ }}
166
+ >
167
+ ${mediaDisplayName(name)}
168
+ </sp-action-button>
169
+ `,
170
+ )}
171
+ </sp-action-group>
172
+ `
173
+ : nothing;
174
+
175
+ // ── Right region: mode actions (Code-mode Export) ──
176
+ const exportTpl =
177
+ !editing && canvasMode === "source"
178
+ ? html`
179
+ <sp-action-button size="s" @click=${ctx.exportFile}>
180
+ <sp-icon-export slot="icon"></sp-icon-export>
181
+ Export
182
+ </sp-action-button>
183
+ `
184
+ : nothing;
185
+
186
+ // Collapse the bar entirely when there is nothing contextual to show.
187
+ if (navTpl === nothing && togglesTpl === nothing && exportTpl === nothing) {
188
+ return nothing;
189
+ }
190
+
191
+ return html`
192
+ <div class="tab-bar">
193
+ ${navTpl}
194
+ <div class="tb-spacer"></div>
195
+ ${togglesTpl} ${exportTpl}
196
+ </div>
197
+ `;
198
+ }
@@ -77,7 +77,7 @@ function render() {
77
77
  @auxclick=${(e: MouseEvent) => {
78
78
  if (e.button === 1) {
79
79
  e.preventDefault();
80
- requestClose(id);
80
+ void requestClose(id);
81
81
  }
82
82
  }}
83
83
  title=${tab.documentPath || "Untitled"}
@@ -88,7 +88,7 @@ function render() {
88
88
  class="tab-strip-close"
89
89
  @click=${(e: Event) => {
90
90
  e.stopPropagation();
91
- requestClose(id);
91
+ void requestClose(id);
92
92
  }}
93
93
  >
94
94
  ×
@@ -5,13 +5,10 @@
5
5
  */
6
6
 
7
7
  import { html, render as litRender, nothing } from "lit-html";
8
- import { updateSession, updateUi } from "../store";
8
+ import { updateSession } from "../store";
9
9
  import { redo as tabRedo, undo as tabUndo } from "../tabs/transact";
10
10
  import { effect, effectScope } from "../reactivity";
11
11
  import { activeTab } from "../workspace/workspace";
12
- import type { DocumentStackEntry } from "../types";
13
- import { getEffectiveMedia } from "../site-context";
14
- import { mediaDisplayName } from "./shared";
15
12
  import { applyPanelCollapse, view } from "../view";
16
13
  import { getRecentProjects } from "../recent-projects";
17
14
  import { openQuickSearch } from "./quick-search";
@@ -23,21 +20,9 @@ import type { EffectScope } from "@vue/reactivity";
23
20
  import type { TemplateResult } from "lit-html";
24
21
 
25
22
  interface ToolbarCtx {
26
- navigateBack: () => void;
27
- navigateToLevel: (level: number) => void;
28
23
  openProject: () => void;
29
24
  openFile?: (path: string) => void;
30
25
  saveFile: () => void;
31
- parseMediaEntries: (media: Record<string, string> | null | undefined) => {
32
- sizeBreakpoints: {
33
- name: string;
34
- query: string;
35
- width: number;
36
- type: string;
37
- }[];
38
- featureQueries: { name: string; query: string }[];
39
- baseWidth: number;
40
- };
41
26
  getCanvasMode: () => string;
42
27
  setCanvasMode: (mode: string) => void;
43
28
  renderCanvas: () => void;
@@ -54,7 +39,6 @@ let _scope: EffectScope | null = null;
54
39
 
55
40
  const toolbarIconMap = {
56
41
  "sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
57
- "sp-icon-back": html`<sp-icon-back slot="icon"></sp-icon-back>`,
58
42
  "sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
59
43
  "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
60
44
  "sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
@@ -160,9 +144,9 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
160
144
  @change=${(e: Event) => {
161
145
  const val = (e.target as unknown as HTMLInputElement).value;
162
146
  if (val === "__new__") {
163
- handleNewProject();
147
+ void handleNewProject();
164
148
  } else {
165
- ctx.openRecentProject(val);
149
+ void ctx.openRecentProject(val);
166
150
  }
167
151
  }}
168
152
  >
@@ -298,65 +282,12 @@ function toolbarTemplate() {
298
282
 
299
283
  const S = {
300
284
  dirty: tab.doc.dirty,
301
- document: tab.doc.document,
302
- documentPath: tab.documentPath,
303
- documentStack: tab.session.documentStack,
304
285
  fileHandle: tab.fileHandle,
305
286
  mode: tab.doc.mode,
306
287
  selection: tab.session.selection,
307
288
  ui: tab.session.ui,
308
289
  };
309
290
  const canvasMode = ctx.getCanvasMode();
310
- const hasStack = S.documentStack && S.documentStack.length > 0;
311
-
312
- const breadcrumbTpl = hasStack
313
- ? html`
314
- <div class="breadcrumb">
315
- <sp-action-button size="s" title="Return to parent document" @click=${ctx.navigateBack}>
316
- ${toolbarIconMap["sp-icon-back"]}Back
317
- </sp-action-button>
318
- ${S.documentStack.map(
319
- (frame: DocumentStackEntry, i: number) => html`
320
- <span class="breadcrumb-item clickable" @click=${() => ctx.navigateToLevel(i)}
321
- >${frame.documentPath?.split("/").pop() || "untitled"}</span
322
- >
323
- <span class="breadcrumb-sep"> › </span>
324
- `,
325
- )}
326
- <span class="breadcrumb-item current">
327
- ${S.documentPath?.split("/").pop() || S.document.tagName || "document"}
328
- </span>
329
- </div>
330
- `
331
- : nothing;
332
-
333
- const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document.$media));
334
- const togglesTpl =
335
- featureQueries.length > 0
336
- ? html`
337
- <sp-action-group compact size="s">
338
- ${featureQueries.map(
339
- (/** @type {{ name: string; query: string }} */ { name, query }) => html`
340
- <sp-action-button
341
- toggles
342
- size="s"
343
- title=${query}
344
- ?selected=${Boolean(S.ui.featureToggles[name])}
345
- @click=${() => {
346
- const newToggles = {
347
- ...S.ui.featureToggles,
348
- [name]: !S.ui.featureToggles[name],
349
- };
350
- updateUi("featureToggles", newToggles);
351
- }}
352
- >
353
- ${mediaDisplayName(name)}
354
- </sp-action-button>
355
- `,
356
- )}
357
- </sp-action-group>
358
- `
359
- : nothing;
360
291
 
361
292
  const modeSwitcherTpl = html`
362
293
  <sp-action-group selects="single" size="s" compact>
@@ -488,9 +419,9 @@ function toolbarTemplate() {
488
419
  @change=${(e: Event) => {
489
420
  const val = (e.target as unknown as HTMLInputElement).value;
490
421
  if (val === "__new__") {
491
- handleNewProject();
422
+ void handleNewProject();
492
423
  } else {
493
- ctx.openRecentProject(val);
424
+ void ctx.openRecentProject(val);
494
425
  }
495
426
  }}
496
427
  >
@@ -543,7 +474,7 @@ function toolbarTemplate() {
543
474
  </sp-action-button>`
544
475
  : nothing}
545
476
  <div class="tb-spacer"></div>
546
- ${breadcrumbTpl} ${togglesTpl} ${modeSwitcherTpl}
477
+ ${modeSwitcherTpl}
547
478
  <sp-action-button
548
479
  quiet
549
480
  size="s"