@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/src/platforms/cloud.ts
CHANGED
|
@@ -334,7 +334,9 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
334
334
|
* {kind:"git"} notices this handler ignores.
|
|
335
335
|
*/
|
|
336
336
|
subscribeFileEvents(handler: (events: FsEvent[]) => void) {
|
|
337
|
-
|
|
337
|
+
// No project means an empty base, so the bare `/events` URL hits no gateway route.
|
|
338
|
+
// That fails in a reconnect loop; mirror `collab`'s guard and degrade to no-op on the hub.
|
|
339
|
+
if (!project || typeof WebSocket === "undefined" || typeof location === "undefined") {
|
|
338
340
|
return () => {};
|
|
339
341
|
}
|
|
340
342
|
let socket: WebSocket | null = null;
|
package/src/project-list.ts
CHANGED
|
@@ -36,3 +36,8 @@ export function getProjectList(): ProjectListEntry[] {
|
|
|
36
36
|
export function resetProjectList(): void {
|
|
37
37
|
cache = [];
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Seed seam for tests and the screenshot automation hook (staged catalogues). */
|
|
41
|
+
export function seedProjectList(entries: ProjectListEntry[]): void {
|
|
42
|
+
cache = entries;
|
|
43
|
+
}
|
|
@@ -339,6 +339,23 @@ function render(): void {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Automation-only seam (scripts/screenshots): open the modal directly in its connected state with a
|
|
344
|
+
* canned deployment, bypassing {@link loadConnection} so no Cloudflare request ever fires. The
|
|
345
|
+
* active project's `build.deploy` block still supplies the connected project/URL line.
|
|
346
|
+
*/
|
|
347
|
+
export function seedPublishConnected(options: {
|
|
348
|
+
accountId?: string;
|
|
349
|
+
deployment: PagesDeploymentInfo;
|
|
350
|
+
}): void {
|
|
351
|
+
_connection = { accountId: options.accountId ?? "demo-account", connected: true };
|
|
352
|
+
_accounts = [];
|
|
353
|
+
_deployment = options.deployment;
|
|
354
|
+
_error = "";
|
|
355
|
+
_busy = false;
|
|
356
|
+
render();
|
|
357
|
+
}
|
|
358
|
+
|
|
342
359
|
/** Open the publish modal for the active project. */
|
|
343
360
|
export function openPublishPanel(): void {
|
|
344
361
|
const config = currentConfig();
|
|
@@ -10,18 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { renderOnly, updateUi } from "../store";
|
|
13
|
-
import {
|
|
13
|
+
import { setProjectState } from "../state";
|
|
14
|
+
import { seedProjectList } from "../project-list";
|
|
15
|
+
import { activeTab, closeAllTabs, workspace } from "../workspace/workspace";
|
|
16
|
+
import type { ProjectListEntry } from "../types";
|
|
14
17
|
import { applyPanelCollapse, view } from "../view";
|
|
15
18
|
import { setEditZoom as applyEditZoomLevel } from "../canvas/canvas-utils";
|
|
19
|
+
import { collabState } from "../collab/collab-state";
|
|
20
|
+
import type { PeerPresence } from "../collab/collab-state";
|
|
21
|
+
import type { SeededAssistantMessage } from "../panels/ai-panel";
|
|
22
|
+
import type { PagesDeploymentInfo } from "../publish/pages-service";
|
|
16
23
|
import type { JxPath } from "../state";
|
|
17
24
|
|
|
18
25
|
/** Callbacks injected from studio.ts (module-local helpers or imports kept out of this module). */
|
|
19
26
|
export interface AutomationDeps {
|
|
20
27
|
getCanvasMode: () => string;
|
|
21
28
|
openBrowseModal: () => void;
|
|
29
|
+
openConnectorGrid: (connection: string | undefined, table: string) => void;
|
|
22
30
|
openNewProjectModal: () => void;
|
|
31
|
+
openQuickSearchPalette: () => void;
|
|
32
|
+
openSettingsModal: (section?: string) => void;
|
|
23
33
|
render: () => void;
|
|
24
34
|
renderActivityBar: () => void;
|
|
35
|
+
seedAssistantMessages: (messages: SeededAssistantMessage[]) => void;
|
|
36
|
+
seedPublishConnected: (options: { accountId?: string; deployment: PagesDeploymentInfo }) => void;
|
|
25
37
|
setCanvasMode: (mode: string) => void;
|
|
26
38
|
statusMessage: (text: string) => void;
|
|
27
39
|
}
|
|
@@ -30,7 +42,14 @@ export interface AutomationApi {
|
|
|
30
42
|
editDef: (defName: string) => void;
|
|
31
43
|
editFunction: (path: JxPath, eventKey: string) => void;
|
|
32
44
|
openBrowse: () => void;
|
|
45
|
+
openDataGrid: (options: { connection?: string; table: string }) => void;
|
|
33
46
|
openNewProject: () => void;
|
|
47
|
+
openQuickSearch: () => void;
|
|
48
|
+
openSettings: (section?: string) => void;
|
|
49
|
+
seedAssistant: (options: { messages: SeededAssistantMessage[] }) => void;
|
|
50
|
+
seedCollab: (options: { peers: PeerPresence[] }) => void;
|
|
51
|
+
seedPublish: (options: { accountId?: string; deployment: PagesDeploymentInfo }) => void;
|
|
52
|
+
showWelcome: (options?: { projects?: ProjectListEntry[] }) => void;
|
|
34
53
|
getState: () => {
|
|
35
54
|
activeTabId: string | null;
|
|
36
55
|
canvasMode: string;
|
|
@@ -66,9 +85,56 @@ export function createAutomationApi(deps: AutomationDeps): AutomationApi {
|
|
|
66
85
|
openBrowse() {
|
|
67
86
|
deps.openBrowseModal();
|
|
68
87
|
},
|
|
88
|
+
openDataGrid(options: { connection?: string; table: string }) {
|
|
89
|
+
deps.openConnectorGrid(options.connection, options.table);
|
|
90
|
+
},
|
|
69
91
|
openNewProject() {
|
|
70
92
|
deps.openNewProjectModal();
|
|
71
93
|
},
|
|
94
|
+
openQuickSearch() {
|
|
95
|
+
deps.openQuickSearchPalette();
|
|
96
|
+
},
|
|
97
|
+
openSettings(section?: string) {
|
|
98
|
+
deps.openSettingsModal(section);
|
|
99
|
+
},
|
|
100
|
+
seedAssistant(options: { messages: SeededAssistantMessage[] }) {
|
|
101
|
+
deps.seedAssistantMessages(options.messages);
|
|
102
|
+
},
|
|
103
|
+
seedCollab(options: { peers: PeerPresence[] }) {
|
|
104
|
+
// Stage a live co-editing session on the active tab: the toolbar's presence chips and the
|
|
105
|
+
// Canvas peer-selection boxes both read this same reactive store.
|
|
106
|
+
const tab = activeTab.value;
|
|
107
|
+
if (!tab) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const state = collabState(tab);
|
|
111
|
+
state.status = "synced";
|
|
112
|
+
state.active = true;
|
|
113
|
+
state.peers = options.peers.map((peer) => ({
|
|
114
|
+
...peer,
|
|
115
|
+
state: {
|
|
116
|
+
...peer.state,
|
|
117
|
+
focusedPath: peer.state.focusedPath ?? tab.documentPath,
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
deps.render();
|
|
121
|
+
},
|
|
122
|
+
seedPublish(options: { accountId?: string; deployment: PagesDeploymentInfo }) {
|
|
123
|
+
deps.seedPublishConnected(options);
|
|
124
|
+
},
|
|
125
|
+
showWelcome(options?: { projects?: ProjectListEntry[] }) {
|
|
126
|
+
// The devserver platform auto-opens the repo-root project on boot; the welcome
|
|
127
|
+
// Screen only renders with no project and no tabs, so stage that state directly.
|
|
128
|
+
// A staged catalogue replaces the real one (screenshots must not leak local paths).
|
|
129
|
+
closeAllTabs();
|
|
130
|
+
setProjectState(null);
|
|
131
|
+
if (options?.projects) {
|
|
132
|
+
seedProjectList(options.projects);
|
|
133
|
+
}
|
|
134
|
+
view.chatPanelCollapsed = true;
|
|
135
|
+
applyPanelCollapse();
|
|
136
|
+
deps.render();
|
|
137
|
+
},
|
|
72
138
|
getState() {
|
|
73
139
|
return {
|
|
74
140
|
activeTabId: workspace.activeTabId,
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* CSS Variables editor — form-based editor for managing design tokens. Colors
|
|
4
|
-
*
|
|
3
|
+
* CSS Variables editor — form-based editor for managing design tokens. Colors are scheme-aware
|
|
4
|
+
* (per-scheme overrides in `@--dark`-style blocks, spec §9.5); sizes/spacing are media-aware; fonts
|
|
5
|
+
* are neither.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
9
10
|
import { projectState } from "../store";
|
|
10
11
|
import { getEffectiveMedia, updateSiteConfig } from "../site-context";
|
|
12
|
+
import { postSiteStyleToLiveHosts } from "../canvas/iframe-host";
|
|
13
|
+
import { isSchemeQuery, schemeOfQuery } from "../utils/canvas-media";
|
|
11
14
|
import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils";
|
|
12
15
|
|
|
13
16
|
import type { JxStyle } from "@jxsuite/schema/types";
|
|
14
17
|
|
|
18
|
+
/** A declared scheme query in $media: the block key name plus the scheme it targets. */
|
|
19
|
+
export interface SchemeEntry {
|
|
20
|
+
name: string;
|
|
21
|
+
scheme: "light" | "dark";
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/** @param {HTMLElement} container */
|
|
16
25
|
export function renderCssVarsEditor(container: HTMLElement) {
|
|
17
26
|
const config = projectState?.projectConfig || {};
|
|
@@ -50,10 +59,20 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
const mediaNames = media
|
|
62
|
+
const mediaNames = media
|
|
63
|
+
? Object.keys(media).filter((m) => m !== "--" && !isSchemeQuery(String(media[m])))
|
|
64
|
+
: [];
|
|
65
|
+
const schemeEntries: SchemeEntry[] = media
|
|
66
|
+
? Object.entries(media).flatMap(([name, query]) => {
|
|
67
|
+
const scheme = schemeOfQuery(String(query));
|
|
68
|
+
return scheme ? [{ name, scheme }] : [];
|
|
69
|
+
})
|
|
70
|
+
: [];
|
|
54
71
|
|
|
55
72
|
const save = () => {
|
|
56
73
|
void updateSiteConfig({ style: { ...rootStyle } });
|
|
74
|
+
// Live canvas feedback: replace the site-style sheet in place — no re-render needed.
|
|
75
|
+
postSiteStyleToLiveHosts();
|
|
57
76
|
};
|
|
58
77
|
|
|
59
78
|
const updateVar = (name: string, val: string) => {
|
|
@@ -61,6 +80,35 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
61
80
|
save();
|
|
62
81
|
};
|
|
63
82
|
|
|
83
|
+
// Write/clear a per-scheme token override in the scheme's `@--name` block (spec §9.5). An empty
|
|
84
|
+
// Value clears the override (and drops the block when it empties) — the token inherits the base.
|
|
85
|
+
const updateSchemeVar = (schemeName: string, varName: string, val: string) => {
|
|
86
|
+
const key = `@${schemeName}`;
|
|
87
|
+
if (val) {
|
|
88
|
+
const block = (rootStyle[key] ??= {}) as Record<string, unknown>;
|
|
89
|
+
block[varName] = val;
|
|
90
|
+
} else {
|
|
91
|
+
const block = rootStyle[key];
|
|
92
|
+
if (block && typeof block === "object") {
|
|
93
|
+
delete (block as Record<string, unknown>)[varName];
|
|
94
|
+
if (Object.keys(block).length === 0) {
|
|
95
|
+
delete rootStyle[key];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
save();
|
|
100
|
+
renderCssVarsEditor(container);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Without a declared scheme query nothing ever shows the scheme UI (here or in the tab bar) —
|
|
104
|
+
// This is the opt-in affordance for existing projects. Re-render after the async config update
|
|
105
|
+
// Lands (unlike style edits, $media is not mutated in place).
|
|
106
|
+
const enableDarkScheme = () => {
|
|
107
|
+
void updateSiteConfig({
|
|
108
|
+
$media: { ...config.$media, "--dark": "(prefers-color-scheme: dark)" },
|
|
109
|
+
}).then(() => renderCssVarsEditor(container));
|
|
110
|
+
};
|
|
111
|
+
|
|
64
112
|
const deleteVar = (name: string) => {
|
|
65
113
|
delete rootStyle[name];
|
|
66
114
|
save();
|
|
@@ -81,7 +129,16 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
81
129
|
<div class="settings-section">
|
|
82
130
|
<h3 class="settings-section-title">CSS Variables</h3>
|
|
83
131
|
|
|
84
|
-
${renderColorSection(
|
|
132
|
+
${renderColorSection(
|
|
133
|
+
groups.color,
|
|
134
|
+
updateVar,
|
|
135
|
+
deleteVar,
|
|
136
|
+
addVar,
|
|
137
|
+
rootStyle,
|
|
138
|
+
schemeEntries,
|
|
139
|
+
updateSchemeVar,
|
|
140
|
+
enableDarkScheme,
|
|
141
|
+
)}
|
|
85
142
|
${renderFontSection(groups.font, updateVar, deleteVar, addVar)}
|
|
86
143
|
${renderSizeSection(groups.size, updateVar, deleteVar, addVar, rootStyle, mediaNames)}
|
|
87
144
|
${groups.other.length > 0
|
|
@@ -104,6 +161,10 @@ function renderColorSection(
|
|
|
104
161
|
updateVar: (name: string, val: string) => void,
|
|
105
162
|
deleteVar: (name: string) => void,
|
|
106
163
|
addVar: (prefix: string, friendlyName: string, val: string) => void,
|
|
164
|
+
rootStyle: JxStyle,
|
|
165
|
+
schemeEntries: SchemeEntry[],
|
|
166
|
+
updateSchemeVar: (schemeName: string, varName: string, val: string) => void,
|
|
167
|
+
enableDarkScheme: () => void,
|
|
107
168
|
) {
|
|
108
169
|
return html`
|
|
109
170
|
<div class="css-vars-group">
|
|
@@ -129,9 +190,66 @@ function renderColorSection(
|
|
|
129
190
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
130
191
|
</sp-action-button>
|
|
131
192
|
</div>
|
|
193
|
+
${schemeEntries.map((entry) =>
|
|
194
|
+
renderSchemeOverride(name, rootStyle, entry, updateSchemeVar),
|
|
195
|
+
)}
|
|
132
196
|
`,
|
|
133
197
|
)}
|
|
134
198
|
${renderAddRow("--color-", "Primary Blue", "#3b82f6", addVar)}
|
|
199
|
+
${schemeEntries.length === 0
|
|
200
|
+
? html`
|
|
201
|
+
<sp-action-button
|
|
202
|
+
size="s"
|
|
203
|
+
class="css-vars-enable-dark"
|
|
204
|
+
title="Add a dark color scheme: declares --dark in $media so every color token can carry a dark value"
|
|
205
|
+
@click=${enableDarkScheme}
|
|
206
|
+
>
|
|
207
|
+
Enable dark scheme
|
|
208
|
+
</sp-action-button>
|
|
209
|
+
`
|
|
210
|
+
: nothing}
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One per-scheme override row under a color token: swatch + field bound to the scheme block's
|
|
217
|
+
* value. Empty shows "inherits" (the base value applies); clearing deletes the override.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} varName
|
|
220
|
+
* @param {JxStyle} rootStyle
|
|
221
|
+
* @param {SchemeEntry} entry
|
|
222
|
+
* @param {(schemeName: string, varName: string, val: string) => void} updateSchemeVar
|
|
223
|
+
*/
|
|
224
|
+
function renderSchemeOverride(
|
|
225
|
+
varName: string,
|
|
226
|
+
rootStyle: JxStyle,
|
|
227
|
+
entry: SchemeEntry,
|
|
228
|
+
updateSchemeVar: (schemeName: string, varName: string, val: string) => void,
|
|
229
|
+
) {
|
|
230
|
+
const block = rootStyle[`@${entry.name}`];
|
|
231
|
+
const current =
|
|
232
|
+
block && typeof block === "object" ? (block as Record<string, unknown>)[varName] : undefined;
|
|
233
|
+
const label = entry.scheme === "dark" ? "Dark" : "Light";
|
|
234
|
+
return html`
|
|
235
|
+
<div class="css-var-media-row css-var-scheme-row">
|
|
236
|
+
<span class="css-var-media-label">${label}</span>
|
|
237
|
+
<div class="css-var-swatch" style="background:${current ?? "transparent"}">
|
|
238
|
+
<input
|
|
239
|
+
type="color"
|
|
240
|
+
.value=${current && String(current).startsWith("#") ? String(current) : "#111111"}
|
|
241
|
+
@input=${(e: Event) =>
|
|
242
|
+
updateSchemeVar(entry.name, varName, (e.target as HTMLInputElement).value)}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<sp-textfield
|
|
246
|
+
size="s"
|
|
247
|
+
placeholder="inherits"
|
|
248
|
+
.value=${current == null ? "" : String(current)}
|
|
249
|
+
@change=${(e: Event) =>
|
|
250
|
+
updateSchemeVar(entry.name, varName, (e.target as HTMLInputElement).value)}
|
|
251
|
+
style="flex:1;max-width:160px"
|
|
252
|
+
></sp-textfield>
|
|
135
253
|
</div>
|
|
136
254
|
`;
|
|
137
255
|
}
|
|
@@ -106,11 +106,11 @@ let _activeSection = "general";
|
|
|
106
106
|
|
|
107
107
|
let _contentEl: HTMLElement | null = null;
|
|
108
108
|
|
|
109
|
-
export function openSettingsModal() {
|
|
109
|
+
export function openSettingsModal(section?: string) {
|
|
110
110
|
if (_handle) {
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
|
-
_activeSection = "general";
|
|
113
|
+
_activeSection = section ?? "general";
|
|
114
114
|
renderModal();
|
|
115
115
|
// Refresh descriptor-contributed sections (cached payloads make this cheap) and rerender the
|
|
116
116
|
// Nav once they land. Lazy import breaks the settings-modal ↔ extension-sections module cycle.
|
package/src/studio.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { consumePatchedDocument, initCanvasPatcher } from "./canvas/canvas-patch
|
|
|
48
48
|
import {
|
|
49
49
|
commitActiveEditSession,
|
|
50
50
|
getEditSnapshot,
|
|
51
|
+
postColorSchemeToLiveHosts,
|
|
51
52
|
setCanvasContextMenuHandler,
|
|
52
53
|
setCanvasSlashHandler,
|
|
53
54
|
setIframePatchEscalation,
|
|
@@ -95,6 +96,9 @@ import { renderHeadTemplate } from "./panels/head-panel";
|
|
|
95
96
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
|
|
96
97
|
import { installAutomationHook } from "./services/automation";
|
|
97
98
|
import { openBrowseModal } from "./browse/browse-modal";
|
|
99
|
+
import { seedAssistantMessages } from "./panels/ai-panel";
|
|
100
|
+
import { seedPublishConnected } from "./publish/publish-panel";
|
|
101
|
+
import { openConnectorGrid } from "./grid/grid-open";
|
|
98
102
|
|
|
99
103
|
import { getPlatform, hasPlatform, registerPlatform } from "./platform";
|
|
100
104
|
import { parseMediaEntries } from "./utils/canvas-media";
|
|
@@ -144,7 +148,8 @@ import {
|
|
|
144
148
|
renderBlockActionBar,
|
|
145
149
|
} from "./panels/block-action-bar";
|
|
146
150
|
import { initCssData } from "./panels/style-utils";
|
|
147
|
-
import { initQuickSearch } from "./panels/quick-search";
|
|
151
|
+
import { initQuickSearch, openQuickSearch } from "./panels/quick-search";
|
|
152
|
+
import { openSettingsModal } from "./settings/settings-modal";
|
|
148
153
|
import { hydrateAccountStatus } from "./account-status";
|
|
149
154
|
import { hydrateProjectList } from "./project-list";
|
|
150
155
|
import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
|
|
@@ -390,9 +395,14 @@ if (!hasPlatform()) {
|
|
|
390
395
|
installAutomationHook({
|
|
391
396
|
getCanvasMode,
|
|
392
397
|
openBrowseModal,
|
|
398
|
+
openConnectorGrid,
|
|
393
399
|
openNewProjectModal,
|
|
400
|
+
openQuickSearchPalette: openQuickSearch,
|
|
401
|
+
openSettingsModal,
|
|
394
402
|
render,
|
|
395
403
|
renderActivityBar,
|
|
404
|
+
seedAssistantMessages,
|
|
405
|
+
seedPublishConnected,
|
|
396
406
|
setCanvasMode,
|
|
397
407
|
statusMessage,
|
|
398
408
|
});
|
|
@@ -581,6 +591,12 @@ effect(() => {
|
|
|
581
591
|
}
|
|
582
592
|
scheduleCanvasRender();
|
|
583
593
|
});
|
|
594
|
+
// Color-scheme preview is a document-level attribute flip inside the iframe — deliberately its
|
|
595
|
+
// Own effect, never part of the ui-effect above: flipping the scheme must not re-render.
|
|
596
|
+
effect(() => {
|
|
597
|
+
const s = activeTab.value?.session.ui.previewColorScheme;
|
|
598
|
+
postColorSchemeToLiveHosts(s === "light" || s === "dark" ? s : null);
|
|
599
|
+
});
|
|
584
600
|
|
|
585
601
|
rightPanelMod.mount({
|
|
586
602
|
getCanvasMode,
|
package/src/tabs/tab.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface TabUi {
|
|
|
43
43
|
/** Full-screen formula workspace target ($expression editing); editingFunction wins if both set. */
|
|
44
44
|
editingFormula: FormulaEditDef | null;
|
|
45
45
|
featureToggles: Record<string, boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Canvas color-scheme preview: follow the OS ("auto") or force a scheme. Drives the
|
|
48
|
+
* data-color-scheme attribute on the canvas iframe root (spec §9.5) and, in the style sidebar,
|
|
49
|
+
* which scheme layer edits target.
|
|
50
|
+
*/
|
|
51
|
+
previewColorScheme: "auto" | "light" | "dark";
|
|
46
52
|
styleSections: Record<string, boolean>;
|
|
47
53
|
inspectorSections: Record<string, boolean>;
|
|
48
54
|
styleShorthands: Record<string, boolean>;
|
|
@@ -156,6 +162,7 @@ function createDefaultUi(canvasMode: string, preview = false) {
|
|
|
156
162
|
inspectorSections: {},
|
|
157
163
|
pendingInlineEdit: null,
|
|
158
164
|
preview,
|
|
165
|
+
previewColorScheme: "auto" as const,
|
|
159
166
|
previewParams: {},
|
|
160
167
|
previewProps: null,
|
|
161
168
|
rightTab: "properties",
|
|
@@ -293,7 +293,10 @@ const OPERATOR_META: Record<string, OperatorMeta> = {
|
|
|
293
293
|
export function operatorEntries(): FormulaCatalogEntry[] {
|
|
294
294
|
const out: FormulaCatalogEntry[] = [];
|
|
295
295
|
for (const op of BLESSED_OPERATORS) {
|
|
296
|
-
|
|
296
|
+
// Own-property lookup only: a bare index would resolve inherited Object.prototype members for
|
|
297
|
+
// Ops like "toLocaleString", yielding a bogus meta whose undefined group/description breaks
|
|
298
|
+
// The palette's substring filter.
|
|
299
|
+
const meta = Object.hasOwn(OPERATOR_META, op) ? OPERATOR_META[op] : undefined;
|
|
297
300
|
if (meta) {
|
|
298
301
|
out.push({
|
|
299
302
|
description: meta.description,
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
/// <reference lib="dom.iterable" />
|
|
3
3
|
/** Canvas media/breakpoint utilities — pure functions extracted for testability. */
|
|
4
4
|
|
|
5
|
+
import { pureSchemeOf } from "@jxsuite/runtime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* True when a feature query participates in the forced-scheme contract (spec §9.5) — a pure
|
|
9
|
+
* prefers-color-scheme query. These surface as the Auto/Light/Dark control, not generic toggles.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} query
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function isSchemeQuery(query: string): boolean {
|
|
15
|
+
return pureSchemeOf(query) !== null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The scheme a feature query targets ("light" | "dark"), or null for non-scheme queries.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} query
|
|
22
|
+
* @returns {"light" | "dark" | null}
|
|
23
|
+
*/
|
|
24
|
+
export function schemeOfQuery(query: string): "light" | "dark" | null {
|
|
25
|
+
return pureSchemeOf(query);
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
/**
|
|
6
29
|
* Classify $media entries into size breakpoints (get a canvas each) and feature queries (rendered
|
|
7
30
|
* as toolbar toggles).
|