pmx-canvas 0.4.1 → 0.4.3
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/.github/extensions/pmx-canvas/extension.mjs +5 -2
- package/CHANGELOG.md +61 -0
- package/Readme.md +34 -12
- package/dist/canvas/global.css +406 -0
- package/dist/canvas/index.js +84 -84
- package/dist/canvas/surface-theme.css +160 -0
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/theme-override.d.ts +27 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/server/canvas-db.d.ts +2 -1
- package/dist/types/server/html-surface.d.ts +8 -1
- package/dist/types/shared/themes.d.ts +27 -0
- package/docs/cli.md +1 -1
- package/docs/environment.md +2 -1
- package/docs/http-api.md +15 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -3
- package/docs/screenshot.png +0 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +19 -3
- package/skills/pmx-canvas/references/full-reference.md +3 -3
- package/src/cli/index.ts +3 -2
- package/src/client/App.tsx +208 -53
- package/src/client/canvas/CommandPalette.tsx +4 -0
- package/src/client/nodes/ExtAppFrame.tsx +18 -5
- package/src/client/nodes/McpAppNode.tsx +10 -2
- package/src/client/state/iframe-mode.ts +16 -1
- package/src/client/state/sse-bridge.ts +13 -4
- package/src/client/state/theme-override.ts +83 -0
- package/src/client/theme/global.css +406 -0
- package/src/client/theme/surface-theme.css +160 -0
- package/src/client/types.ts +3 -1
- package/src/server/canvas-db.ts +3 -2
- package/src/server/html-surface.ts +17 -3
- package/src/server/operations/ops/canvas-wire.ts +15 -2
- package/src/server/server.ts +35 -1
- package/src/shared/themes.ts +58 -0
|
@@ -69,6 +69,17 @@ function isEmbeddedDocument(): boolean {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Amp orb services run with AMP_ORB=1 and the server stamps that into the page
|
|
74
|
+
* as window.__PMX_AMP_ORB (see canvasSpaHtml). The orb portal's nested-iframe
|
|
75
|
+
* embed blocks src-URL child iframes, but the probe is unreliable there — a
|
|
76
|
+
* tiny probe iframe sometimes loads even though node-sized ones will not — so
|
|
77
|
+
* a known orb host must skip the probe and go straight to srcdoc.
|
|
78
|
+
*/
|
|
79
|
+
function isAmpOrbHost(): boolean {
|
|
80
|
+
return (window as Window & { __PMX_AMP_ORB?: unknown }).__PMX_AMP_ORB === true;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
/**
|
|
73
84
|
* Resolve the boot-wide iframe mode once; all surface hooks share the result.
|
|
74
85
|
* The blocked-src condition only exists when the canvas page itself runs
|
|
@@ -76,7 +87,7 @@ function isEmbeddedDocument(): boolean {
|
|
|
76
87
|
* normal path pays zero probe latency before surfaces mount. Only embedded
|
|
77
88
|
* documents run the probe (`embedded` is overridable for tests).
|
|
78
89
|
*/
|
|
79
|
-
export function resolveIframeMode(opts: { embedded?: boolean } = {}): Promise<IframeMode> {
|
|
90
|
+
export function resolveIframeMode(opts: { embedded?: boolean; ampOrb?: boolean } = {}): Promise<IframeMode> {
|
|
80
91
|
if (iframeMode.value) return Promise.resolve(iframeMode.value);
|
|
81
92
|
if (pending) return pending;
|
|
82
93
|
const forced = forcedIframeMode();
|
|
@@ -88,6 +99,10 @@ export function resolveIframeMode(opts: { embedded?: boolean } = {}): Promise<If
|
|
|
88
99
|
iframeMode.value = 'src';
|
|
89
100
|
return Promise.resolve('src');
|
|
90
101
|
}
|
|
102
|
+
if (opts.ampOrb ?? isAmpOrbHost()) {
|
|
103
|
+
iframeMode.value = 'srcdoc';
|
|
104
|
+
return Promise.resolve('srcdoc');
|
|
105
|
+
}
|
|
91
106
|
pending = probeSrcIframes().then((srcWorks) => {
|
|
92
107
|
const mode: IframeMode = srcWorks ? 'src' : 'srcdoc';
|
|
93
108
|
iframeMode.value = mode;
|
|
@@ -26,11 +26,13 @@ import {
|
|
|
26
26
|
workbenchConnectionEpoch,
|
|
27
27
|
} from './canvas-store';
|
|
28
28
|
import { fetchAxSurfaceState } from './intent-bridge';
|
|
29
|
+
import { initSessionThemeOverride, themeOverrideActive } from './theme-override';
|
|
29
30
|
import { DEFAULT_POSITIONS, makeNodeState } from './node-factory';
|
|
30
31
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
31
32
|
import { resetAttentionBridge, syncAttentionFromSse } from './attention-bridge';
|
|
32
33
|
import { dissolveIntent, resetIntents, settleIntent, upsertIntent } from './intent-store';
|
|
33
34
|
import type { PmxAxIntent } from '../../shared/ax-intent.js';
|
|
35
|
+
import { isCanvasTheme } from '../../shared/themes.js';
|
|
34
36
|
|
|
35
37
|
let eventSource: EventSource | null = null;
|
|
36
38
|
let savedLayout: Map<string, Partial<CanvasNodeState>> | null = null;
|
|
@@ -275,8 +277,10 @@ function ensureLedgerNode(summary: Record<string, unknown>): void {
|
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
function applyCanvasTheme(theme: string): void {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
// Registry-driven: this is the ONLY path a server-side theme reaches the
|
|
281
|
+
// browser (connected snapshot + theme-changed events) — a hardcoded subset
|
|
282
|
+
// here silently drops themes on reload and breaks cross-tab sync.
|
|
283
|
+
if (!isCanvasTheme(theme)) return;
|
|
280
284
|
document.documentElement.setAttribute('data-theme', theme);
|
|
281
285
|
invalidateTokenCache();
|
|
282
286
|
if (canvasTheme.value !== theme) canvasTheme.value = theme;
|
|
@@ -396,7 +400,9 @@ function handleConnected(data: Record<string, unknown>): void {
|
|
|
396
400
|
connectionStatus.value = 'connected';
|
|
397
401
|
// Reconnect marker for holders of server-minted URLs (Finding S).
|
|
398
402
|
workbenchConnectionEpoch.value += 1;
|
|
399
|
-
|
|
403
|
+
// A ?theme= session override (host-default theming) wins over the
|
|
404
|
+
// server-global theme for THIS client only.
|
|
405
|
+
if (typeof data.theme === 'string' && !themeOverrideActive()) {
|
|
400
406
|
applyCanvasTheme(data.theme);
|
|
401
407
|
}
|
|
402
408
|
if (data.ledgerSummary) {
|
|
@@ -887,7 +893,7 @@ function handleTraceState(data: Record<string, unknown>): void {
|
|
|
887
893
|
}
|
|
888
894
|
|
|
889
895
|
function handleThemeChanged(data: Record<string, unknown>): void {
|
|
890
|
-
if (typeof data.theme === 'string') {
|
|
896
|
+
if (typeof data.theme === 'string' && !themeOverrideActive()) {
|
|
891
897
|
applyCanvasTheme(data.theme);
|
|
892
898
|
}
|
|
893
899
|
}
|
|
@@ -1079,6 +1085,9 @@ function startPollingTransport(): () => void {
|
|
|
1079
1085
|
}
|
|
1080
1086
|
|
|
1081
1087
|
export function connectSSE(): () => void {
|
|
1088
|
+
// Host-default theming: a ?theme= param applies before the first server
|
|
1089
|
+
// frame so an embedding host panel never flashes the server-global theme.
|
|
1090
|
+
initSessionThemeOverride(applyCanvasTheme);
|
|
1082
1091
|
savedLayout = restoreLayout();
|
|
1083
1092
|
ensureStatusNode();
|
|
1084
1093
|
hasInitialServerLayout.value = false;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session theme override — host-default theming.
|
|
3
|
+
*
|
|
4
|
+
* Hosts that embed the workbench (the GitHub Copilot panel, the Claude Code
|
|
5
|
+
* desktop browser pane, ChatGPT-style app browsers, …) can open it with
|
|
6
|
+
* `?theme=<name>` or `?theme=auto` to give THEIR panel a fitting default
|
|
7
|
+
* without touching the server-global theme every other client sees. The
|
|
8
|
+
* override is client-local: server theme frames are ignored while it is
|
|
9
|
+
* active, and it ends the moment the user explicitly picks a theme from the
|
|
10
|
+
* picker (which then applies + saves globally as usual).
|
|
11
|
+
*
|
|
12
|
+
* `theme=auto` follows the host's prefers-color-scheme live, mapping to the
|
|
13
|
+
* built-in light/dark themes — so a light-mode host panel gets a light canvas
|
|
14
|
+
* by default and flips with the host appearance.
|
|
15
|
+
*/
|
|
16
|
+
import { type CanvasThemeName, isCanvasTheme } from '../../shared/themes.js';
|
|
17
|
+
|
|
18
|
+
let active = false;
|
|
19
|
+
// Latched when the user explicitly ends the override: the URL param survives
|
|
20
|
+
// reconnects (connectSSE re-inits on every transport drop), but the user's
|
|
21
|
+
// choice must stick for the page lifetime — only a reload re-activates.
|
|
22
|
+
let userEnded = false;
|
|
23
|
+
let schemeQuery: MediaQueryList | null = null;
|
|
24
|
+
let schemeListener: (() => void) | null = null;
|
|
25
|
+
|
|
26
|
+
export function themeOverrideActive(): boolean {
|
|
27
|
+
return active;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Test-only: reset the module state, including the page-lifetime latch. */
|
|
31
|
+
export function resetThemeOverrideForTests(): void {
|
|
32
|
+
active = false;
|
|
33
|
+
userEnded = false;
|
|
34
|
+
detachSchemeListener();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The user picked a theme explicitly — the session override ends for good. */
|
|
38
|
+
export function clearThemeOverride(): void {
|
|
39
|
+
active = false;
|
|
40
|
+
userEnded = true;
|
|
41
|
+
detachSchemeListener();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detachSchemeListener(): void {
|
|
45
|
+
if (schemeQuery && schemeListener) {
|
|
46
|
+
schemeQuery.removeEventListener('change', schemeListener);
|
|
47
|
+
}
|
|
48
|
+
schemeQuery = null;
|
|
49
|
+
schemeListener = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sessionThemeParam(): CanvasThemeName | 'auto' | null {
|
|
53
|
+
// DOM-less callers (server-side rendering of the module graph, unit tests
|
|
54
|
+
// that import the sse-bridge without a DOM) have no override.
|
|
55
|
+
if (typeof window === 'undefined' || !window.location) return null;
|
|
56
|
+
const value = new URLSearchParams(window.location.search).get('theme');
|
|
57
|
+
if (value === 'auto') return 'auto';
|
|
58
|
+
return isCanvasTheme(value) ? value : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Activate the override from the URL (no-op without a valid `?theme=`).
|
|
63
|
+
* `apply` is the canvas theme applier (client-side only — never saved).
|
|
64
|
+
*/
|
|
65
|
+
export function initSessionThemeOverride(apply: (theme: CanvasThemeName) => void): void {
|
|
66
|
+
if (userEnded) return;
|
|
67
|
+
const param = sessionThemeParam();
|
|
68
|
+
if (!param) return;
|
|
69
|
+
// Re-init happens on every reconnect — replace any prior listener instead
|
|
70
|
+
// of accumulating one per reconnect cycle.
|
|
71
|
+
detachSchemeListener();
|
|
72
|
+
active = true;
|
|
73
|
+
if (param !== 'auto') {
|
|
74
|
+
apply(param);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
schemeQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
78
|
+
schemeListener = () => {
|
|
79
|
+
if (active && schemeQuery) apply(schemeQuery.matches ? 'light' : 'dark');
|
|
80
|
+
};
|
|
81
|
+
schemeListener();
|
|
82
|
+
schemeQuery.addEventListener('change', schemeListener);
|
|
83
|
+
}
|
|
@@ -171,6 +171,281 @@
|
|
|
171
171
|
--c-annotation: #ffff00;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
:root[data-theme="midnight"] {
|
|
175
|
+
/* ── Core palette (midnight · deep indigo night) ─────────── */
|
|
176
|
+
--c-bg: #0A0D1C;
|
|
177
|
+
--c-panel: #12172E;
|
|
178
|
+
--c-panel-soft: #0D1224;
|
|
179
|
+
--c-line: #232B4A;
|
|
180
|
+
--c-text: #E6E9F7;
|
|
181
|
+
--c-text-soft: #C6CCE8;
|
|
182
|
+
--c-muted: #8D96BC;
|
|
183
|
+
--c-dim: #5B6383;
|
|
184
|
+
--c-accent: #8B96FF;
|
|
185
|
+
--c-ok: #34D399;
|
|
186
|
+
--c-warn: #F5C452;
|
|
187
|
+
--c-warn-alt: #FFB300;
|
|
188
|
+
--c-danger: #FF6B8A;
|
|
189
|
+
--c-purple: #B388FF;
|
|
190
|
+
--c-thinking: #B388FF;
|
|
191
|
+
--c-subagent: #22D3EE;
|
|
192
|
+
--c-input-bg: #0D1224;
|
|
193
|
+
--c-dot-grid: rgba(139, 150, 255, 0.08);
|
|
194
|
+
--c-panel-overlay: rgba(10, 13, 28, 0.6);
|
|
195
|
+
--c-panel-glass: rgba(18, 23, 46, 0.92);
|
|
196
|
+
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
197
|
+
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
198
|
+
--c-glow-accent: rgba(139, 150, 255, 0.15);
|
|
199
|
+
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
200
|
+
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
201
|
+
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
202
|
+
--c-accent-8: rgba(139, 150, 255, 0.08);
|
|
203
|
+
--c-accent-10: rgba(139, 150, 255, 0.1);
|
|
204
|
+
--c-accent-12: rgba(139, 150, 255, 0.12);
|
|
205
|
+
--c-accent-15: rgba(139, 150, 255, 0.15);
|
|
206
|
+
--c-accent-25: rgba(139, 150, 255, 0.25);
|
|
207
|
+
--c-accent-30: rgba(139, 150, 255, 0.3);
|
|
208
|
+
--c-accent-40: rgba(139, 150, 255, 0.4);
|
|
209
|
+
--c-ok-10: rgba(52, 211, 153, 0.1);
|
|
210
|
+
--c-ok-12: rgba(52, 211, 153, 0.12);
|
|
211
|
+
--c-ok-20: rgba(52, 211, 153, 0.2);
|
|
212
|
+
--c-ok-25: rgba(52, 211, 153, 0.25);
|
|
213
|
+
--c-warn-10: rgba(245, 196, 82, 0.1);
|
|
214
|
+
--c-warn-12: rgba(245, 196, 82, 0.12);
|
|
215
|
+
--c-warn-15: rgba(245, 196, 82, 0.15);
|
|
216
|
+
--c-warn-20: rgba(245, 196, 82, 0.2);
|
|
217
|
+
--c-warn-22: rgba(245, 196, 82, 0.22);
|
|
218
|
+
--c-warn-30: rgba(245, 196, 82, 0.3);
|
|
219
|
+
--c-warn-60: rgba(245, 196, 82, 0.6);
|
|
220
|
+
--c-danger-12: rgba(255, 107, 138, 0.12);
|
|
221
|
+
--c-line-30: rgba(35, 43, 74, 0.3);
|
|
222
|
+
--c-contrast-fg: #0A0D1C;
|
|
223
|
+
--c-accent-hover: #A3ACFF;
|
|
224
|
+
--c-warn-hover: #F7D06E;
|
|
225
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
226
|
+
--c-annotation: #EDEDF7;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
:root[data-theme="sepia"] {
|
|
230
|
+
/* ── Core palette (sepia · warm amber paper) ─────────────── */
|
|
231
|
+
--c-bg: #F2E7D5;
|
|
232
|
+
--c-panel: #EBDCC2;
|
|
233
|
+
--c-panel-soft: #E8D8BC;
|
|
234
|
+
--c-line: #D3C1A0;
|
|
235
|
+
--c-text: #2E2416;
|
|
236
|
+
--c-text-soft: #554833;
|
|
237
|
+
--c-muted: #77664C;
|
|
238
|
+
--c-dim: #978870;
|
|
239
|
+
--c-accent: #B4632C;
|
|
240
|
+
--c-ok: #3E7D4F;
|
|
241
|
+
--c-warn: #A97B1E;
|
|
242
|
+
--c-warn-alt: #8F6A0D;
|
|
243
|
+
--c-danger: #B3402E;
|
|
244
|
+
--c-purple: #8456B0;
|
|
245
|
+
--c-thinking: #8456B0;
|
|
246
|
+
--c-subagent: #0E7C7B;
|
|
247
|
+
--c-input-bg: #F7EEDD;
|
|
248
|
+
--c-dot-grid: rgba(46, 36, 22, 0.08);
|
|
249
|
+
--c-panel-overlay: rgba(242, 231, 213, 0.88);
|
|
250
|
+
--c-panel-glass: rgba(235, 220, 194, 0.88);
|
|
251
|
+
--c-shadow: rgba(46, 36, 22, 0.08);
|
|
252
|
+
--c-shadow-heavy: rgba(46, 36, 22, 0.16);
|
|
253
|
+
--c-glow-accent: rgba(180, 99, 44, 0.12);
|
|
254
|
+
--c-surface-hover: rgba(46, 36, 22, 0.06);
|
|
255
|
+
--c-surface-subtle: rgba(46, 36, 22, 0.03);
|
|
256
|
+
--c-surface-th: rgba(46, 36, 22, 0.04);
|
|
257
|
+
--c-accent-8: rgba(180, 99, 44, 0.08);
|
|
258
|
+
--c-accent-10: rgba(180, 99, 44, 0.1);
|
|
259
|
+
--c-accent-12: rgba(180, 99, 44, 0.12);
|
|
260
|
+
--c-accent-15: rgba(180, 99, 44, 0.15);
|
|
261
|
+
--c-accent-25: rgba(180, 99, 44, 0.25);
|
|
262
|
+
--c-accent-30: rgba(180, 99, 44, 0.3);
|
|
263
|
+
--c-accent-40: rgba(180, 99, 44, 0.4);
|
|
264
|
+
--c-ok-10: rgba(62, 125, 79, 0.1);
|
|
265
|
+
--c-ok-12: rgba(62, 125, 79, 0.12);
|
|
266
|
+
--c-ok-20: rgba(62, 125, 79, 0.2);
|
|
267
|
+
--c-ok-25: rgba(62, 125, 79, 0.25);
|
|
268
|
+
--c-warn-10: rgba(169, 123, 30, 0.1);
|
|
269
|
+
--c-warn-12: rgba(169, 123, 30, 0.12);
|
|
270
|
+
--c-warn-15: rgba(169, 123, 30, 0.15);
|
|
271
|
+
--c-warn-20: rgba(169, 123, 30, 0.2);
|
|
272
|
+
--c-warn-22: rgba(169, 123, 30, 0.22);
|
|
273
|
+
--c-warn-30: rgba(169, 123, 30, 0.3);
|
|
274
|
+
--c-warn-60: rgba(169, 123, 30, 0.6);
|
|
275
|
+
--c-danger-12: rgba(179, 64, 46, 0.12);
|
|
276
|
+
--c-line-30: rgba(211, 193, 160, 0.5);
|
|
277
|
+
--c-contrast-fg: #F2E7D5;
|
|
278
|
+
--c-accent-hover: #C4763F;
|
|
279
|
+
--c-warn-hover: #BD9037;
|
|
280
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(46, 36, 22, 0.02));
|
|
281
|
+
--c-annotation: #2E2416;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
:root[data-theme="arctic"] {
|
|
285
|
+
/* ── Core palette (arctic · cool slate + ice) ────────────── */
|
|
286
|
+
--c-bg: #20242E;
|
|
287
|
+
--c-panel: #2A2F3C;
|
|
288
|
+
--c-panel-soft: #242936;
|
|
289
|
+
--c-line: #3A4152;
|
|
290
|
+
--c-text: #E9EDF4;
|
|
291
|
+
--c-text-soft: #CBD3E1;
|
|
292
|
+
--c-muted: #939DB1;
|
|
293
|
+
--c-dim: #667082;
|
|
294
|
+
--c-accent: #7FC8DE;
|
|
295
|
+
--c-ok: #74C596;
|
|
296
|
+
--c-warn: #E8C476;
|
|
297
|
+
--c-warn-alt: #E0A93F;
|
|
298
|
+
--c-danger: #D9707C;
|
|
299
|
+
--c-purple: #B48EAD;
|
|
300
|
+
--c-thinking: #B48EAD;
|
|
301
|
+
--c-subagent: #6FD6C4;
|
|
302
|
+
--c-input-bg: #242936;
|
|
303
|
+
--c-dot-grid: rgba(127, 200, 222, 0.08);
|
|
304
|
+
--c-panel-overlay: rgba(32, 36, 46, 0.6);
|
|
305
|
+
--c-panel-glass: rgba(42, 47, 60, 0.92);
|
|
306
|
+
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
307
|
+
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
308
|
+
--c-glow-accent: rgba(127, 200, 222, 0.15);
|
|
309
|
+
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
310
|
+
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
311
|
+
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
312
|
+
--c-accent-8: rgba(127, 200, 222, 0.08);
|
|
313
|
+
--c-accent-10: rgba(127, 200, 222, 0.1);
|
|
314
|
+
--c-accent-12: rgba(127, 200, 222, 0.12);
|
|
315
|
+
--c-accent-15: rgba(127, 200, 222, 0.15);
|
|
316
|
+
--c-accent-25: rgba(127, 200, 222, 0.25);
|
|
317
|
+
--c-accent-30: rgba(127, 200, 222, 0.3);
|
|
318
|
+
--c-accent-40: rgba(127, 200, 222, 0.4);
|
|
319
|
+
--c-ok-10: rgba(116, 197, 150, 0.1);
|
|
320
|
+
--c-ok-12: rgba(116, 197, 150, 0.12);
|
|
321
|
+
--c-ok-20: rgba(116, 197, 150, 0.2);
|
|
322
|
+
--c-ok-25: rgba(116, 197, 150, 0.25);
|
|
323
|
+
--c-warn-10: rgba(232, 196, 118, 0.1);
|
|
324
|
+
--c-warn-12: rgba(232, 196, 118, 0.12);
|
|
325
|
+
--c-warn-15: rgba(232, 196, 118, 0.15);
|
|
326
|
+
--c-warn-20: rgba(232, 196, 118, 0.2);
|
|
327
|
+
--c-warn-22: rgba(232, 196, 118, 0.22);
|
|
328
|
+
--c-warn-30: rgba(232, 196, 118, 0.3);
|
|
329
|
+
--c-warn-60: rgba(232, 196, 118, 0.6);
|
|
330
|
+
--c-danger-12: rgba(217, 112, 124, 0.12);
|
|
331
|
+
--c-line-30: rgba(58, 65, 82, 0.3);
|
|
332
|
+
--c-contrast-fg: #20242E;
|
|
333
|
+
--c-accent-hover: #9AD5E7;
|
|
334
|
+
--c-warn-hover: #EDD194;
|
|
335
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
336
|
+
--c-annotation: #ECEFF4;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
:root[data-theme="ember"] {
|
|
340
|
+
/* ── Core palette (ember · neutral charcoal + vivid orange-red) ─── */
|
|
341
|
+
--c-bg: #0B0B0C;
|
|
342
|
+
--c-panel: #161618;
|
|
343
|
+
--c-panel-soft: #101012;
|
|
344
|
+
--c-line: #29292C;
|
|
345
|
+
--c-text: #EDEBE6;
|
|
346
|
+
--c-text-soft: #CBC9C2;
|
|
347
|
+
--c-muted: #96948D;
|
|
348
|
+
--c-dim: #66655F;
|
|
349
|
+
--c-accent: #F34E1C;
|
|
350
|
+
--c-ok: #4CC38A;
|
|
351
|
+
--c-warn: #F5B944;
|
|
352
|
+
--c-warn-alt: #FFB300;
|
|
353
|
+
--c-danger: #F0566A;
|
|
354
|
+
--c-purple: #A78BFA;
|
|
355
|
+
--c-thinking: #A78BFA;
|
|
356
|
+
--c-subagent: #38BDF8;
|
|
357
|
+
--c-input-bg: #101012;
|
|
358
|
+
--c-dot-grid: rgba(243, 78, 28, 0.08);
|
|
359
|
+
--c-panel-overlay: rgba(11, 11, 12, 0.6);
|
|
360
|
+
--c-panel-glass: rgba(22, 22, 24, 0.92);
|
|
361
|
+
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
362
|
+
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
363
|
+
--c-glow-accent: rgba(243, 78, 28, 0.15);
|
|
364
|
+
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
365
|
+
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
366
|
+
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
367
|
+
--c-accent-8: rgba(243, 78, 28, 0.08);
|
|
368
|
+
--c-accent-10: rgba(243, 78, 28, 0.1);
|
|
369
|
+
--c-accent-12: rgba(243, 78, 28, 0.12);
|
|
370
|
+
--c-accent-15: rgba(243, 78, 28, 0.15);
|
|
371
|
+
--c-accent-25: rgba(243, 78, 28, 0.25);
|
|
372
|
+
--c-accent-30: rgba(243, 78, 28, 0.3);
|
|
373
|
+
--c-accent-40: rgba(243, 78, 28, 0.4);
|
|
374
|
+
--c-ok-10: rgba(76, 195, 138, 0.1);
|
|
375
|
+
--c-ok-12: rgba(76, 195, 138, 0.12);
|
|
376
|
+
--c-ok-20: rgba(76, 195, 138, 0.2);
|
|
377
|
+
--c-ok-25: rgba(76, 195, 138, 0.25);
|
|
378
|
+
--c-warn-10: rgba(245, 185, 68, 0.1);
|
|
379
|
+
--c-warn-12: rgba(245, 185, 68, 0.12);
|
|
380
|
+
--c-warn-15: rgba(245, 185, 68, 0.15);
|
|
381
|
+
--c-warn-20: rgba(245, 185, 68, 0.2);
|
|
382
|
+
--c-warn-22: rgba(245, 185, 68, 0.22);
|
|
383
|
+
--c-warn-30: rgba(245, 185, 68, 0.3);
|
|
384
|
+
--c-warn-60: rgba(245, 185, 68, 0.6);
|
|
385
|
+
--c-danger-12: rgba(240, 86, 106, 0.12);
|
|
386
|
+
--c-line-30: rgba(41, 41, 44, 0.3);
|
|
387
|
+
--c-contrast-fg: #0B0B0C;
|
|
388
|
+
--c-accent-hover: #FF6B3A;
|
|
389
|
+
--c-warn-hover: #F7C868;
|
|
390
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
391
|
+
--c-annotation: #F2F0EB;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
:root[data-theme="forest"] {
|
|
395
|
+
/* ── Core palette (forest · deep green) ──────────────────── */
|
|
396
|
+
--c-bg: #0C1712;
|
|
397
|
+
--c-panel: #14231B;
|
|
398
|
+
--c-panel-soft: #101D16;
|
|
399
|
+
--c-line: #26402F;
|
|
400
|
+
--c-text: #E4EFE7;
|
|
401
|
+
--c-text-soft: #C4D6CA;
|
|
402
|
+
--c-muted: #8BA695;
|
|
403
|
+
--c-dim: #5D7566;
|
|
404
|
+
--c-accent: #5CCF8F;
|
|
405
|
+
--c-ok: #74D99F;
|
|
406
|
+
--c-warn: #E0B84F;
|
|
407
|
+
--c-warn-alt: #D9A421;
|
|
408
|
+
--c-danger: #E57373;
|
|
409
|
+
--c-purple: #A88FE0;
|
|
410
|
+
--c-thinking: #A88FE0;
|
|
411
|
+
--c-subagent: #4FD8CE;
|
|
412
|
+
--c-input-bg: #101D16;
|
|
413
|
+
--c-dot-grid: rgba(92, 207, 143, 0.08);
|
|
414
|
+
--c-panel-overlay: rgba(12, 23, 18, 0.6);
|
|
415
|
+
--c-panel-glass: rgba(20, 35, 27, 0.92);
|
|
416
|
+
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
417
|
+
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
418
|
+
--c-glow-accent: rgba(92, 207, 143, 0.15);
|
|
419
|
+
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
420
|
+
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
421
|
+
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
422
|
+
--c-accent-8: rgba(92, 207, 143, 0.08);
|
|
423
|
+
--c-accent-10: rgba(92, 207, 143, 0.1);
|
|
424
|
+
--c-accent-12: rgba(92, 207, 143, 0.12);
|
|
425
|
+
--c-accent-15: rgba(92, 207, 143, 0.15);
|
|
426
|
+
--c-accent-25: rgba(92, 207, 143, 0.25);
|
|
427
|
+
--c-accent-30: rgba(92, 207, 143, 0.3);
|
|
428
|
+
--c-accent-40: rgba(92, 207, 143, 0.4);
|
|
429
|
+
--c-ok-10: rgba(116, 217, 159, 0.1);
|
|
430
|
+
--c-ok-12: rgba(116, 217, 159, 0.12);
|
|
431
|
+
--c-ok-20: rgba(116, 217, 159, 0.2);
|
|
432
|
+
--c-ok-25: rgba(116, 217, 159, 0.25);
|
|
433
|
+
--c-warn-10: rgba(224, 184, 79, 0.1);
|
|
434
|
+
--c-warn-12: rgba(224, 184, 79, 0.12);
|
|
435
|
+
--c-warn-15: rgba(224, 184, 79, 0.15);
|
|
436
|
+
--c-warn-20: rgba(224, 184, 79, 0.2);
|
|
437
|
+
--c-warn-22: rgba(224, 184, 79, 0.22);
|
|
438
|
+
--c-warn-30: rgba(224, 184, 79, 0.3);
|
|
439
|
+
--c-warn-60: rgba(224, 184, 79, 0.6);
|
|
440
|
+
--c-danger-12: rgba(229, 115, 115, 0.12);
|
|
441
|
+
--c-line-30: rgba(38, 64, 47, 0.3);
|
|
442
|
+
--c-contrast-fg: #0C1712;
|
|
443
|
+
--c-accent-hover: #7BDAA4;
|
|
444
|
+
--c-warn-hover: #E8C871;
|
|
445
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
446
|
+
--c-annotation: #EDF5EF;
|
|
447
|
+
}
|
|
448
|
+
|
|
174
449
|
* {
|
|
175
450
|
box-sizing: border-box;
|
|
176
451
|
margin: 0;
|
|
@@ -677,10 +952,141 @@ body,
|
|
|
677
952
|
max-width: 100%;
|
|
678
953
|
}
|
|
679
954
|
|
|
955
|
+
/* Positioning parent for a toolbar popover menu. Every menu MUST live inside
|
|
956
|
+
one of these wrapped around its trigger — anchoring to a wider ancestor
|
|
957
|
+
opens the menu far from the button (0.4.2 report Finding T). */
|
|
958
|
+
.toolbar-menu-anchor {
|
|
959
|
+
position: relative;
|
|
960
|
+
display: inline-flex;
|
|
961
|
+
}
|
|
962
|
+
|
|
680
963
|
.canvas-toolbar button svg {
|
|
681
964
|
display: block;
|
|
682
965
|
}
|
|
683
966
|
|
|
967
|
+
/* ── Toolbar popover menus (theme picker + narrow-screen More menu) ── */
|
|
968
|
+
.toolbar-menu {
|
|
969
|
+
position: absolute;
|
|
970
|
+
top: calc(100% + 8px);
|
|
971
|
+
right: 0;
|
|
972
|
+
z-index: 1200;
|
|
973
|
+
display: flex;
|
|
974
|
+
flex-direction: column;
|
|
975
|
+
gap: 2px;
|
|
976
|
+
min-width: 224px;
|
|
977
|
+
max-height: min(70vh, 560px);
|
|
978
|
+
overflow-y: auto;
|
|
979
|
+
padding: 6px;
|
|
980
|
+
background: var(--c-panel-glass);
|
|
981
|
+
backdrop-filter: blur(12px);
|
|
982
|
+
border: 1px solid var(--c-line);
|
|
983
|
+
border-radius: var(--radius);
|
|
984
|
+
box-shadow: 0 18px 48px var(--c-shadow-heavy);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.toolbar-menu-item {
|
|
988
|
+
display: flex;
|
|
989
|
+
align-items: center;
|
|
990
|
+
gap: 10px;
|
|
991
|
+
width: 100%;
|
|
992
|
+
min-height: 40px;
|
|
993
|
+
padding: 8px 12px;
|
|
994
|
+
background: none;
|
|
995
|
+
border: 1px solid transparent;
|
|
996
|
+
border-radius: var(--radius-sm);
|
|
997
|
+
color: var(--c-text-soft);
|
|
998
|
+
font: inherit;
|
|
999
|
+
font-size: 13px;
|
|
1000
|
+
text-align: left;
|
|
1001
|
+
cursor: pointer;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.toolbar-menu-item:hover {
|
|
1005
|
+
background: var(--c-surface-hover);
|
|
1006
|
+
color: var(--c-text);
|
|
1007
|
+
border-color: var(--c-line);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.toolbar-menu-item.active {
|
|
1011
|
+
color: var(--c-accent);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.toolbar-menu-item svg {
|
|
1015
|
+
flex-shrink: 0;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.toolbar-menu-check {
|
|
1019
|
+
margin-left: auto;
|
|
1020
|
+
color: var(--c-accent);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.toolbar-menu-separator {
|
|
1024
|
+
height: 1px;
|
|
1025
|
+
margin: 4px 8px;
|
|
1026
|
+
background: var(--c-line);
|
|
1027
|
+
flex-shrink: 0;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.toolbar-menu-heading {
|
|
1031
|
+
padding: 6px 12px 2px;
|
|
1032
|
+
font-size: 10px;
|
|
1033
|
+
letter-spacing: 0.08em;
|
|
1034
|
+
text-transform: uppercase;
|
|
1035
|
+
color: var(--c-dim);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.theme-swatch {
|
|
1039
|
+
display: inline-flex;
|
|
1040
|
+
align-items: center;
|
|
1041
|
+
justify-content: center;
|
|
1042
|
+
width: 18px;
|
|
1043
|
+
height: 18px;
|
|
1044
|
+
border: 1px solid var(--c-line);
|
|
1045
|
+
border-radius: 50%;
|
|
1046
|
+
flex-shrink: 0;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.theme-swatch-dot {
|
|
1050
|
+
width: 7px;
|
|
1051
|
+
height: 7px;
|
|
1052
|
+
border-radius: 50%;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.toolbar-more-glyph {
|
|
1056
|
+
display: block;
|
|
1057
|
+
font-size: 16px;
|
|
1058
|
+
font-weight: 700;
|
|
1059
|
+
line-height: 1;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Wide screens: the full two-bar toolbar; the More trigger stays hidden. */
|
|
1063
|
+
.toolbar-collapsible {
|
|
1064
|
+
display: contents;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.toolbar-more {
|
|
1068
|
+
display: none;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/* Narrow screens / embedded panels: ONE compact bar (brand, status, view
|
|
1072
|
+
controls) — every secondary control folds into the More menu instead of
|
|
1073
|
+
wrapping into a second toolbar row. */
|
|
1074
|
+
@media (max-width: 900px) {
|
|
1075
|
+
.toolbar-collapsible {
|
|
1076
|
+
display: none;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.toolbar-actions-bar {
|
|
1080
|
+
display: none;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.toolbar-more {
|
|
1084
|
+
/* inline-flex (NOT contents): this span is also the positioning anchor
|
|
1085
|
+
for the More menu — display:contents cannot anchor absolute children. */
|
|
1086
|
+
display: inline-flex;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
684
1090
|
@media (max-width: 900px) {
|
|
685
1091
|
.toolbar-tooltip {
|
|
686
1092
|
min-width: 150px;
|