@jxsuite/studio 0.11.0 → 0.13.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 +4248 -3412
- package/dist/studio.js.map +49 -43
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +3 -2
- package/src/canvas/canvas-render.js +152 -20
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +9 -12
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +11 -15
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +0 -456
- package/src/store.js +97 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/workspace/workspace.js +89 -0
|
@@ -7,18 +7,19 @@ import { html, nothing } from "lit-html";
|
|
|
7
7
|
import { live } from "lit-html/directives/live.js";
|
|
8
8
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
9
9
|
import {
|
|
10
|
-
getState,
|
|
11
|
-
update,
|
|
12
|
-
updateUi,
|
|
13
10
|
getNodeAtPath,
|
|
14
|
-
updateStyle,
|
|
15
|
-
updateMediaStyle,
|
|
16
|
-
updateNestedStyle,
|
|
17
|
-
updateMediaNestedStyle,
|
|
18
11
|
COMMON_SELECTORS,
|
|
19
12
|
isNestedSelector,
|
|
20
13
|
debouncedStyleCommit,
|
|
21
14
|
} from "../store.js";
|
|
15
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
16
|
+
import {
|
|
17
|
+
transactDoc,
|
|
18
|
+
mutateUpdateStyle,
|
|
19
|
+
mutateUpdateMediaStyle,
|
|
20
|
+
mutateUpdateNestedStyle,
|
|
21
|
+
mutateUpdateMediaNestedStyle,
|
|
22
|
+
} from "../tabs/transact.js";
|
|
22
23
|
import { inferInputType, propLabel } from "../utils/studio-utils.js";
|
|
23
24
|
import { renderFieldRow } from "../ui/field-row.js";
|
|
24
25
|
import { parseMediaEntries } from "../utils/canvas-media.js";
|
|
@@ -65,15 +66,15 @@ function renderStyleRow(
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
* @param {any} shortProp @param {any} entry @param {any} style @param {any}
|
|
69
|
+
* @param {any} shortProp @param {any} entry @param {any} style @param {any} mutateFn
|
|
69
70
|
* @param {any} _deleteFn @param {Record<string, any>} inherited
|
|
70
71
|
*/
|
|
71
|
-
function renderShorthandRow(shortProp, entry, style,
|
|
72
|
-
const
|
|
72
|
+
function renderShorthandRow(shortProp, entry, style, mutateFn, _deleteFn, inherited = {}) {
|
|
73
|
+
const tab = activeTab.value;
|
|
73
74
|
const longhands = getLonghands(shortProp);
|
|
74
75
|
const shortVal = style[shortProp];
|
|
75
76
|
const hasLonghands = longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
76
|
-
const isExpanded =
|
|
77
|
+
const isExpanded = tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
77
78
|
const hasAnyVal =
|
|
78
79
|
shortVal !== undefined || longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
79
80
|
|
|
@@ -86,12 +87,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
86
87
|
title="Clear ${shortProp}"
|
|
87
88
|
@click=${(/** @type {any} */ e) => {
|
|
88
89
|
e.stopPropagation();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
transactDoc(activeTab.value, (t) => {
|
|
91
|
+
if (shortVal !== undefined) mutateFn(t, shortProp, undefined);
|
|
92
|
+
for (const l of longhands) {
|
|
93
|
+
if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
95
96
|
}}
|
|
96
97
|
></span>`
|
|
97
98
|
: nothing}
|
|
@@ -109,12 +110,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
109
110
|
? longhands.map((/** @type {any} */ l) => inherited[l.name] || "0").join(" ")
|
|
110
111
|
: ""}
|
|
111
112
|
@input=${debouncedStyleCommit(`short:${shortProp}`, 400, (/** @type {any} */ e) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
transactDoc(activeTab.value, (t) => {
|
|
114
|
+
for (const l of longhands) {
|
|
115
|
+
if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
|
|
116
|
+
}
|
|
117
|
+
mutateFn(t, shortProp, e.target.value || undefined);
|
|
118
|
+
});
|
|
118
119
|
})}
|
|
119
120
|
></sp-textfield>
|
|
120
121
|
<sp-action-button
|
|
@@ -122,10 +123,10 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
122
123
|
quiet
|
|
123
124
|
@click=${(/** @type {any} */ e) => {
|
|
124
125
|
e.stopPropagation();
|
|
125
|
-
|
|
126
|
-
...
|
|
126
|
+
activeTab.value.session.ui.styleShorthands = {
|
|
127
|
+
...activeTab.value.session.ui.styleShorthands,
|
|
127
128
|
[shortProp]: !isExpanded,
|
|
128
|
-
}
|
|
129
|
+
};
|
|
129
130
|
}}
|
|
130
131
|
>
|
|
131
132
|
${isExpanded
|
|
@@ -162,12 +163,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
162
163
|
? emptyVal
|
|
163
164
|
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
164
165
|
);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
transactDoc(activeTab.value, (t) => {
|
|
167
|
+
for (const l of longhands) {
|
|
168
|
+
if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
|
|
169
|
+
}
|
|
170
|
+
mutateFn(t, shortProp, compress(vals));
|
|
171
|
+
});
|
|
171
172
|
}}
|
|
172
173
|
></span>`
|
|
173
174
|
: nothing}
|
|
@@ -186,12 +187,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
186
187
|
? newVal || emptyVal
|
|
187
188
|
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
188
189
|
);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
transactDoc(activeTab.value, (t) => {
|
|
191
|
+
for (const l of longhands) {
|
|
192
|
+
if (style[l.name] !== undefined) mutateFn(t, l.name, undefined);
|
|
193
|
+
}
|
|
194
|
+
mutateFn(t, shortProp, compress(vals));
|
|
195
|
+
});
|
|
195
196
|
},
|
|
196
197
|
{ placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
|
|
197
198
|
)}
|
|
@@ -212,11 +213,12 @@ function renderShorthandRow(shortProp, entry, style, commitFn, _deleteFn, inheri
|
|
|
212
213
|
* @param {any} activeSelector
|
|
213
214
|
*/
|
|
214
215
|
function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
215
|
-
const
|
|
216
|
+
const tab = activeTab.value;
|
|
217
|
+
const sel = /** @type {import("../state.js").JxPath} */ (tab.session.selection);
|
|
216
218
|
const style = node.style || {};
|
|
217
|
-
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(
|
|
219
|
+
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
|
|
218
220
|
const mediaNames = sizeBreakpoints.map((bp) => bp.name);
|
|
219
|
-
const
|
|
221
|
+
const mediaTab = activeMediaTab;
|
|
220
222
|
|
|
221
223
|
// ── Media tabs template ──────────────────────────────────────────────────
|
|
222
224
|
const mediaTabsT =
|
|
@@ -224,12 +226,12 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
224
226
|
? html`
|
|
225
227
|
<sp-tabs
|
|
226
228
|
size="s"
|
|
227
|
-
selected=${
|
|
229
|
+
selected=${mediaTab || "base"}
|
|
228
230
|
@change=${(/** @type {any} */ e) => {
|
|
229
231
|
const val = e.target.selected;
|
|
230
232
|
const newMedia = val === "base" ? null : val;
|
|
231
|
-
if (newMedia !==
|
|
232
|
-
|
|
233
|
+
if (newMedia !== tab.session.ui.activeMedia) {
|
|
234
|
+
tab.session.ui.activeMedia = newMedia;
|
|
233
235
|
}
|
|
234
236
|
}}
|
|
235
237
|
>
|
|
@@ -242,7 +244,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
242
244
|
: nothing;
|
|
243
245
|
|
|
244
246
|
// ── Selector dropdown ──────────────────────────────────────────────────────
|
|
245
|
-
const contextStyle =
|
|
247
|
+
const contextStyle = mediaTab ? style[`@${mediaTab}`] || {} : style;
|
|
246
248
|
const existingSelectors = Object.keys(contextStyle).filter(isNestedSelector);
|
|
247
249
|
const existingSet = new Set(existingSelectors);
|
|
248
250
|
const commonSet = new Set(COMMON_SELECTORS);
|
|
@@ -281,7 +283,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
281
283
|
inp.remove();
|
|
282
284
|
picker.style.display = "";
|
|
283
285
|
if (accept && v && isNestedSelector(v)) {
|
|
284
|
-
|
|
286
|
+
activeTab.value.session.ui.activeSelector = v;
|
|
285
287
|
}
|
|
286
288
|
};
|
|
287
289
|
inp.addEventListener("keydown", (ev) => {
|
|
@@ -292,7 +294,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
292
294
|
return;
|
|
293
295
|
}
|
|
294
296
|
const newSelector = val === "__base__" ? null : val;
|
|
295
|
-
|
|
297
|
+
activeTab.value.session.ui.activeSelector = newSelector;
|
|
296
298
|
}}
|
|
297
299
|
>
|
|
298
300
|
<sp-menu-item value="__base__">(base)</sp-menu-item>
|
|
@@ -328,14 +330,19 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
328
330
|
size="s"
|
|
329
331
|
class="style-filter-input"
|
|
330
332
|
placeholder="Filter properties…"
|
|
331
|
-
.value=${live(
|
|
332
|
-
@input=${(/** @type {any} */ e) =>
|
|
333
|
+
.value=${live(tab.session.ui.styleFilter || "")}
|
|
334
|
+
@input=${(/** @type {any} */ e) => {
|
|
335
|
+
activeTab.value.session.ui.styleFilter = e.target.value;
|
|
336
|
+
}}
|
|
333
337
|
></sp-textfield>
|
|
334
338
|
<sp-action-button
|
|
335
339
|
size="xs"
|
|
336
340
|
class="style-filter-toggle"
|
|
337
|
-
?selected=${
|
|
338
|
-
@click=${() =>
|
|
341
|
+
?selected=${tab.session.ui.styleFilterActive}
|
|
342
|
+
@click=${() => {
|
|
343
|
+
activeTab.value.session.ui.styleFilterActive =
|
|
344
|
+
!activeTab.value.session.ui.styleFilterActive;
|
|
345
|
+
}}
|
|
339
346
|
>
|
|
340
347
|
Active
|
|
341
348
|
</sp-action-button>
|
|
@@ -345,40 +352,50 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
345
352
|
// ── Determine the active style object ──────────────────────────────────────
|
|
346
353
|
/** @type {Record<string, any>} */
|
|
347
354
|
let activeStyle;
|
|
348
|
-
/** @type {any} */
|
|
355
|
+
/** @type {(prop: any, val: any) => void} */
|
|
349
356
|
let commitStyle;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
/** @type {(t: any, prop: any, val: any) => void} */
|
|
358
|
+
let commitMutate;
|
|
359
|
+
if (activeSelector && mediaTab && mediaNames.length > 0) {
|
|
360
|
+
activeStyle = (style[`@${mediaTab}`] || {})[activeSelector] || {};
|
|
361
|
+
commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
362
|
+
mutateUpdateMediaNestedStyle(t, sel, mediaTab, activeSelector, prop, val);
|
|
363
|
+
commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
|
|
364
|
+
transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
|
|
354
365
|
} else if (activeSelector) {
|
|
355
366
|
activeStyle = style[activeSelector] || {};
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
368
|
+
mutateUpdateNestedStyle(t, sel, activeSelector, prop, val);
|
|
369
|
+
commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
|
|
370
|
+
transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
|
|
371
|
+
} else if (mediaTab !== null && mediaNames.length > 0) {
|
|
359
372
|
activeStyle = {};
|
|
360
|
-
for (const [p, v] of Object.entries(style[`@${
|
|
373
|
+
for (const [p, v] of Object.entries(style[`@${mediaTab}`] || {})) {
|
|
361
374
|
if (typeof v !== "object") activeStyle[p] = v;
|
|
362
375
|
}
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
377
|
+
mutateUpdateMediaStyle(t, sel, mediaTab, prop, val);
|
|
378
|
+
commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
|
|
379
|
+
transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
|
|
365
380
|
} else {
|
|
366
381
|
activeStyle = {};
|
|
367
382
|
for (const [p, v] of Object.entries(style)) {
|
|
368
383
|
if (typeof v !== "object") activeStyle[p] = v;
|
|
369
384
|
}
|
|
370
|
-
|
|
371
|
-
|
|
385
|
+
commitMutate = (/** @type {any} */ t, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
386
|
+
mutateUpdateStyle(t, sel, prop, val);
|
|
387
|
+
commitStyle = (/** @type {any} */ prop, /** @type {any} */ val) =>
|
|
388
|
+
transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
|
|
372
389
|
}
|
|
373
390
|
|
|
374
391
|
// ── Compute inherited style from higher breakpoints ──────────────────────
|
|
375
392
|
/** @type {Record<string, any>} */
|
|
376
|
-
const inheritedStyle = computeInheritedStyle(style, mediaNames,
|
|
393
|
+
const inheritedStyle = computeInheritedStyle(style, mediaNames, mediaTab, activeSelector);
|
|
377
394
|
|
|
378
395
|
// Auto-open sections that have properties
|
|
379
|
-
const newSections = autoOpenSections({ style: activeStyle },
|
|
380
|
-
if (JSON.stringify(newSections) !== JSON.stringify(
|
|
381
|
-
|
|
396
|
+
const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
|
|
397
|
+
if (JSON.stringify(newSections) !== JSON.stringify(tab.session.ui.styleSections)) {
|
|
398
|
+
tab.session.ui.styleSections = newSections;
|
|
382
399
|
}
|
|
383
400
|
|
|
384
401
|
// Partition properties into sections
|
|
@@ -402,8 +419,8 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
402
419
|
}
|
|
403
420
|
|
|
404
421
|
// ── Filter state ─────────────────────────────────────────────────────────
|
|
405
|
-
const filterText = (
|
|
406
|
-
const filterActive =
|
|
422
|
+
const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
|
|
423
|
+
const filterActive = tab.session.ui.styleFilterActive;
|
|
407
424
|
const isFiltering = filterText.length > 0 || filterActive;
|
|
408
425
|
|
|
409
426
|
// ── Section templates ────────────────────────────────────────────────────
|
|
@@ -449,7 +466,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
449
466
|
hasVal || longhands.some((/** @type {any} */ l) => activeStyle[l.name] !== undefined);
|
|
450
467
|
if (!hasAny && !condMet) continue;
|
|
451
468
|
rows.push(
|
|
452
|
-
renderShorthandRow(prop, entry, activeStyle,
|
|
469
|
+
renderShorthandRow(prop, entry, activeStyle, commitMutate, () => {}, inheritedStyle),
|
|
453
470
|
);
|
|
454
471
|
} else {
|
|
455
472
|
const isWarning = hasVal && !condMet;
|
|
@@ -459,9 +476,8 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
459
476
|
entry,
|
|
460
477
|
prop,
|
|
461
478
|
val ?? "",
|
|
462
|
-
(/** @type {any} */ newVal) =>
|
|
463
|
-
|
|
464
|
-
() => update(commitStyle(getState(), prop, undefined)),
|
|
479
|
+
(/** @type {any} */ newVal) => commitStyle(prop, newVal || undefined),
|
|
480
|
+
() => commitStyle(prop, undefined),
|
|
465
481
|
isWarning,
|
|
466
482
|
sec.$layout === "grid",
|
|
467
483
|
inheritedStyle[prop],
|
|
@@ -472,14 +488,17 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
472
488
|
}
|
|
473
489
|
|
|
474
490
|
if (isFiltering && rows.length === 0) return nothing;
|
|
475
|
-
const isOpen = isFiltering ? true : (
|
|
491
|
+
const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
|
|
476
492
|
|
|
477
493
|
return html`
|
|
478
494
|
<sp-accordion-item
|
|
479
495
|
label=${sec.label}
|
|
480
496
|
.open=${isOpen}
|
|
481
497
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
482
|
-
|
|
498
|
+
activeTab.value.session.ui.styleSections = {
|
|
499
|
+
...activeTab.value.session.ui.styleSections,
|
|
500
|
+
[sec.key]: e.target.open,
|
|
501
|
+
};
|
|
483
502
|
}}
|
|
484
503
|
>
|
|
485
504
|
${sectionActiveProps.length > 0
|
|
@@ -492,17 +511,17 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
492
511
|
@click=${(/** @type {any} */ e) => {
|
|
493
512
|
e.stopPropagation();
|
|
494
513
|
e.preventDefault();
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
514
|
+
transactDoc(activeTab.value, (t) => {
|
|
515
|
+
for (const { prop, entry } of sectionActiveProps) {
|
|
516
|
+
if (activeStyle[prop] !== undefined) commitMutate(t, prop, undefined);
|
|
517
|
+
if (inferInputType(entry) === "shorthand") {
|
|
518
|
+
for (const l of getLonghands(prop)) {
|
|
519
|
+
if (activeStyle[l.name] !== undefined)
|
|
520
|
+
commitMutate(t, l.name, undefined);
|
|
521
|
+
}
|
|
502
522
|
}
|
|
503
523
|
}
|
|
504
|
-
}
|
|
505
|
-
update(s);
|
|
524
|
+
});
|
|
506
525
|
}}
|
|
507
526
|
></span>
|
|
508
527
|
</span>
|
|
@@ -515,13 +534,16 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
515
534
|
|
|
516
535
|
// ── Custom section ─────────────────────────────────────────────────────────
|
|
517
536
|
const cssInitialMap = getCssInitialMap();
|
|
518
|
-
const customIsOpen =
|
|
537
|
+
const customIsOpen = tab.session.ui.styleSections.other ?? otherProps.length > 0;
|
|
519
538
|
const customSectionT = html`
|
|
520
539
|
<sp-accordion-item
|
|
521
540
|
label="Custom"
|
|
522
541
|
.open=${customIsOpen}
|
|
523
542
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
524
|
-
|
|
543
|
+
activeTab.value.session.ui.styleSections = {
|
|
544
|
+
...activeTab.value.session.ui.styleSections,
|
|
545
|
+
other: e.target.open,
|
|
546
|
+
};
|
|
525
547
|
}}
|
|
526
548
|
>
|
|
527
549
|
<div>
|
|
@@ -535,9 +557,10 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
535
557
|
@change=${(/** @type {any} */ e) => {
|
|
536
558
|
const newProp = e.target.value.trim();
|
|
537
559
|
if (newProp && newProp !== prop) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
560
|
+
transactDoc(activeTab.value, (t) => {
|
|
561
|
+
commitMutate(t, prop, undefined);
|
|
562
|
+
commitMutate(t, newProp, String(activeStyle[prop]));
|
|
563
|
+
});
|
|
541
564
|
}
|
|
542
565
|
}}
|
|
543
566
|
></sp-textfield>
|
|
@@ -547,14 +570,10 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
547
570
|
.value=${live(String(activeStyle[prop]))}
|
|
548
571
|
placeholder=${ifDefined(cssInitialMap.get(prop))}
|
|
549
572
|
@input=${debouncedStyleCommit(`custom:${prop}`, 400, (/** @type {any} */ e) => {
|
|
550
|
-
|
|
573
|
+
commitStyle(prop, e.target.value);
|
|
551
574
|
})}
|
|
552
575
|
></sp-textfield>
|
|
553
|
-
<sp-action-button
|
|
554
|
-
size="xs"
|
|
555
|
-
quiet
|
|
556
|
-
@click=${() => update(commitStyle(getState(), prop, undefined))}
|
|
557
|
-
>
|
|
576
|
+
<sp-action-button size="xs" quiet @click=${() => commitStyle(prop, undefined)}>
|
|
558
577
|
<sp-icon-close slot="icon"></sp-icon-close>
|
|
559
578
|
</sp-action-button>
|
|
560
579
|
</div>
|
|
@@ -571,7 +590,7 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
571
590
|
const prop = e.target.value.trim();
|
|
572
591
|
if (prop) {
|
|
573
592
|
const initial = cssInitialMap.get(prop) || "";
|
|
574
|
-
|
|
593
|
+
commitStyle(prop, initial || "");
|
|
575
594
|
e.target.value = "";
|
|
576
595
|
}
|
|
577
596
|
}
|
|
@@ -599,19 +618,23 @@ function styleSidebarTemplate(node, activeMediaTab, activeSelector) {
|
|
|
599
618
|
* @returns {import("lit-html").TemplateResult}
|
|
600
619
|
*/
|
|
601
620
|
export function renderStylePanelTemplate(ctx) {
|
|
602
|
-
const
|
|
603
|
-
if (
|
|
604
|
-
|
|
621
|
+
const tab = activeTab.value;
|
|
622
|
+
if (!tab) return html`<div class="empty-state">No document loaded</div>`;
|
|
623
|
+
if (ctx.getCanvasMode() === "settings" && tab.session.ui.stylebookSelection) {
|
|
624
|
+
const node = tab.doc.document;
|
|
605
625
|
if (!node) return html`<div class="empty-state">No document loaded</div>`;
|
|
606
626
|
return html`
|
|
607
|
-
<div class="stylebook-style-header">
|
|
608
|
-
|
|
627
|
+
<div class="stylebook-style-header">
|
|
628
|
+
Styling: <${tab.session.ui.stylebookSelection}>
|
|
629
|
+
</div>
|
|
630
|
+
${styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector)}
|
|
609
631
|
`;
|
|
610
632
|
}
|
|
611
|
-
if (!
|
|
612
|
-
|
|
633
|
+
if (!tab.session.selection)
|
|
634
|
+
return html`<div class="empty-state">Select an element to style</div>`;
|
|
635
|
+
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
613
636
|
if (!node) return html`<div class="empty-state">Select an element to style</div>`;
|
|
614
|
-
return styleSidebarTemplate(node,
|
|
637
|
+
return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector);
|
|
615
638
|
}
|
|
616
639
|
|
|
617
640
|
/** Single property input row (generic field row helper) */
|
|
@@ -9,8 +9,6 @@ import { styleMap } from "lit-html/directives/style-map.js";
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
getState,
|
|
12
|
-
update,
|
|
13
|
-
updateStyle,
|
|
14
12
|
updateSession,
|
|
15
13
|
updateUi,
|
|
16
14
|
canvasWrap,
|
|
@@ -18,6 +16,8 @@ import {
|
|
|
18
16
|
elToPath,
|
|
19
17
|
projectState,
|
|
20
18
|
} from "../store.js";
|
|
19
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
20
|
+
import { transactDoc, mutateUpdateStyle } from "../tabs/transact.js";
|
|
21
21
|
import { view } from "../view.js";
|
|
22
22
|
import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
23
23
|
import { componentRegistry } from "../files/components.js";
|
|
@@ -272,7 +272,7 @@ export function renderStylebookOverlays() {
|
|
|
272
272
|
const selectedTag = S.ui.stylebookSelection;
|
|
273
273
|
|
|
274
274
|
for (const panel of canvasPanels) {
|
|
275
|
-
const hoverTag = panel._lastHoverTag;
|
|
275
|
+
const hoverTag = /** @type {any} */ (panel)._lastHoverTag;
|
|
276
276
|
/**
|
|
277
277
|
* @type {{
|
|
278
278
|
* cls: string;
|
|
@@ -691,8 +691,9 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
691
691
|
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
692
692
|
if (swatch) swatch.style.backgroundColor = colorPicker.value;
|
|
693
693
|
if (!isNew && varName) {
|
|
694
|
-
|
|
695
|
-
|
|
694
|
+
transactDoc(activeTab.value, (t) =>
|
|
695
|
+
mutateUpdateStyle(t, [], varName, colorPicker.value),
|
|
696
|
+
);
|
|
696
697
|
}
|
|
697
698
|
}}
|
|
698
699
|
/>
|
|
@@ -751,8 +752,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
751
752
|
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
752
753
|
if (swatch) swatch.style.backgroundColor = v;
|
|
753
754
|
if (!isNew && varName) {
|
|
754
|
-
|
|
755
|
-
update(updateStyle(S, [], varName, v));
|
|
755
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
|
|
756
756
|
}
|
|
757
757
|
}, 400);
|
|
758
758
|
}}
|
|
@@ -765,8 +765,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
765
765
|
const bar = /** @type {any} */ (row.querySelector(".sb-var-size-bar"));
|
|
766
766
|
if (bar) bar.style.width = newVal;
|
|
767
767
|
if (!isNew && varName) {
|
|
768
|
-
|
|
769
|
-
update(updateStyle(S, [], varName, newVal));
|
|
768
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, newVal));
|
|
770
769
|
}
|
|
771
770
|
},
|
|
772
771
|
});
|
|
@@ -798,8 +797,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
798
797
|
const v = textFieldEl.value;
|
|
799
798
|
const fontPrev = /** @type {any} */ (row.querySelector(".sb-var-font-preview"));
|
|
800
799
|
if (fontPrev) fontPrev.style.fontFamily = v;
|
|
801
|
-
|
|
802
|
-
update(updateStyle(S, [], varName, v));
|
|
800
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, v));
|
|
803
801
|
}, 400);
|
|
804
802
|
}}
|
|
805
803
|
></sp-textfield>
|
|
@@ -824,8 +822,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
824
822
|
const val = getValueFn();
|
|
825
823
|
const generatedVar = friendlyNameToVar(name, catMeta.prefix);
|
|
826
824
|
if (!generatedVar || !val) return;
|
|
827
|
-
|
|
828
|
-
update(updateStyle(S, [], generatedVar, val));
|
|
825
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], generatedVar, val));
|
|
829
826
|
}}
|
|
830
827
|
>Add</sp-action-button
|
|
831
828
|
>
|
|
@@ -858,8 +855,7 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
858
855
|
class="sb-var-del"
|
|
859
856
|
style="pointer-events:auto"
|
|
860
857
|
@click=${() => {
|
|
861
|
-
|
|
862
|
-
update(updateStyle(S, [], varName, undefined));
|
|
858
|
+
transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], varName, undefined));
|
|
863
859
|
}}
|
|
864
860
|
>
|
|
865
861
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab strip — renders open tabs above the canvas area.
|
|
3
|
+
*
|
|
4
|
+
* Uses the reactive workspace model: reads from workspace.tabOrder, workspace.tabs,
|
|
5
|
+
* workspace.activeTabId. Clicks call activateTab/closeTab from workspace.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
9
|
+
import { effect, effectScope } from "../reactivity.js";
|
|
10
|
+
import { workspace, activateTab, closeTab } from "../workspace/workspace.js";
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("../tabs/tab.js").Tab} Tab */
|
|
13
|
+
|
|
14
|
+
/** @type {HTMLElement | null} */
|
|
15
|
+
let _host = null;
|
|
16
|
+
|
|
17
|
+
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
18
|
+
let _scope = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mount the tab strip into the given host element.
|
|
22
|
+
*
|
|
23
|
+
* @param {HTMLElement} host
|
|
24
|
+
*/
|
|
25
|
+
export function mount(host) {
|
|
26
|
+
_host = host;
|
|
27
|
+
_scope = effectScope();
|
|
28
|
+
_scope.run(() => {
|
|
29
|
+
effect(() => {
|
|
30
|
+
void workspace.tabOrder;
|
|
31
|
+
void workspace.activeTabId;
|
|
32
|
+
for (const tab of workspace.tabs.values()) {
|
|
33
|
+
void tab.doc.dirty;
|
|
34
|
+
void tab.documentPath;
|
|
35
|
+
}
|
|
36
|
+
render();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function unmount() {
|
|
42
|
+
_scope?.stop();
|
|
43
|
+
_scope = null;
|
|
44
|
+
_host = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render() {
|
|
48
|
+
if (!_host) return;
|
|
49
|
+
|
|
50
|
+
if (workspace.tabOrder.length <= 1) {
|
|
51
|
+
litRender(nothing, _host);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
litRender(
|
|
56
|
+
html`
|
|
57
|
+
<div class="tab-strip">
|
|
58
|
+
${workspace.tabOrder.map((id) => {
|
|
59
|
+
const tab = workspace.tabs.get(id);
|
|
60
|
+
if (!tab) return nothing;
|
|
61
|
+
const isActive = id === workspace.activeTabId;
|
|
62
|
+
const isDirty = tab.doc.dirty;
|
|
63
|
+
const label = tabLabel(tab);
|
|
64
|
+
return html`
|
|
65
|
+
<div
|
|
66
|
+
class="tab-strip-tab ${isActive ? "active" : ""}"
|
|
67
|
+
@click=${() => activateTab(id)}
|
|
68
|
+
@auxclick=${(/** @type {MouseEvent} */ e) => {
|
|
69
|
+
if (e.button === 1) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
requestClose(id);
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
title=${tab.documentPath || "Untitled"}
|
|
75
|
+
>
|
|
76
|
+
<span class="tab-strip-label">${label}</span>
|
|
77
|
+
${isDirty ? html`<span class="tab-strip-dirty">●</span>` : nothing}
|
|
78
|
+
<button
|
|
79
|
+
class="tab-strip-close"
|
|
80
|
+
@click=${(/** @type {Event} */ e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
requestClose(id);
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
×
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
_host,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive a short label from the tab's documentPath.
|
|
98
|
+
*
|
|
99
|
+
* @param {Tab} tab
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function tabLabel(tab) {
|
|
103
|
+
const path = tab.documentPath;
|
|
104
|
+
if (!path) return "Untitled";
|
|
105
|
+
const parts = path.split("/");
|
|
106
|
+
return parts[parts.length - 1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Close a tab, prompting if dirty.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} id
|
|
113
|
+
*/
|
|
114
|
+
function requestClose(id) {
|
|
115
|
+
const tab = workspace.tabs.get(id);
|
|
116
|
+
if (!tab) return;
|
|
117
|
+
if (tab.doc.dirty) {
|
|
118
|
+
const confirmed = window.confirm(
|
|
119
|
+
`"${tabLabel(tab)}" has unsaved changes. Close without saving?`,
|
|
120
|
+
);
|
|
121
|
+
if (!confirmed) return;
|
|
122
|
+
}
|
|
123
|
+
closeTab(id);
|
|
124
|
+
}
|