@jxsuite/studio 0.28.5 → 0.30.1
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.css +98 -98
- package/dist/studio.js +9368 -6586
- package/dist/studio.js.map +109 -83
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +11 -10
- 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 +96 -76
- 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 +68 -24
- package/src/files/fs-events.ts +162 -0
- 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 +14 -17
- 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 +43 -33
- 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 +113 -35
- 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 +88 -35
- 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 +71 -1
- 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 +14 -8
- package/src/ui/unit-selector.ts +5 -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 && {
|
|
@@ -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 =
|
|
709
|
+
parsed = Math.trunc(Number(v)) || 0;
|
|
710
710
|
} else if (def.type === "number") {
|
|
711
|
-
parsed = Number
|
|
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:
|
|
863
|
+
mutateUpdateDef(t, name, { version: Math.trunc(Number(v)) || 1 }),
|
|
864
864
|
),
|
|
865
865
|
)}
|
|
866
866
|
`;
|
|
@@ -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>
|
|
@@ -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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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>`;
|
|
@@ -1357,7 +1365,7 @@ function renderJsonTextField(
|
|
|
1357
1365
|
try {
|
|
1358
1366
|
transactDoc(activeTab.value, (t) =>
|
|
1359
1367
|
mutateUpdateDef(t, name, {
|
|
1360
|
-
[prop]: JSON.parse((e.target as HTMLInputElement).value),
|
|
1368
|
+
[prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
|
|
1361
1369
|
}),
|
|
1362
1370
|
);
|
|
1363
1371
|
} catch {}
|
|
@@ -1439,10 +1447,10 @@ export function renderSchemaFieldsTemplate(
|
|
|
1439
1447
|
@change=${(e: Event) => {
|
|
1440
1448
|
clearTimeout(debounce);
|
|
1441
1449
|
debounce = setTimeout(() => {
|
|
1442
|
-
const parsed =
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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,
|
|
@@ -1490,7 +1498,7 @@ export function renderSchemaFieldsTemplate(
|
|
|
1490
1498
|
try {
|
|
1491
1499
|
transactDoc(activeTab.value, (t) =>
|
|
1492
1500
|
mutateUpdateDef(t, name, {
|
|
1493
|
-
[prop]: JSON.parse((e.target as HTMLInputElement).value),
|
|
1501
|
+
[prop]: JSON.parse((e.target as HTMLInputElement).value) as unknown,
|
|
1494
1502
|
}),
|
|
1495
1503
|
);
|
|
1496
1504
|
} catch {}
|
|
@@ -1501,7 +1509,9 @@ export function renderSchemaFieldsTemplate(
|
|
|
1501
1509
|
`;
|
|
1502
1510
|
} else if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
|
|
1503
1511
|
// Array of objects with defined schema → multi-row inline form
|
|
1504
|
-
const rows = Array.isArray(currentValue)
|
|
1512
|
+
const rows: Record<string, unknown>[] = Array.isArray(currentValue)
|
|
1513
|
+
? (currentValue as Record<string, unknown>[])
|
|
1514
|
+
: [];
|
|
1505
1515
|
const itemProps = ps.items.properties as Record<string, Record<string, unknown>>;
|
|
1506
1516
|
control = html`
|
|
1507
1517
|
<div class="array-object-field">
|
|
@@ -1626,7 +1636,7 @@ export function renderExternalPrototypeEditorTemplate(
|
|
|
1626
1636
|
${schema.description
|
|
1627
1637
|
? html`<div class="signal-hint" style="padding:4px 0 8px">${schema.description}</div>`
|
|
1628
1638
|
: nothing}
|
|
1629
|
-
${renderSchemaFieldsTemplate(schema, def, name, S, ctx)}
|
|
1639
|
+
${renderSchemaFieldsTemplate(schema as JsonSchema, def, name, S, ctx)}
|
|
1630
1640
|
`;
|
|
1631
1641
|
}
|
|
1632
1642
|
} else {
|
|
@@ -1636,7 +1646,7 @@ export function renderExternalPrototypeEditorTemplate(
|
|
|
1636
1646
|
>
|
|
1637
1647
|
Loading schema…
|
|
1638
1648
|
</div>`;
|
|
1639
|
-
fetchPluginSchema(def, {
|
|
1649
|
+
void fetchPluginSchema(def, {
|
|
1640
1650
|
...(S.documentPath != null && { documentPath: S.documentPath }),
|
|
1641
1651
|
}).then((schema) => {
|
|
1642
1652
|
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
|
×
|