@jxsuite/studio 1.1.0 → 1.3.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/iframe-entry.js +139 -17
- package/dist/iframe-entry.js.map +7 -6
- package/dist/studio.js +88974 -88711
- package/dist/studio.js.map +377 -377
- package/package.json +8 -8
- package/src/canvas/iframe-entry.ts +17 -1
- package/src/canvas/iframe-host.ts +62 -1
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/canvas/iframe-render.ts +35 -14
- package/src/canvas/site-style-css.ts +121 -0
- package/src/panels/ai-panel.ts +53 -1
- package/src/panels/style-panel.ts +52 -22
- package/src/panels/tab-bar.ts +39 -4
- package/src/platforms/cloud.ts +3 -1
- package/src/project-list.ts +5 -0
- package/src/publish/publish-panel.ts +17 -0
- package/src/services/automation.ts +67 -1
- package/src/settings/css-vars-editor.ts +122 -4
- package/src/settings/settings-modal.ts +2 -2
- package/src/studio.ts +17 -1
- package/src/tabs/tab.ts +7 -0
- package/src/ui/formula-catalog.ts +4 -1
- package/src/utils/canvas-media.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"@atlaskit/pragmatic-drag-and-drop": "^2.0.1",
|
|
40
40
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.0",
|
|
41
41
|
"@jxsuite/ai": "^0.33.0",
|
|
42
|
-
"@jxsuite/collab": "^0.4.
|
|
43
|
-
"@jxsuite/create": "^1.1.
|
|
44
|
-
"@jxsuite/formulas": "^0.0.
|
|
45
|
-
"@jxsuite/markup": "^0.3.
|
|
46
|
-
"@jxsuite/protocol": "^0.5.
|
|
47
|
-
"@jxsuite/runtime": "^1.
|
|
48
|
-
"@jxsuite/schema": "^1.
|
|
42
|
+
"@jxsuite/collab": "^0.4.1",
|
|
43
|
+
"@jxsuite/create": "^1.1.1",
|
|
44
|
+
"@jxsuite/formulas": "^0.0.3",
|
|
45
|
+
"@jxsuite/markup": "^0.3.1",
|
|
46
|
+
"@jxsuite/protocol": "^0.5.1",
|
|
47
|
+
"@jxsuite/runtime": "^1.2.0",
|
|
48
|
+
"@jxsuite/schema": "^1.2.0",
|
|
49
49
|
"@spectrum-web-components/accordion": "^1.12.2",
|
|
50
50
|
"@spectrum-web-components/action-bar": "1.12.2",
|
|
51
51
|
"@spectrum-web-components/action-button": "^1.12.2",
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { postMessageChannel } from "./iframe-channel";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
applyPreviewColorScheme,
|
|
11
|
+
applySiteStyle,
|
|
12
|
+
installCanvasImageRetry,
|
|
13
|
+
renderResolvedDocument,
|
|
14
|
+
} from "./iframe-render";
|
|
10
15
|
import { measureHits, startInteraction } from "./iframe-interaction";
|
|
11
16
|
import {
|
|
12
17
|
AUTO_SCROLL_STEP,
|
|
@@ -482,9 +487,20 @@ export function startCanvasIframe(opts: {
|
|
|
482
487
|
clearIframeDrag();
|
|
483
488
|
return;
|
|
484
489
|
}
|
|
490
|
+
if (msg.kind === "setColorScheme") {
|
|
491
|
+
// Document-level attribute flip — deliberately patch-free (no render, no gen).
|
|
492
|
+
applyPreviewColorScheme(container.ownerDocument, msg.scheme);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (msg.kind === "siteStyleUpdate") {
|
|
496
|
+
// Live design-token edit: swap the site-style sheet in place, no re-render.
|
|
497
|
+
applySiteStyle(msg.siteStyle, msg.media);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
485
500
|
if (msg.kind !== "render" || msg.gen < latestGen) {
|
|
486
501
|
return;
|
|
487
502
|
}
|
|
503
|
+
applyPreviewColorScheme(container.ownerDocument, msg.colorScheme ?? null);
|
|
488
504
|
// A render replaces the DOM under a live edit session — COMMIT it first (never discard). The
|
|
489
505
|
// Resulting editCommit/editEnd post synchronously here, so on the FIFO channel they precede
|
|
490
506
|
// This render's renderComplete and the parent routes them by the host's not-yet-flipped tab
|
|
@@ -24,7 +24,15 @@ import { clearDragGhost, moveDragGhost } from "../panels/drag-ghost";
|
|
|
24
24
|
import { applyDropInstruction } from "../panels/dnd";
|
|
25
25
|
import { rectOf } from "../utils/geometry";
|
|
26
26
|
import { effect, effectScope } from "../reactivity";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
canvasPanels,
|
|
29
|
+
canvasWrap,
|
|
30
|
+
pathsEqual,
|
|
31
|
+
projectState,
|
|
32
|
+
renderOnly,
|
|
33
|
+
updateCanvas,
|
|
34
|
+
updateUi,
|
|
35
|
+
} from "../store";
|
|
28
36
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
29
37
|
import { collabState } from "../collab/collab-state";
|
|
30
38
|
import { getPlatform, hasPlatform } from "../platform";
|
|
@@ -1370,6 +1378,7 @@ export async function mountIframeCanvas(
|
|
|
1370
1378
|
// oxlint-disable-next-line unicorn/prefer-structured-clone
|
|
1371
1379
|
const cloneableShadow = JSON.parse(JSON.stringify(doc)) as unknown;
|
|
1372
1380
|
const message: ParentToIframe = {
|
|
1381
|
+
colorScheme: activeSchemeWire(),
|
|
1373
1382
|
doc: cloneableDoc,
|
|
1374
1383
|
docBase: resolved.docBase ?? `${canvasBaseOrigin()}/`,
|
|
1375
1384
|
gen,
|
|
@@ -1419,6 +1428,7 @@ export function mountStylebookCanvas(
|
|
|
1419
1428
|
// oxlint-disable-next-line unicorn/prefer-structured-clone
|
|
1420
1429
|
const cloneableShadow = JSON.parse(JSON.stringify(generated.doc)) as unknown;
|
|
1421
1430
|
const message: ParentToIframe = {
|
|
1431
|
+
colorScheme: activeSchemeWire(),
|
|
1422
1432
|
doc: cloneableDoc,
|
|
1423
1433
|
docBase: `${canvasBaseOrigin()}/`,
|
|
1424
1434
|
gen,
|
|
@@ -1443,6 +1453,57 @@ export function mountStylebookCanvas(
|
|
|
1443
1453
|
}
|
|
1444
1454
|
}
|
|
1445
1455
|
|
|
1456
|
+
/** The active tab's forced preview scheme as wire data (auto → null). */
|
|
1457
|
+
function activeSchemeWire(): "light" | "dark" | null {
|
|
1458
|
+
const s = activeTab.value?.session.ui.previewColorScheme;
|
|
1459
|
+
return s === "light" || s === "dark" ? s : null;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Flip the color-scheme preview on every ready host (page and stylebook alike — both render
|
|
1464
|
+
* scheme-aware CSS). A pure attribute write iframe-side: no render, no patch, no gen.
|
|
1465
|
+
*/
|
|
1466
|
+
export function postColorSchemeToLiveHosts(scheme: "light" | "dark" | null): void {
|
|
1467
|
+
for (const host of liveHosts) {
|
|
1468
|
+
if (!host.iframe.isConnected) {
|
|
1469
|
+
liveHosts.delete(host);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (host.ready) {
|
|
1473
|
+
host.channel.post({ kind: "setColorScheme", scheme });
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Push the project's current site style to every ready PAGE host as an in-place sheet replace (live
|
|
1480
|
+
* design-token editing — stylebook hosts pre-merge site style into the specimen doc and are
|
|
1481
|
+
* skipped). Render-free; the next full render carries the same style via its own siteStyle.
|
|
1482
|
+
*/
|
|
1483
|
+
export function postSiteStyleToLiveHosts(): void {
|
|
1484
|
+
const config = projectState?.projectConfig;
|
|
1485
|
+
if (!config) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
// Site config comes off a reactive store — only plain values may cross postMessage.
|
|
1489
|
+
// oxlint-disable-next-line unicorn/prefer-structured-clone
|
|
1490
|
+
const siteStyle = JSON.parse(JSON.stringify(config.style ?? null)) as Record<
|
|
1491
|
+
string,
|
|
1492
|
+
unknown
|
|
1493
|
+
> | null;
|
|
1494
|
+
// oxlint-disable-next-line unicorn/prefer-structured-clone
|
|
1495
|
+
const media = JSON.parse(JSON.stringify(config.$media ?? {})) as Record<string, string>;
|
|
1496
|
+
for (const host of liveHosts) {
|
|
1497
|
+
if (!host.iframe.isConnected) {
|
|
1498
|
+
liveHosts.delete(host);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (host.ready && !host.stylebook) {
|
|
1502
|
+
host.channel.post({ kind: "siteStyleUpdate", media, siteStyle });
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1446
1507
|
/**
|
|
1447
1508
|
* Post a live style update to every ready stylebook host (gen-tagged per host so a stale update is
|
|
1448
1509
|
* dropped iframe-side; the superseding render carries the same style). Returns how many hosts
|
|
@@ -61,8 +61,21 @@ export type ParentToIframe =
|
|
|
61
61
|
docBase: string;
|
|
62
62
|
mapperCtx: WireMapperCtx;
|
|
63
63
|
siteStyle: Record<string, unknown> | null;
|
|
64
|
+
// Forced color-scheme preview (spec §9.5): "light"/"dark" sets data-color-scheme on the
|
|
65
|
+
// Iframe root, null clears it (auto — follow the OS).
|
|
66
|
+
colorScheme: "light" | "dark" | null;
|
|
64
67
|
gen: number;
|
|
65
68
|
}
|
|
69
|
+
// Flip the forced color-scheme preview on the iframe root without re-rendering — a document-level
|
|
70
|
+
// Idempotent attribute write, deliberately gen-less (like endEdit).
|
|
71
|
+
| { kind: "setColorScheme"; scheme: "light" | "dark" | null }
|
|
72
|
+
// Replace the injected site-style sheet in place (live design-token editing) — idempotent and
|
|
73
|
+
// Render-free; the superseding render carries the same style via its own siteStyle.
|
|
74
|
+
| {
|
|
75
|
+
kind: "siteStyleUpdate";
|
|
76
|
+
siteStyle: Record<string, unknown> | null;
|
|
77
|
+
media: Record<string, string>;
|
|
78
|
+
}
|
|
66
79
|
// Ask the iframe to measure the given document paths and post their current rects back. Used to
|
|
67
80
|
// Draw the selection overlay regardless of where the selection change originated (canvas click,
|
|
68
81
|
// Layers panel, keyboard) — the parent can't measure iframe nodes itself (cross-origin bridge).
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
transposeCanvasUnits,
|
|
25
25
|
} from "@jxsuite/runtime";
|
|
26
26
|
import { classifyRenderNode, serializeJxPath } from "./path-mapping";
|
|
27
|
+
import { SITE_STYLE_ID, buildSiteStyleCSS } from "./site-style-css";
|
|
27
28
|
import type { CanvasMode } from "./iframe-protocol";
|
|
28
29
|
import type { JxDocument } from "@jxsuite/schema/types";
|
|
29
30
|
import type { PathMapCtx } from "./path-mapping";
|
|
@@ -294,22 +295,42 @@ export function syncStylebookCss(doc: Document, mode: CanvasMode): void {
|
|
|
294
295
|
doc.head.append(style);
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
/**
|
|
298
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Apply the project's site style as a real stylesheet (replace-in-place): custom properties on
|
|
300
|
+
* `:root`, plain properties on `body`, conditional blocks dual-emitted per the forced-scheme
|
|
301
|
+
* contract. A stylesheet — not inline root properties — so `:root[data-color-scheme]` override
|
|
302
|
+
* selectors can win (spec §9.5), and so removed tokens can't linger on reused iframes.
|
|
303
|
+
*/
|
|
304
|
+
export function applySiteStyle(
|
|
305
|
+
siteStyle: Record<string, unknown> | null | undefined,
|
|
306
|
+
mediaQueries: Record<string, string> = {},
|
|
307
|
+
): void {
|
|
308
|
+
const existing = document.head.querySelector(`#${SITE_STYLE_ID}`);
|
|
299
309
|
if (!siteStyle || typeof siteStyle !== "object") {
|
|
310
|
+
existing?.remove();
|
|
300
311
|
return;
|
|
301
312
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
const css = buildSiteStyleCSS(siteStyle, mediaQueries, transposeCanvasUnits);
|
|
314
|
+
if (existing) {
|
|
315
|
+
existing.textContent = css;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const tag = document.createElement("style");
|
|
319
|
+
tag.id = SITE_STYLE_ID;
|
|
320
|
+
tag.textContent = css;
|
|
321
|
+
document.head.append(tag);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Force or clear the color-scheme preview on the iframe's root element (the platform's
|
|
326
|
+
* data-color-scheme contract, spec §9.5). Survives re-renders and patches — renders only replace
|
|
327
|
+
* the container's children, never the root element.
|
|
328
|
+
*/
|
|
329
|
+
export function applyPreviewColorScheme(doc: Document, scheme: "light" | "dark" | null): void {
|
|
330
|
+
if (scheme) {
|
|
331
|
+
doc.documentElement.dataset.colorScheme = scheme;
|
|
332
|
+
} else {
|
|
333
|
+
delete doc.documentElement.dataset.colorScheme;
|
|
313
334
|
}
|
|
314
335
|
}
|
|
315
336
|
|
|
@@ -443,7 +464,7 @@ export async function renderResolvedDocument(opts: {
|
|
|
443
464
|
// Iframe clears it (page-level templates are inert in design/edit via prepareForEditMode, so only
|
|
444
465
|
// Component internals get stamped).
|
|
445
466
|
setStampPropBindings(opts.mode === "design" || opts.mode === "edit");
|
|
446
|
-
applySiteStyle(opts.siteStyle);
|
|
467
|
+
applySiteStyle(opts.siteStyle, (opts.doc as { $media?: Record<string, string> }).$media ?? {});
|
|
447
468
|
injectHead(opts.doc);
|
|
448
469
|
syncEditModeCss(opts.container.ownerDocument, opts.mode);
|
|
449
470
|
syncStylebookCss(opts.container.ownerDocument, opts.mode);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Site-style stylesheet builder — pure functions extracted for testability.
|
|
4
|
+
*
|
|
5
|
+
* The canvas iframe injects the project's `style` as a real stylesheet instead of inline properties
|
|
6
|
+
* on the root element: inline custom properties would defeat the forced-scheme override selectors
|
|
7
|
+
* (`:root[data-color-scheme]`, spec §9.5), and object-valued `@--name` blocks used to be dropped
|
|
8
|
+
* entirely. Scheme-query blocks dual-emit through the runtime's schemeSelectors so the canvas
|
|
9
|
+
* honors both the OS preference and the preview toggle.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { pureSchemeOf, schemeSelectors, camelToKebab } from "@jxsuite/runtime";
|
|
13
|
+
|
|
14
|
+
/** Id of the injected site-style tag (replace-in-place, never accumulate). */
|
|
15
|
+
export const SITE_STYLE_ID = "jx-site-style";
|
|
16
|
+
|
|
17
|
+
/** Serialize scalar declarations, kebab-casing property names and transposing values. */
|
|
18
|
+
function toDecls(props: Record<string, unknown>, transpose: (value: string) => string): string {
|
|
19
|
+
return Object.entries(props)
|
|
20
|
+
.map(([key, value]) => `${camelToKebab(key)}: ${transpose(String(value))}`)
|
|
21
|
+
.join("; ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the site-style sheet text: custom properties on `:root`, plain properties on `body`,
|
|
26
|
+
* conditional `@`-blocks resolved against `mediaQueries` (scheme queries dual-emitted per the
|
|
27
|
+
* forced-scheme contract), and `color-scheme: light dark` declared when a scheme query exists.
|
|
28
|
+
*
|
|
29
|
+
* @param {Record<string, unknown>} siteStyle
|
|
30
|
+
* @param {Record<string, string>} mediaQueries
|
|
31
|
+
* @param {(value: string) => string} transpose - Unit transposer (canvas vh→cqh etc.)
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function buildSiteStyleCSS(
|
|
35
|
+
siteStyle: Record<string, unknown>,
|
|
36
|
+
mediaQueries: Record<string, string>,
|
|
37
|
+
transpose: (value: string) => string,
|
|
38
|
+
): string {
|
|
39
|
+
const rules: string[] = [];
|
|
40
|
+
const rootProps: Record<string, unknown> = {};
|
|
41
|
+
const bodyProps: Record<string, unknown> = {};
|
|
42
|
+
const condBlocks: [string, Record<string, unknown>][] = [];
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Object.entries(siteStyle)) {
|
|
45
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
46
|
+
if (key.startsWith("@")) {
|
|
47
|
+
condBlocks.push([key, value as Record<string, unknown>]);
|
|
48
|
+
}
|
|
49
|
+
// Non-@ objects (nested element selectors) are page-content styling — the resolved doc's
|
|
50
|
+
// Own style pass covers those; the site sheet handles tokens + conditional overrides.
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[") || key.startsWith("@")) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (key.startsWith("--")) {
|
|
57
|
+
rootProps[key] = value;
|
|
58
|
+
} else {
|
|
59
|
+
bodyProps[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rootCSS = toDecls(rootProps, transpose);
|
|
64
|
+
if (rootCSS) {
|
|
65
|
+
rules.push(`:root { ${rootCSS} }`);
|
|
66
|
+
}
|
|
67
|
+
const bodyCSS = toDecls(bodyProps, transpose);
|
|
68
|
+
if (bodyCSS) {
|
|
69
|
+
rules.push(`body { ${bodyCSS} }`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [atKey, block] of condBlocks) {
|
|
73
|
+
const query = atKey.startsWith("@--")
|
|
74
|
+
? (mediaQueries[atKey.slice(1)] ?? atKey.slice(1))
|
|
75
|
+
: atKey.startsWith("@(")
|
|
76
|
+
? atKey.slice(1)
|
|
77
|
+
: null;
|
|
78
|
+
if (query === null) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const scheme = pureSchemeOf(query);
|
|
82
|
+
const emit = (selector: string, props: string) => {
|
|
83
|
+
if (!props) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (scheme) {
|
|
87
|
+
const { auto, forced } = schemeSelectors(selector, scheme);
|
|
88
|
+
rules.push(`@media ${query} { ${auto} { ${props} } }`, `${forced} { ${props} }`);
|
|
89
|
+
} else {
|
|
90
|
+
rules.push(`@media ${query} { ${selector} { ${props} } }`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const condRoot: Record<string, unknown> = {};
|
|
94
|
+
const condBody: Record<string, unknown> = {};
|
|
95
|
+
const condSubs: [string, Record<string, unknown>][] = [];
|
|
96
|
+
for (const [k, v] of Object.entries(block)) {
|
|
97
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
98
|
+
if (!k.startsWith("@")) {
|
|
99
|
+
condSubs.push([k, v as Record<string, unknown>]);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (k.startsWith("--")) {
|
|
104
|
+
condRoot[k] = v;
|
|
105
|
+
} else {
|
|
106
|
+
condBody[k] = v;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
emit(":root", toDecls(condRoot, transpose));
|
|
110
|
+
emit("body", toDecls(condBody, transpose));
|
|
111
|
+
for (const [sel, sub] of condSubs) {
|
|
112
|
+
emit(sel, toDecls(sub, transpose));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Object.values(mediaQueries).some((q) => pureSchemeOf(q) !== null)) {
|
|
117
|
+
rules.push(":root { color-scheme: light dark }");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rules.join("\n");
|
|
121
|
+
}
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { TemplateResult } from "lit-html";
|
|
|
21
21
|
import { getPlatform } from "../platform";
|
|
22
22
|
import { effect, effectScope } from "../reactivity";
|
|
23
23
|
import { createDocumentAssistant } from "../services/document-assistant";
|
|
24
|
-
import { hasOpenAiKey } from "../services/ai-settings";
|
|
24
|
+
import { hasOpenAiKey, setOpenAiKey } from "../services/ai-settings";
|
|
25
25
|
import { fetchAvailableModels, isManagedProxy, isProxyConfigured } from "../services/ai-models";
|
|
26
26
|
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
27
27
|
import { clearMarkdownCache } from "./ai-chat/chat-markdown";
|
|
@@ -263,6 +263,58 @@ export async function seedAssistantPrompt(text: string): Promise<void> {
|
|
|
263
263
|
await handleAssistantSend(text);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
// ─── Automation seeding (screenshot runner) ─────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/** A canned tool-call chip for {@link seedAssistantMessages}. */
|
|
269
|
+
export interface SeededToolCall {
|
|
270
|
+
name: string;
|
|
271
|
+
/** JSON-encoded arguments, e.g. `{"path":["children",0],"text":"…"}`. */
|
|
272
|
+
arguments: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A canned transcript entry for {@link seedAssistantMessages}. */
|
|
276
|
+
export interface SeededAssistantMessage {
|
|
277
|
+
role: "user" | "assistant";
|
|
278
|
+
content: string;
|
|
279
|
+
toolCalls?: SeededToolCall[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let seededCount = 0;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Automation-only seam (scripts/screenshots): stage a canned conversation without ever invoking a
|
|
286
|
+
* model. Stores an inert demo key so the key gate opens (localStorage-only on the dev server — no
|
|
287
|
+
* request fires), switches to the chat view, and pushes fully-formed messages straight into the
|
|
288
|
+
* reactive chat state — the same path session restore uses — so the panel repaints through its
|
|
289
|
+
* normal watcher.
|
|
290
|
+
*/
|
|
291
|
+
export function seedAssistantMessages(messages: SeededAssistantMessage[]): void {
|
|
292
|
+
setOpenAiKey("sk-demo");
|
|
293
|
+
keyEditing = false;
|
|
294
|
+
view = "chat";
|
|
295
|
+
stickToBottom = true;
|
|
296
|
+
for (const msg of messages) {
|
|
297
|
+
seededCount += 1;
|
|
298
|
+
const seq = seededCount;
|
|
299
|
+
assistant.chatState.messages.push({
|
|
300
|
+
content: msg.content,
|
|
301
|
+
id: `seeded_${seq}`,
|
|
302
|
+
role: msg.role,
|
|
303
|
+
timestamp: seq,
|
|
304
|
+
...(msg.toolCalls
|
|
305
|
+
? {
|
|
306
|
+
toolCalls: msg.toolCalls.map((tc, i) => ({
|
|
307
|
+
arguments: tc.arguments,
|
|
308
|
+
id: `seeded_${seq}_tc${i}`,
|
|
309
|
+
name: tc.name,
|
|
310
|
+
})),
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
scheduleAiRender();
|
|
316
|
+
}
|
|
317
|
+
|
|
266
318
|
// ─── Controls ─────────────────────────────────────────────────────────────────
|
|
267
319
|
|
|
268
320
|
function stop() {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { inferInputType, propLabel } from "../utils/studio-utils";
|
|
24
24
|
import { renderFieldRow } from "../ui/field-row";
|
|
25
25
|
import { renderDynamicSlot } from "../ui/dynamic-slot";
|
|
26
|
-
import { parseMediaEntries } from "../utils/canvas-media";
|
|
26
|
+
import { parseMediaEntries, schemeOfQuery } from "../utils/canvas-media";
|
|
27
27
|
import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
|
|
28
28
|
import { computeInheritedStyle } from "../utils/inherited-style";
|
|
29
29
|
import { mediaDisplayName } from "./shared";
|
|
@@ -313,6 +313,20 @@ function styleSidebarTemplate(
|
|
|
313
313
|
const mediaNames = sizeBreakpoints.map((bp) => bp.name);
|
|
314
314
|
const mediaTab = activeMediaTab || null;
|
|
315
315
|
|
|
316
|
+
// ── Scheme-layer routing (spec §9.5) ────────────────────────────────────────
|
|
317
|
+
// With the tab-bar scheme control forcing a scheme that has a matching declared scheme query,
|
|
318
|
+
// Base-context edits target that scheme's `@--name` block — no extra sidebar tabs. Breakpoint
|
|
319
|
+
// Tabs stay breakpoint-scoped (scheme × breakpoint compound blocks are unsupported).
|
|
320
|
+
const forcedScheme = tab.session.ui.previewColorScheme;
|
|
321
|
+
const schemeLayer =
|
|
322
|
+
mediaTab === null && (forcedScheme === "light" || forcedScheme === "dark")
|
|
323
|
+
? (Object.entries(getEffectiveMedia(tab.doc.document.$media) ?? {}).find(
|
|
324
|
+
([, q]) => schemeOfQuery(String(q)) === forcedScheme,
|
|
325
|
+
)?.[0] ?? null)
|
|
326
|
+
: null;
|
|
327
|
+
// The media context edits actually target: a valid breakpoint tab, else the scheme layer.
|
|
328
|
+
const editMedia = mediaTab && mediaNames.length > 0 ? mediaTab : schemeLayer;
|
|
329
|
+
|
|
316
330
|
// ── Media tabs template ──────────────────────────────────────────────────
|
|
317
331
|
const mediaTabsT =
|
|
318
332
|
mediaNames.length > 0
|
|
@@ -337,7 +351,9 @@ function styleSidebarTemplate(
|
|
|
337
351
|
: nothing;
|
|
338
352
|
|
|
339
353
|
// ── Selector dropdown ──────────────────────────────────────────────────────
|
|
340
|
-
const contextStyle =
|
|
354
|
+
const contextStyle = editMedia
|
|
355
|
+
? (style[`@${editMedia}`] as Record<string, unknown>) || {}
|
|
356
|
+
: style;
|
|
341
357
|
const existingSelectors = Object.keys(contextStyle).filter((s) => isNestedSelector(s));
|
|
342
358
|
const existingSet = new Set(existingSelectors);
|
|
343
359
|
const commonSet = new Set(COMMON_SELECTORS);
|
|
@@ -413,10 +429,21 @@ function styleSidebarTemplate(
|
|
|
413
429
|
</sp-picker>
|
|
414
430
|
`;
|
|
415
431
|
|
|
416
|
-
// ── Combined toolbar (media tabs + selector)
|
|
432
|
+
// ── Combined toolbar (media tabs + scheme badge + selector) ────────────────
|
|
433
|
+
// No scheme tabs — the tab-bar control is the one switch; the badge shows where edits land.
|
|
434
|
+
const schemeBadgeT = schemeLayer
|
|
435
|
+
? html`
|
|
436
|
+
<span
|
|
437
|
+
class="style-scheme-badge"
|
|
438
|
+
title="Edits target the @${schemeLayer} variant — set the tab-bar scheme control to Auto to edit base styles"
|
|
439
|
+
>
|
|
440
|
+
${mediaDisplayName(schemeLayer)} variant
|
|
441
|
+
</span>
|
|
442
|
+
`
|
|
443
|
+
: nothing;
|
|
417
444
|
const toolbarT = html`
|
|
418
445
|
<div class="style-toolbar">
|
|
419
|
-
<div class="style-toolbar-tabs">${mediaTabsT}</div>
|
|
446
|
+
<div class="style-toolbar-tabs">${mediaTabsT} ${schemeBadgeT}</div>
|
|
420
447
|
${selectorT}
|
|
421
448
|
</div>
|
|
422
449
|
`;
|
|
@@ -450,15 +477,15 @@ function styleSidebarTemplate(
|
|
|
450
477
|
// ── Determine the active style object ──────────────────────────────────────
|
|
451
478
|
let activeStyle: JxStyle;
|
|
452
479
|
let commitMutate: StyleMutateFn;
|
|
453
|
-
if (activeSelector && isTagPath(activeSelector) &&
|
|
454
|
-
const mediaObj = getNestedStyle(style, `@${
|
|
480
|
+
if (activeSelector && isTagPath(activeSelector) && editMedia) {
|
|
481
|
+
const mediaObj = getNestedStyle(style, `@${editMedia}`) ?? {};
|
|
455
482
|
activeStyle = resolveNestedTagStyle(mediaObj, activeSelector);
|
|
456
483
|
const stylePath = activeSelector.split(" ");
|
|
457
484
|
commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
|
|
458
485
|
mutateUpdateMediaNestedStylePath(
|
|
459
486
|
t,
|
|
460
487
|
sel,
|
|
461
|
-
|
|
488
|
+
editMedia,
|
|
462
489
|
stylePath,
|
|
463
490
|
prop,
|
|
464
491
|
val as string | undefined,
|
|
@@ -468,14 +495,14 @@ function styleSidebarTemplate(
|
|
|
468
495
|
const stylePath = activeSelector.split(" ");
|
|
469
496
|
commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
|
|
470
497
|
mutateUpdateNestedStylePath(t, sel, stylePath, prop, val as string | undefined);
|
|
471
|
-
} else if (activeSelector &&
|
|
472
|
-
const mediaObj = getNestedStyle(style, `@${
|
|
498
|
+
} else if (activeSelector && editMedia) {
|
|
499
|
+
const mediaObj = getNestedStyle(style, `@${editMedia}`) ?? {};
|
|
473
500
|
activeStyle = getNestedStyle(mediaObj, activeSelector) ?? {};
|
|
474
501
|
commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
|
|
475
502
|
mutateUpdateMediaNestedStyle(
|
|
476
503
|
t,
|
|
477
504
|
sel,
|
|
478
|
-
|
|
505
|
+
editMedia,
|
|
479
506
|
activeSelector,
|
|
480
507
|
prop,
|
|
481
508
|
val as string | undefined,
|
|
@@ -486,29 +513,32 @@ function styleSidebarTemplate(
|
|
|
486
513
|
mutateUpdateNestedStyle(t, sel, activeSelector, prop, val as string | undefined);
|
|
487
514
|
} else {
|
|
488
515
|
activeStyle = {};
|
|
489
|
-
const
|
|
490
|
-
const flatSource =
|
|
516
|
+
const inMediaCtx = editMedia !== null;
|
|
517
|
+
const flatSource = inMediaCtx ? (getNestedStyle(style, `@${editMedia}`) ?? {}) : style;
|
|
491
518
|
for (const [p, v] of Object.entries(flatSource)) {
|
|
492
519
|
if (typeof v !== "object") {
|
|
493
520
|
activeStyle[p] = v;
|
|
494
521
|
}
|
|
495
522
|
}
|
|
496
|
-
commitMutate =
|
|
523
|
+
commitMutate = inMediaCtx
|
|
497
524
|
? (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
|
|
498
|
-
mutateUpdateMediaStyle(t, sel,
|
|
525
|
+
mutateUpdateMediaStyle(t, sel, editMedia, prop, val as string | undefined)
|
|
499
526
|
: (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
|
|
500
527
|
mutateUpdateStyle(t, sel, prop, val as string | undefined);
|
|
501
528
|
}
|
|
502
529
|
const commitStyle = (prop: string, val?: string | Record<string, unknown> | undefined) =>
|
|
503
530
|
transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
|
|
504
531
|
|
|
505
|
-
// ── Compute inherited style
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
532
|
+
// ── Compute inherited style ────────────────────────────────────────────────
|
|
533
|
+
// Scheme layer: the base styles show through as inherited (placeholders); breakpoint tabs
|
|
534
|
+
// Inherit from higher breakpoints as before.
|
|
535
|
+
const inheritedStyle: Record<string, string | number> = schemeLayer
|
|
536
|
+
? (Object.fromEntries(
|
|
537
|
+
Object.entries(
|
|
538
|
+
activeSelector ? (getNestedStyle(style, activeSelector) ?? {}) : style,
|
|
539
|
+
).filter(([, v]) => typeof v !== "object"),
|
|
540
|
+
) as Record<string, string | number>)
|
|
541
|
+
: computeInheritedStyle(style, mediaNames, mediaTab, activeSelector);
|
|
512
542
|
|
|
513
543
|
// Auto-open sections that have properties
|
|
514
544
|
const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
|
|
@@ -683,7 +713,7 @@ function styleSidebarTemplate(
|
|
|
683
713
|
sec.$layout === "grid",
|
|
684
714
|
inheritedStyle[prop] as string | undefined,
|
|
685
715
|
templateSignals,
|
|
686
|
-
`style|${sel.join("/")}|${
|
|
716
|
+
`style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}|${prop}`,
|
|
687
717
|
),
|
|
688
718
|
);
|
|
689
719
|
}
|
package/src/panels/tab-bar.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { effect, effectScope } from "../reactivity";
|
|
|
15
15
|
import { activeTab } from "../workspace/workspace";
|
|
16
16
|
import { applyTransform, fitToScreen, resetZoom, setEditZoom } from "../canvas/canvas-utils";
|
|
17
17
|
import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
|
|
18
|
+
import { isSchemeQuery } from "../utils/canvas-media";
|
|
18
19
|
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
19
20
|
import { componentPropEntries, isComponentDoc } from "../component-props";
|
|
20
21
|
import { mediaDisplayName } from "./shared";
|
|
@@ -75,6 +76,7 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
|
|
|
75
76
|
void tab.session.ui.editingFunction;
|
|
76
77
|
void tab.session.ui.featureToggles;
|
|
77
78
|
void tab.session.ui.preview;
|
|
79
|
+
void tab.session.ui.previewColorScheme;
|
|
78
80
|
void tab.session.ui.previewParams;
|
|
79
81
|
void tab.session.ui.previewProps;
|
|
80
82
|
void tab.session.ui.showLayout;
|
|
@@ -308,13 +310,46 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
308
310
|
}
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
// ── Right region:
|
|
313
|
+
// ── Right region: color-scheme preview (projects that declare a scheme query, spec §9.5) ──
|
|
314
|
+
// One switch per tab: drives the canvas data-color-scheme attribute and (in the style
|
|
315
|
+
// Sidebar) which scheme layer edits target. Ungated — works in every canvas mode.
|
|
312
316
|
const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
|
|
317
|
+
const schemeQueries = featureQueries.filter(({ query }) => isSchemeQuery(query));
|
|
318
|
+
const plainQueries = featureQueries.filter(({ query }) => !isSchemeQuery(query));
|
|
319
|
+
const scheme = S.ui.previewColorScheme ?? "auto";
|
|
320
|
+
const schemeTpl =
|
|
321
|
+
schemeQueries.length > 0
|
|
322
|
+
? html`
|
|
323
|
+
<sp-action-group compact size="s" class="tb-scheme">
|
|
324
|
+
${(
|
|
325
|
+
[
|
|
326
|
+
["auto", "Auto", "Follow the OS color scheme"],
|
|
327
|
+
["light", "Light", "Force the light scheme"],
|
|
328
|
+
["dark", "Dark", "Force the dark scheme"],
|
|
329
|
+
] as const
|
|
330
|
+
).map(
|
|
331
|
+
([value, label, title]) => html`
|
|
332
|
+
<sp-action-button
|
|
333
|
+
toggles
|
|
334
|
+
size="s"
|
|
335
|
+
title=${title}
|
|
336
|
+
?selected=${scheme === value}
|
|
337
|
+
@click=${() => updateUi("previewColorScheme", value)}
|
|
338
|
+
>
|
|
339
|
+
${label}
|
|
340
|
+
</sp-action-button>
|
|
341
|
+
`,
|
|
342
|
+
)}
|
|
343
|
+
</sp-action-group>
|
|
344
|
+
`
|
|
345
|
+
: nothing;
|
|
346
|
+
|
|
347
|
+
// ── Right region: media feature toggles (non-scheme queries) ──
|
|
313
348
|
const togglesTpl =
|
|
314
|
-
|
|
349
|
+
plainQueries.length > 0
|
|
315
350
|
? html`
|
|
316
351
|
<sp-action-group compact size="s">
|
|
317
|
-
${
|
|
352
|
+
${plainQueries.map(
|
|
318
353
|
({ name, query }: { name: string; query: string }) => html`
|
|
319
354
|
<sp-action-button
|
|
320
355
|
toggles
|
|
@@ -352,7 +387,7 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
|
|
|
352
387
|
<div class="tab-bar">
|
|
353
388
|
${navTpl} ${zoomTpl}
|
|
354
389
|
<div class="tb-spacer"></div>
|
|
355
|
-
${settingsTpl} ${togglesTpl} ${exportTpl}
|
|
390
|
+
${settingsTpl} ${schemeTpl} ${togglesTpl} ${exportTpl}
|
|
356
391
|
</div>
|
|
357
392
|
`;
|
|
358
393
|
}
|