fix(embed): SVG text now follows the iframe theme
Browse filesThree stacked bugs were keeping the chart text black in dark mode:
1. EmbedStudio mounted both preview iframes with
sandbox="allow-scripts" only (no allow-same-origin). The bootstrap
script couldn't reach window.parent.document, so the cascade fell
through localStorage to matchMedia(prefers-color-scheme) - which
returns the OS preference, not the app theme. Result: the iframe
could land on a different theme than the article. Aligned the
sandbox with HtmlEmbedView ("allow-scripts allow-same-origin").
2. The THEME_BOOTSTRAP in shared/embed-doc.ts always overwrote the
srcdoc-provided data-theme attribute, even when every detection
path failed. Honour the existing data-theme before falling back
to matchMedia so an explicit isDark from buildEmbedSrcdoc wins.
3. SVG text/line/path don't inherit CSS color the way HTML elements
do - their default fill/stroke is "black", not "currentColor".
Charts that scoped colours via `body { color: var(--text-color) }`
still rendered black axes/labels in dark mode. Added defaults to
BASE_STYLES so any SVG text without an explicit fill, and any d3
axis/grid line/path without an explicit stroke, inherits the
cascaded color via currentColor. Charts that paint their own
colours via attribute or inline style still win by specificity.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@@ -149,6 +149,15 @@ const THEME_BOOTSTRAP = `
|
|
| 149 |
if (!theme) {
|
| 150 |
try { theme = window.localStorage && window.localStorage.getItem('theme'); } catch(e) {}
|
| 151 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if (!theme) {
|
| 153 |
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
| 154 |
}
|
|
@@ -221,6 +230,18 @@ body {
|
|
| 221 |
scroll container that would trap scroll events or inflate scrollHeight. */
|
| 222 |
overflow: clip;
|
| 223 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
`.trim();
|
| 225 |
|
| 226 |
/**
|
|
|
|
| 149 |
if (!theme) {
|
| 150 |
try { theme = window.localStorage && window.localStorage.getItem('theme'); } catch(e) {}
|
| 151 |
}
|
| 152 |
+
// Respect the srcdoc-provided data-theme before falling back to the
|
| 153 |
+
// OS preference. Without this, a cross-origin sandbox iframe (e.g.
|
| 154 |
+
// an embed-studio preview with sandbox="allow-scripts" but no
|
| 155 |
+
// allow-same-origin) can't read the parent and the bootstrap would
|
| 156 |
+
// otherwise clobber the explicitly-set initial theme with
|
| 157 |
+
// matchMedia's guess.
|
| 158 |
+
if (!theme) {
|
| 159 |
+
theme = document.documentElement.getAttribute('data-theme') || null;
|
| 160 |
+
}
|
| 161 |
if (!theme) {
|
| 162 |
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
| 163 |
}
|
|
|
|
| 230 |
scroll container that would trap scroll events or inflate scrollHeight. */
|
| 231 |
overflow: clip;
|
| 232 |
}
|
| 233 |
+
/* SVG quirk: text/line/path do not inherit CSS color the way HTML does.
|
| 234 |
+
Their default fill/stroke is "black", not "currentColor". A chart
|
| 235 |
+
that only sets body color via var(--text-color) would still render
|
| 236 |
+
black axes/labels in dark mode. Default everything without its own
|
| 237 |
+
fill/stroke to currentColor so the cascaded body color drives the
|
| 238 |
+
look. Charts that want a specific colour just set the attribute or
|
| 239 |
+
inline style and win by specificity. */
|
| 240 |
+
svg text:not([fill]) { fill: currentColor; }
|
| 241 |
+
svg .axis path:not([stroke]),
|
| 242 |
+
svg .axis line:not([stroke]),
|
| 243 |
+
svg .domain:not([stroke]) { stroke: currentColor; }
|
| 244 |
+
svg .grid line:not([stroke]) { stroke: currentColor; opacity: 0.12; }
|
| 245 |
`.trim();
|
| 246 |
|
| 247 |
/**
|
|
@@ -393,14 +393,14 @@ export function EmbedStudio({
|
|
| 393 |
<iframe
|
| 394 |
ref={frameA}
|
| 395 |
title="Chart preview A"
|
| 396 |
-
sandbox="allow-scripts"
|
| 397 |
onLoad={() => handleFrameLoad("a")}
|
| 398 |
style={frameStyle("a")}
|
| 399 |
/>
|
| 400 |
<iframe
|
| 401 |
ref={frameB}
|
| 402 |
title="Chart preview B"
|
| 403 |
-
sandbox="allow-scripts"
|
| 404 |
onLoad={() => handleFrameLoad("b")}
|
| 405 |
style={frameStyle("b")}
|
| 406 |
/>
|
|
|
|
| 393 |
<iframe
|
| 394 |
ref={frameA}
|
| 395 |
title="Chart preview A"
|
| 396 |
+
sandbox="allow-scripts allow-same-origin"
|
| 397 |
onLoad={() => handleFrameLoad("a")}
|
| 398 |
style={frameStyle("a")}
|
| 399 |
/>
|
| 400 |
<iframe
|
| 401 |
ref={frameB}
|
| 402 |
title="Chart preview B"
|
| 403 |
+
sandbox="allow-scripts allow-same-origin"
|
| 404 |
onLoad={() => handleFrameLoad("b")}
|
| 405 |
style={frameStyle("b")}
|
| 406 |
/>
|