11import { kitAssets } from "./kits.ts" ;
2- import { type Theme , themeById , tokenThemeCss , viewerThemeCss } from "./themes.ts" ;
2+ import {
3+ type Mode ,
4+ schemeCss ,
5+ type Theme ,
6+ themeById ,
7+ tokenThemeCss ,
8+ viewerThemeCss ,
9+ } from "./themes.ts" ;
10+
11+ // The kit's two custom SVG accent ramps (teal, coral) aren't in the theme
12+ // palette, so they carry their own light/dark values. Like the theme tokens
13+ // they pin to a forced mode (no media query) when one is given, else flip with
14+ // the OS — kept in sync via the shared schemeCss. Dark overrides only bg/text;
15+ // the line color is shared, so it's repeated in both maps.
16+ const KIT_ACCENTS_LIGHT : Record < string , string > = {
17+ "c-teal-bg" : "#e1f4f1" ,
18+ "c-teal-line" : "#1fa996" ,
19+ "c-teal-text" : "#0c6e62" ,
20+ "c-coral-bg" : "#fdece5" ,
21+ "c-coral-line" : "#e8835e" ,
22+ "c-coral-text" : "#a44f28" ,
23+ } ;
24+ const KIT_ACCENTS_DARK : Record < string , string > = {
25+ ...KIT_ACCENTS_LIGHT ,
26+ "c-teal-bg" : "rgba(31, 169, 150, 0.18)" ,
27+ "c-teal-text" : "#6fd0c2" ,
28+ "c-coral-bg" : "rgba(232, 131, 94, 0.18)" ,
29+ "c-coral-text" : "#f0a987" ,
30+ } ;
31+ const kitAccentCss = ( mode ?: Mode ) : string => schemeCss ( KIT_ACCENTS_LIGHT , KIT_ACCENTS_DARK , mode ) ;
32+
33+ // When a scheme is pinned, force the document's used color-scheme to match so
34+ // the UA-painted canvas, scrollbars, and native form controls follow it too
35+ // (the token vars alone don't drive those). Overrides the static
36+ // `color-scheme: light dark` default the kit/base CSS sets. Empty when the
37+ // scheme is left to the OS, preserving the media-query behavior unchanged.
38+ const colorSchemeCss = ( mode ?: Mode ) : string => ( mode ? `:root{color-scheme:${ mode } }` : "" ) ;
339
440// Origins html parts may load external resources from. Mirrors the allowlist
541// agents already know from Claude's inline widget surface.
@@ -63,17 +99,7 @@ body {
6399// attributes (fill/font-size on text, etc.) — that's why text styling is
64100// opt-in via classes.
65101const KIT_CSS = `
66- :root {
67- color-scheme: light dark;
68- --c-teal-bg: #e1f4f1; --c-teal-line: #1fa996; --c-teal-text: #0c6e62;
69- --c-coral-bg: #fdece5; --c-coral-line: #e8835e; --c-coral-text: #a44f28;
70- }
71- @media (prefers-color-scheme: dark) {
72- :root {
73- --c-teal-bg: rgba(31, 169, 150, 0.18); --c-teal-text: #6fd0c2;
74- --c-coral-bg: rgba(232, 131, 94, 0.18); --c-coral-text: #f0a987;
75- }
76- }
102+ :root { color-scheme: light dark; }
77103button {
78104 font: 500 14px/1.4 var(--font-sans);
79105 color: var(--color-text-primary);
@@ -210,11 +236,18 @@ function buildRichCsp(origin: string): string {
210236// mermaid / DOMPurify / @pierre -diffs sanitizer bypass can no longer reach the
211237// board. `css` is the part-specific stylesheet (prose/diff/mermaid rules);
212238// chrome theme vars come from viewerThemeCss so the part matches the viewer.
239+ // `mode` PINS those vars (and any shiki dark-flip the css carries) to the
240+ // scheme the chrome resolved, so this frame can't diverge from it. Unlike an
241+ // html part, it deliberately does NOT force `color-scheme`: these frames are
242+ // transparent so the themed card surface shows through, and a forced
243+ // `color-scheme` would paint an opaque UA canvas behind them. They carry no
244+ // native scrollbars/controls that need it, so the var pinning alone suffices.
213245export function renderSandboxedPart ( doc : {
214246 body : string ;
215247 css : string ;
216248 origin : string ;
217249 theme ?: Theme | string ;
250+ mode ?: Mode ;
218251} ) : string {
219252 const theme =
220253 typeof doc . theme === "string" || doc . theme == null ? themeById ( doc . theme ) : doc . theme ;
@@ -229,7 +262,7 @@ export function renderSandboxedPart(doc: {
229262 img-src in buildRichCsp allows that origin. (html parts don't need this —
230263 they load via /s/:id, whose URL is already the base.) -->
231264<base href="${ doc . origin } /">
232- <style>${ viewerThemeCss ( theme ) } ${ doc . css } </style>
265+ <style>${ viewerThemeCss ( theme , doc . mode ) } ${ doc . css } </style>
233266</head>
234267<body>
235268${ doc . body }
@@ -243,6 +276,9 @@ export function renderHtmlPage(doc: {
243276 html : string ;
244277 origin : string ;
245278 theme ?: Theme | string ;
279+ // Pins the iframe's color scheme to the one the chrome resolved (see Mode).
280+ // Omitted → the scheme follows the OS via tokenThemeCss's media query.
281+ mode ?: Mode ;
246282 // Opt-in kits (kits.ts): their CSS/JS is injected after the base kit. The JS
247283 // is plain inline script — same trust level as the bridge, already covered by
248284 // the html-part CSP's `script-src 'unsafe-inline'`. Unknown ids are ignored.
@@ -258,7 +294,7 @@ export function renderHtmlPage(doc: {
258294<meta name="viewport" content="width=device-width, initial-scale=1">
259295<meta http-equiv="Content-Security-Policy" content="${ buildCsp ( doc . origin ) } ">
260296<title>${ escapeHtml ( doc . title ) } </title>
261- <style>${ tokenThemeCss ( theme ) } ${ TOKENS_CSS } ${ KIT_CSS } ${ kit . css } </style>
297+ <style>${ tokenThemeCss ( theme , doc . mode ) } ${ TOKENS_CSS } ${ KIT_CSS } ${ kitAccentCss ( doc . mode ) } ${ kit . css } ${ colorSchemeCss ( doc . mode ) } </style>
262298</head>
263299<body>
264300${ SVG_DEFS }
0 commit comments