@jxsuite/studio 0.28.5 → 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.
- package/dist/studio.js +6769 -5399
- package/dist/studio.js.map +86 -79
- package/package.json +5 -4
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +92 -74
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +25 -21
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +12 -15
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +24 -22
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +60 -34
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +73 -33
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +41 -0
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +6 -1
- package/src/ui/unit-selector.ts +2 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- 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(
|
|
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)]
|
|
416
|
+
groups[defCategory(def)]!.push([name, asSignalDef(def)]);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
const categories = [
|
|
420
|
-
{ items: groups.state
|
|
421
|
-
{ items: groups.computed
|
|
422
|
-
{ items: groups.data
|
|
423
|
-
{ items: groups.expression
|
|
424
|
-
{ items: groups.function
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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) {
|
package/src/panels/statusbar.ts
CHANGED
|
@@ -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;
|
|
75
|
-
const
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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">></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]
|
|
508
|
+
sectionProps[sec]!.push({ entry, prop });
|
|
509
509
|
}
|
|
510
510
|
for (const sec of cssMeta.$sections) {
|
|
511
|
-
sectionProps[sec.key]
|
|
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
|
|
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]
|
|
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
|
+
}
|
package/src/panels/tab-strip.ts
CHANGED
|
@@ -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
|
×
|
package/src/panels/toolbar.ts
CHANGED
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
|
-
import { updateSession
|
|
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
|
-
${
|
|
477
|
+
${modeSwitcherTpl}
|
|
547
478
|
<sp-action-button
|
|
548
479
|
quiet
|
|
549
480
|
size="s"
|