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
|
@@ -121,6 +121,166 @@
|
|
|
121
121
|
--color-danger: #ff0000;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
:root[data-theme="midnight"] {
|
|
125
|
+
/* ── Core palette (midnight · deep indigo night) ─────────── */
|
|
126
|
+
--c-bg: #0A0D1C;
|
|
127
|
+
--c-panel: #12172E;
|
|
128
|
+
--c-panel-soft: #0D1224;
|
|
129
|
+
--c-line: #232B4A;
|
|
130
|
+
--c-text: #E6E9F7;
|
|
131
|
+
--c-text-soft: #C6CCE8;
|
|
132
|
+
--c-muted: #8D96BC;
|
|
133
|
+
--c-dim: #5B6383;
|
|
134
|
+
--c-accent: #8B96FF;
|
|
135
|
+
--c-ok: #34D399;
|
|
136
|
+
--c-warn: #F5C452;
|
|
137
|
+
--c-warn-alt: #FFB300;
|
|
138
|
+
--c-danger: #FF6B8A;
|
|
139
|
+
--c-purple: #B388FF;
|
|
140
|
+
|
|
141
|
+
--color-bg: #0A0D1C;
|
|
142
|
+
--color-panel: #12172E;
|
|
143
|
+
--color-surface: #0D1224;
|
|
144
|
+
--color-border: #232B4A;
|
|
145
|
+
--color-text: #E6E9F7;
|
|
146
|
+
--color-text-primary: #E6E9F7;
|
|
147
|
+
--color-text-secondary: #C6CCE8;
|
|
148
|
+
--color-text-muted: #8D96BC;
|
|
149
|
+
--color-text-dim: #5B6383;
|
|
150
|
+
--color-accent: #8B96FF;
|
|
151
|
+
--color-success: #34D399;
|
|
152
|
+
--color-warning: #F5C452;
|
|
153
|
+
--color-danger: #FF6B8A;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:root[data-theme="sepia"] {
|
|
157
|
+
/* ── Core palette (sepia · warm amber paper) ─────────────── */
|
|
158
|
+
--c-bg: #F2E7D5;
|
|
159
|
+
--c-panel: #EBDCC2;
|
|
160
|
+
--c-panel-soft: #E8D8BC;
|
|
161
|
+
--c-line: #D3C1A0;
|
|
162
|
+
--c-text: #2E2416;
|
|
163
|
+
--c-text-soft: #554833;
|
|
164
|
+
--c-muted: #77664C;
|
|
165
|
+
--c-dim: #978870;
|
|
166
|
+
--c-accent: #B4632C;
|
|
167
|
+
--c-ok: #3E7D4F;
|
|
168
|
+
--c-warn: #A97B1E;
|
|
169
|
+
--c-warn-alt: #8F6A0D;
|
|
170
|
+
--c-danger: #B3402E;
|
|
171
|
+
--c-purple: #8456B0;
|
|
172
|
+
|
|
173
|
+
--color-bg: #F2E7D5;
|
|
174
|
+
--color-panel: #EBDCC2;
|
|
175
|
+
--color-surface: #E8D8BC;
|
|
176
|
+
--color-border: #D3C1A0;
|
|
177
|
+
--color-text: #2E2416;
|
|
178
|
+
--color-text-primary: #2E2416;
|
|
179
|
+
--color-text-secondary: #554833;
|
|
180
|
+
--color-text-muted: #77664C;
|
|
181
|
+
--color-text-dim: #978870;
|
|
182
|
+
--color-accent: #B4632C;
|
|
183
|
+
--color-success: #3E7D4F;
|
|
184
|
+
--color-warning: #A97B1E;
|
|
185
|
+
--color-danger: #B3402E;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:root[data-theme="arctic"] {
|
|
189
|
+
/* ── Core palette (arctic · cool slate + ice) ────────────── */
|
|
190
|
+
--c-bg: #20242E;
|
|
191
|
+
--c-panel: #2A2F3C;
|
|
192
|
+
--c-panel-soft: #242936;
|
|
193
|
+
--c-line: #3A4152;
|
|
194
|
+
--c-text: #E9EDF4;
|
|
195
|
+
--c-text-soft: #CBD3E1;
|
|
196
|
+
--c-muted: #939DB1;
|
|
197
|
+
--c-dim: #667082;
|
|
198
|
+
--c-accent: #7FC8DE;
|
|
199
|
+
--c-ok: #74C596;
|
|
200
|
+
--c-warn: #E8C476;
|
|
201
|
+
--c-warn-alt: #E0A93F;
|
|
202
|
+
--c-danger: #D9707C;
|
|
203
|
+
--c-purple: #B48EAD;
|
|
204
|
+
|
|
205
|
+
--color-bg: #20242E;
|
|
206
|
+
--color-panel: #2A2F3C;
|
|
207
|
+
--color-surface: #242936;
|
|
208
|
+
--color-border: #3A4152;
|
|
209
|
+
--color-text: #E9EDF4;
|
|
210
|
+
--color-text-primary: #E9EDF4;
|
|
211
|
+
--color-text-secondary: #CBD3E1;
|
|
212
|
+
--color-text-muted: #939DB1;
|
|
213
|
+
--color-text-dim: #667082;
|
|
214
|
+
--color-accent: #7FC8DE;
|
|
215
|
+
--color-success: #74C596;
|
|
216
|
+
--color-warning: #E8C476;
|
|
217
|
+
--color-danger: #D9707C;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
:root[data-theme="ember"] {
|
|
221
|
+
/* ── Core palette (ember · neutral charcoal + vivid orange-red) ─── */
|
|
222
|
+
--c-bg: #0B0B0C;
|
|
223
|
+
--c-panel: #161618;
|
|
224
|
+
--c-panel-soft: #101012;
|
|
225
|
+
--c-line: #29292C;
|
|
226
|
+
--c-text: #EDEBE6;
|
|
227
|
+
--c-text-soft: #CBC9C2;
|
|
228
|
+
--c-muted: #96948D;
|
|
229
|
+
--c-dim: #66655F;
|
|
230
|
+
--c-accent: #F34E1C;
|
|
231
|
+
--c-ok: #4CC38A;
|
|
232
|
+
--c-warn: #F5B944;
|
|
233
|
+
--c-warn-alt: #FFB300;
|
|
234
|
+
--c-danger: #F0566A;
|
|
235
|
+
--c-purple: #A78BFA;
|
|
236
|
+
|
|
237
|
+
--color-bg: #0B0B0C;
|
|
238
|
+
--color-panel: #161618;
|
|
239
|
+
--color-surface: #101012;
|
|
240
|
+
--color-border: #29292C;
|
|
241
|
+
--color-text: #EDEBE6;
|
|
242
|
+
--color-text-primary: #EDEBE6;
|
|
243
|
+
--color-text-secondary: #CBC9C2;
|
|
244
|
+
--color-text-muted: #96948D;
|
|
245
|
+
--color-text-dim: #66655F;
|
|
246
|
+
--color-accent: #F34E1C;
|
|
247
|
+
--color-success: #4CC38A;
|
|
248
|
+
--color-warning: #F5B944;
|
|
249
|
+
--color-danger: #F0566A;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
:root[data-theme="forest"] {
|
|
253
|
+
/* ── Core palette (forest · deep green) ──────────────────── */
|
|
254
|
+
--c-bg: #0C1712;
|
|
255
|
+
--c-panel: #14231B;
|
|
256
|
+
--c-panel-soft: #101D16;
|
|
257
|
+
--c-line: #26402F;
|
|
258
|
+
--c-text: #E4EFE7;
|
|
259
|
+
--c-text-soft: #C4D6CA;
|
|
260
|
+
--c-muted: #8BA695;
|
|
261
|
+
--c-dim: #5D7566;
|
|
262
|
+
--c-accent: #5CCF8F;
|
|
263
|
+
--c-ok: #74D99F;
|
|
264
|
+
--c-warn: #E0B84F;
|
|
265
|
+
--c-warn-alt: #D9A421;
|
|
266
|
+
--c-danger: #E57373;
|
|
267
|
+
--c-purple: #A88FE0;
|
|
268
|
+
|
|
269
|
+
--color-bg: #0C1712;
|
|
270
|
+
--color-panel: #14231B;
|
|
271
|
+
--color-surface: #101D16;
|
|
272
|
+
--color-border: #26402F;
|
|
273
|
+
--color-text: #E4EFE7;
|
|
274
|
+
--color-text-primary: #E4EFE7;
|
|
275
|
+
--color-text-secondary: #C4D6CA;
|
|
276
|
+
--color-text-muted: #8BA695;
|
|
277
|
+
--color-text-dim: #5D7566;
|
|
278
|
+
--color-accent: #5CCF8F;
|
|
279
|
+
--color-success: #74D99F;
|
|
280
|
+
--color-warning: #E0B84F;
|
|
281
|
+
--color-danger: #E57373;
|
|
282
|
+
}
|
|
283
|
+
|
|
124
284
|
html,
|
|
125
285
|
body {
|
|
126
286
|
margin: 0;
|
package/src/client/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CanvasThemeName } from '../shared/themes.js';
|
|
2
|
+
|
|
1
3
|
export interface ViewportState {
|
|
2
4
|
x: number;
|
|
3
5
|
y: number;
|
|
@@ -108,7 +110,7 @@ export function isExcalidrawNode(node: CanvasNodeState): boolean {
|
|
|
108
110
|
|
|
109
111
|
export interface CanvasLayout {
|
|
110
112
|
viewport: ViewportState;
|
|
111
|
-
theme?:
|
|
113
|
+
theme?: CanvasThemeName;
|
|
112
114
|
nodes: CanvasNodeState[];
|
|
113
115
|
edges: CanvasEdge[];
|
|
114
116
|
annotations?: CanvasAnnotation[];
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Database } from 'bun:sqlite';
|
|
|
9
9
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
10
10
|
import { dirname } from 'node:path';
|
|
11
11
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
12
|
+
import { type CanvasThemeName, normalizeCanvasThemeName } from '../shared/themes.js';
|
|
12
13
|
import type {
|
|
13
14
|
CanvasAnnotation,
|
|
14
15
|
CanvasEdge,
|
|
@@ -42,10 +43,10 @@ import {
|
|
|
42
43
|
|
|
43
44
|
const SCHEMA_VERSION = 1;
|
|
44
45
|
|
|
45
|
-
export type CanvasTheme =
|
|
46
|
+
export type CanvasTheme = CanvasThemeName;
|
|
46
47
|
|
|
47
48
|
export function normalizeCanvasTheme(value: unknown, fallback: CanvasTheme = 'dark'): CanvasTheme {
|
|
48
|
-
return value
|
|
49
|
+
return normalizeCanvasThemeName(value, fallback);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const SCHEMA_SQL = `
|
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
HTML_SURFACE_PUSH_SOURCE,
|
|
22
22
|
} from '../shared/ax-surface-protocol.js';
|
|
23
23
|
import { contentHeightReporterTag } from '../shared/content-height-reporter.js';
|
|
24
|
+
import { type CanvasThemeName, normalizeCanvasThemeName } from '../shared/themes.js';
|
|
24
25
|
|
|
25
|
-
export type SurfaceTheme =
|
|
26
|
+
export type SurfaceTheme = CanvasThemeName;
|
|
26
27
|
|
|
27
28
|
/** Path the surface document links for its theme tokens (served from dist/canvas). */
|
|
28
29
|
export const SURFACE_THEME_STYLESHEET = '/canvas/surface-theme.css';
|
|
@@ -31,7 +32,7 @@ export const SURFACE_THEME_STYLESHEET = '/canvas/surface-theme.css';
|
|
|
31
32
|
export const HTML_SURFACE_SANDBOX = 'allow-scripts';
|
|
32
33
|
|
|
33
34
|
export function normalizeSurfaceTheme(value: string | null | undefined): SurfaceTheme {
|
|
34
|
-
return value
|
|
35
|
+
return normalizeCanvasThemeName(value);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -235,6 +236,12 @@ export interface HtmlSurfaceOptions {
|
|
|
235
236
|
axState?: unknown;
|
|
236
237
|
/** Nonce for the content-height reporter (lets the node grow to fit content). */
|
|
237
238
|
contentHeightToken?: string;
|
|
239
|
+
/**
|
|
240
|
+
* Contents of surface-theme.css to inline as a <style> instead of the
|
|
241
|
+
* same-origin <link> — required for srcdoc-rendered surfaces (nested-iframe
|
|
242
|
+
* hosts) where the stylesheet subresource may not load.
|
|
243
|
+
*/
|
|
244
|
+
inlineThemeCss?: string;
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/**
|
|
@@ -243,7 +250,14 @@ export interface HtmlSurfaceOptions {
|
|
|
243
250
|
*/
|
|
244
251
|
export function buildHtmlSurfaceDocument(userHtml: string, options: HtmlSurfaceOptions): string {
|
|
245
252
|
const themeToken = sanitizeToken(options.themeToken);
|
|
246
|
-
|
|
253
|
+
// Inline the theme tokens when the caller supplies the stylesheet content —
|
|
254
|
+
// in srcdoc-mode embeds (Amp orb portals) the surface document is rendered
|
|
255
|
+
// inline and a <link> subresource may not load, leaving the surface unstyled
|
|
256
|
+
// (dark-on-dark reads as blank). The <link> stays as the fallback for callers
|
|
257
|
+
// without the content.
|
|
258
|
+
const link = options.inlineThemeCss
|
|
259
|
+
? `<style data-pmx-surface-theme>${options.inlineThemeCss.replace(/<\//g, '<\\/')}</style>`
|
|
260
|
+
: `<link rel="stylesheet" href="${SURFACE_THEME_STYLESHEET}">`;
|
|
247
261
|
const themeBridge = buildThemeBridge(themeToken);
|
|
248
262
|
const presentationBridge = options.presentation
|
|
249
263
|
? buildPresentationEscapeBridge(sanitizeToken(options.presentationExitToken))
|
|
@@ -10,7 +10,8 @@ import { z } from 'zod';
|
|
|
10
10
|
import { normalizeCanvasTheme } from '../../canvas-db.js';
|
|
11
11
|
import { applyCanvasNodeUpdates } from '../../canvas-operations.js';
|
|
12
12
|
import { canvasState } from '../../canvas-state.js';
|
|
13
|
-
import {
|
|
13
|
+
import { CANVAS_THEMES, isCanvasTheme } from '../../../shared/themes.js';
|
|
14
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
14
15
|
|
|
15
16
|
const emptyShape = {};
|
|
16
17
|
const emptySchema = z.looseObject(emptyShape);
|
|
@@ -28,7 +29,12 @@ const themeGetOperation = defineOperation<z.infer<typeof emptySchema>, Record<st
|
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
const themeSetShape = {
|
|
31
|
-
theme: z
|
|
32
|
+
theme: z
|
|
33
|
+
.unknown()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(
|
|
36
|
+
'Theme name from the canvas theme registry (e.g. dark, light, high-contrast, midnight, sepia, arctic, ember, forest)',
|
|
37
|
+
),
|
|
32
38
|
};
|
|
33
39
|
const themeSetSchema = z.looseObject(themeSetShape);
|
|
34
40
|
|
|
@@ -42,6 +48,13 @@ const themeSetOperation = defineOperation<z.infer<typeof themeSetSchema>, Record
|
|
|
42
48
|
path: '/api/canvas/theme',
|
|
43
49
|
},
|
|
44
50
|
handler: (input, ctx) => {
|
|
51
|
+
// Reject unknown names instead of silently keeping the prior theme
|
|
52
|
+
// (0.4.2 report: the soft-accept made typos look like successes).
|
|
53
|
+
if (input.theme !== undefined && !isCanvasTheme(input.theme)) {
|
|
54
|
+
throw new OperationError(
|
|
55
|
+
`Unknown theme ${JSON.stringify(input.theme)}. Valid themes: ${CANVAS_THEMES.join(', ')}.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
45
58
|
const theme = normalizeCanvasTheme(input.theme, canvasState.theme);
|
|
46
59
|
const next = canvasState.setTheme(theme);
|
|
47
60
|
ctx.emit('theme-changed', { theme: next });
|
package/src/server/server.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { buildAgentContextPreamble } from './agent-context.js';
|
|
|
60
60
|
import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
|
|
61
61
|
import { resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
62
62
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
63
|
+
import { canvasThemeScheme, isCanvasTheme } from '../shared/themes.js';
|
|
63
64
|
import { validateLocalImageFile } from './image-source.js';
|
|
64
65
|
import {
|
|
65
66
|
cancelCodeGraphRecompute,
|
|
@@ -984,6 +985,14 @@ function canvasSpaHtml(): string {
|
|
|
984
985
|
}, 4000);
|
|
985
986
|
})();
|
|
986
987
|
</script>
|
|
988
|
+
${
|
|
989
|
+
// Amp orb services run with AMP_ORB=1. The portal's nested-iframe embed
|
|
990
|
+
// blocks src-URL child iframes, and the runtime probe is unreliable there
|
|
991
|
+
// (a tiny probe iframe can load even though node-sized ones will not), so
|
|
992
|
+
// the client needs to KNOW it is in an orb and force srcdoc mode directly
|
|
993
|
+
// (see src/client/state/iframe-mode.ts).
|
|
994
|
+
process.env.AMP_ORB ? '<script>window.__PMX_AMP_ORB = true;</script>' : ''
|
|
995
|
+
}
|
|
987
996
|
<script type="module" src="/canvas/index.js?v=${CANVAS_ASSET_VERSION}"></script>
|
|
988
997
|
</body>
|
|
989
998
|
</html>`;
|
|
@@ -1040,6 +1049,22 @@ function resolveCanvasBundleDir(): string {
|
|
|
1040
1049
|
return fallback;
|
|
1041
1050
|
}
|
|
1042
1051
|
|
|
1052
|
+
// Memoized surface-theme.css content for inlining into HTML surface documents
|
|
1053
|
+
// (srcdoc-rendered surfaces in nested-iframe hosts may fail to load the <link>
|
|
1054
|
+
// subresource, leaving them unstyled — dark-on-dark reads as blank). Read from
|
|
1055
|
+
// the same bundle root that serves /canvas/surface-theme.css; null keeps the
|
|
1056
|
+
// <link> fallback when the asset is missing (e.g. an unbuilt dev tree).
|
|
1057
|
+
let _surfaceThemeCss: string | null | undefined;
|
|
1058
|
+
function surfaceThemeCssInline(): string | null {
|
|
1059
|
+
if (_surfaceThemeCss !== undefined) return _surfaceThemeCss;
|
|
1060
|
+
try {
|
|
1061
|
+
_surfaceThemeCss = readFileSync(resolve(resolveCanvasBundleDir(), 'surface-theme.css'), 'utf-8');
|
|
1062
|
+
} catch {
|
|
1063
|
+
_surfaceThemeCss = null;
|
|
1064
|
+
}
|
|
1065
|
+
return _surfaceThemeCss;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1043
1068
|
/**
|
|
1044
1069
|
* Containment check for bundle-asset serving. Separator-agnostic: on Windows,
|
|
1045
1070
|
* `resolve()` returns backslash paths, so comparing against a `${dir}/` template
|
|
@@ -1262,6 +1287,9 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1262
1287
|
...(url.searchParams.get('frameToken')
|
|
1263
1288
|
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1264
1289
|
: {}),
|
|
1290
|
+
// Inline the theme tokens so srcdoc-rendered surfaces (Amp orb portals)
|
|
1291
|
+
// are styled without depending on the <link> subresource.
|
|
1292
|
+
...(surfaceThemeCssInline() ? { inlineThemeCss: surfaceThemeCssInline() as string } : {}),
|
|
1265
1293
|
});
|
|
1266
1294
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1267
1295
|
}
|
|
@@ -1358,8 +1386,14 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1358
1386
|
);
|
|
1359
1387
|
|
|
1360
1388
|
const themeValue = url.searchParams.get('theme');
|
|
1389
|
+
// The viewer bundle themes dark/light/high-contrast directly; other named
|
|
1390
|
+
// canvas themes (midnight, sepia, …) collapse to their dark/light scheme.
|
|
1361
1391
|
const theme =
|
|
1362
|
-
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
|
|
1392
|
+
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
|
|
1393
|
+
? themeValue
|
|
1394
|
+
: isCanvasTheme(themeValue)
|
|
1395
|
+
? canvasThemeScheme(themeValue)
|
|
1396
|
+
: undefined;
|
|
1363
1397
|
const title = (node.data.title as string) || node.id;
|
|
1364
1398
|
// Devtools panel is double-gated: the operator must opt in via the env flag
|
|
1365
1399
|
// AND the request must carry ?devtools=1. Off by default in all normal runs.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical canvas theme registry — single source of truth for every layer
|
|
3
|
+
* that validates, stores, or renders a theme name (client shell, HTML
|
|
4
|
+
* surfaces, json-render viewer, CLI/env plumbing, persistence).
|
|
5
|
+
*
|
|
6
|
+
* Adding a theme: add the name here, author its variable blocks in
|
|
7
|
+
* src/client/theme/global.css AND src/client/theme/surface-theme.css (guarded
|
|
8
|
+
* by tests/unit/surface-theme-tokens.test.ts), and give it meta below. The
|
|
9
|
+
* theme picker, server validation, and surface documents pick it up from this
|
|
10
|
+
* list.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CANVAS_THEMES = [
|
|
14
|
+
'dark',
|
|
15
|
+
'light',
|
|
16
|
+
'high-contrast',
|
|
17
|
+
'midnight',
|
|
18
|
+
'sepia',
|
|
19
|
+
'arctic',
|
|
20
|
+
'ember',
|
|
21
|
+
'forest',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type CanvasThemeName = (typeof CANVAS_THEMES)[number];
|
|
25
|
+
|
|
26
|
+
export function isCanvasTheme(value: unknown): value is CanvasThemeName {
|
|
27
|
+
return typeof value === 'string' && (CANVAS_THEMES as readonly string[]).includes(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeCanvasThemeName(value: unknown, fallback: CanvasThemeName = 'dark'): CanvasThemeName {
|
|
31
|
+
return isCanvasTheme(value) ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CanvasThemeMeta {
|
|
35
|
+
/** Human label shown in the theme picker. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Whether embedded viewers that only know dark/light should treat it as light. */
|
|
38
|
+
scheme: 'dark' | 'light';
|
|
39
|
+
/** Picker swatch colors — MUST match --c-bg / --c-accent in global.css. */
|
|
40
|
+
swatchBg: string;
|
|
41
|
+
swatchAccent: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const CANVAS_THEME_META: Record<CanvasThemeName, CanvasThemeMeta> = {
|
|
45
|
+
dark: { label: 'Dark', scheme: 'dark', swatchBg: '#081524', swatchAccent: '#4BBCFF' },
|
|
46
|
+
light: { label: 'Light', scheme: 'light', swatchBg: '#F4EFE6', swatchAccent: '#1A7ABF' },
|
|
47
|
+
'high-contrast': { label: 'High contrast', scheme: 'dark', swatchBg: '#000000', swatchAccent: '#00ffff' },
|
|
48
|
+
midnight: { label: 'Midnight', scheme: 'dark', swatchBg: '#0A0D1C', swatchAccent: '#8B96FF' },
|
|
49
|
+
sepia: { label: 'Sepia', scheme: 'light', swatchBg: '#F2E7D5', swatchAccent: '#B4632C' },
|
|
50
|
+
arctic: { label: 'Arctic', scheme: 'dark', swatchBg: '#20242E', swatchAccent: '#7FC8DE' },
|
|
51
|
+
ember: { label: 'Ember', scheme: 'dark', swatchBg: '#0B0B0C', swatchAccent: '#F34E1C' },
|
|
52
|
+
forest: { label: 'Forest', scheme: 'dark', swatchBg: '#0C1712', swatchAccent: '#5CCF8F' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Collapse a theme to the dark/light scheme embedded dark|light-only viewers understand. */
|
|
56
|
+
export function canvasThemeScheme(value: unknown): 'dark' | 'light' {
|
|
57
|
+
return CANVAS_THEME_META[normalizeCanvasThemeName(value)].scheme;
|
|
58
|
+
}
|