pmx-canvas 0.4.0 → 0.4.2
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/CHANGELOG.md +51 -0
- package/Readme.md +9 -4
- package/dist/canvas/global.css +397 -0
- package/dist/canvas/index.js +89 -89
- package/dist/canvas/surface-theme.css +160 -0
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/nodes/iframe-document-url.d.ts +1 -0
- package/dist/types/client/nodes/use-surface-frame.d.ts +13 -0
- package/dist/types/client/state/canvas-store.d.ts +1 -0
- package/dist/types/client/state/iframe-mode.d.ts +26 -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/RELEASE.md +10 -4
- package/docs/cli.md +1 -1
- package/docs/environment.md +2 -1
- package/docs/http-api.md +29 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +9 -1
- package/docs/screenshot.png +0 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +32 -4
- package/skills/pmx-canvas/references/full-reference.md +3 -3
- package/skills/pmx-canvas-testing/SKILL.md +3 -0
- package/src/cli/index.ts +3 -2
- package/src/client/App.tsx +201 -53
- package/src/client/index.tsx +5 -0
- package/src/client/nodes/ExtAppFrame.tsx +36 -1
- package/src/client/nodes/HtmlNode.tsx +5 -1
- package/src/client/nodes/McpAppNode.tsx +9 -2
- package/src/client/nodes/iframe-document-url.ts +62 -9
- package/src/client/nodes/use-surface-frame.ts +53 -0
- package/src/client/state/canvas-store.ts +5 -0
- package/src/client/state/iframe-mode.ts +118 -0
- package/src/client/state/sse-bridge.ts +8 -2
- package/src/client/theme/global.css +397 -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 +6 -1
- package/src/server/server.ts +59 -2
- package/src/shared/themes.ts +58 -0
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
|
|
@@ -1139,6 +1164,20 @@ function handleFrameDocument(pathname: string): Response {
|
|
|
1139
1164
|
});
|
|
1140
1165
|
}
|
|
1141
1166
|
|
|
1167
|
+
// Tiny always-200 document the client loads in a hidden iframe at boot to learn
|
|
1168
|
+
// whether this embedding context loads iframes from src URLs at all — nested
|
|
1169
|
+
// iframe hosts like the Amp orb portal block them, and the client then falls
|
|
1170
|
+
// back to fetch() + srcdoc surfaces (see src/client/state/iframe-mode.ts).
|
|
1171
|
+
function handleIframeProbe(): Response {
|
|
1172
|
+
return new Response('<!doctype html><title>pmx-canvas iframe probe</title>', {
|
|
1173
|
+
headers: {
|
|
1174
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1175
|
+
'Cache-Control': 'no-store',
|
|
1176
|
+
'Content-Security-Policy': 'sandbox allow-scripts',
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1142
1181
|
// ── Ext-app recovery diagnostics (0.3.2 report Finding N) ──────
|
|
1143
1182
|
//
|
|
1144
1183
|
// The WebKit black-tile recovery trail is otherwise trapped inside host panels
|
|
@@ -1248,6 +1287,9 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1248
1287
|
...(url.searchParams.get('frameToken')
|
|
1249
1288
|
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1250
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 } : {}),
|
|
1251
1293
|
});
|
|
1252
1294
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1253
1295
|
}
|
|
@@ -1344,8 +1386,14 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1344
1386
|
);
|
|
1345
1387
|
|
|
1346
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.
|
|
1347
1391
|
const theme =
|
|
1348
|
-
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;
|
|
1349
1397
|
const title = (node.data.title as string) || node.id;
|
|
1350
1398
|
// Devtools panel is double-gated: the operator must opt in via the env flag
|
|
1351
1399
|
// AND the request must carry ?devtools=1. Off by default in all normal runs.
|
|
@@ -3011,10 +3059,19 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3011
3059
|
return handleCreateFrameDocument(req);
|
|
3012
3060
|
}
|
|
3013
3061
|
|
|
3014
|
-
|
|
3062
|
+
// HEAD lets clients revalidate a minted frame URL after reconnect
|
|
3063
|
+
// (Finding S) without downloading the whole document.
|
|
3064
|
+
if (
|
|
3065
|
+
url.pathname.startsWith('/api/canvas/frame-documents/') &&
|
|
3066
|
+
(req.method === 'GET' || req.method === 'HEAD')
|
|
3067
|
+
) {
|
|
3015
3068
|
return handleFrameDocument(url.pathname);
|
|
3016
3069
|
}
|
|
3017
3070
|
|
|
3071
|
+
if (url.pathname === '/api/canvas/iframe-probe' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
3072
|
+
return handleIframeProbe();
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3018
3075
|
if (url.pathname.startsWith('/api/canvas/surface/') && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
3019
3076
|
return handleNodeSurface(url.pathname, url);
|
|
3020
3077
|
}
|
|
@@ -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: '#0E0E11', swatchAccent: '#FF5C2E' },
|
|
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
|
+
}
|