Skip to content

Commit 2139637

Browse files
committed
fix jupyter notebook dark theme
1 parent 901e82e commit 2139637

File tree

2 files changed

+124
-9
lines changed

2 files changed

+124
-9
lines changed

source/npm/qsharp/ux/qsharp-ux.css

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,17 @@ modern-normalize (see https://mattbrictson.com/blog/css-normalize-and-reset for
7676

7777
/* Histogram palette overrides for dark themes.
7878
79-
VS Code webviews can indicate the theme in a few ways:
80-
- Classes: .vscode-dark, .vscode-high-contrast, .vscode-high-contrast-light
81-
- Attribute: data-vscode-theme-kind="vscode-..." (preferred)
79+
Theme detection sources:
80+
- VS Code webviews: body[data-vscode-theme-kind="vscode-dark"|"vscode-high-contrast"].
81+
- JupyterLab/Notebook 7: use data-qs-color-scheme selector set by render code.
8282
83-
Important: VS Code adds .vscode-high-contrast for backwards compatibility even
84-
when the theme is actually vscode-high-contrast-light, so we must explicitly
85-
exclude that case.
83+
Note: the Jupyter widgets (AnyWidget) render inside a Shadow DOM, so we include
84+
data-qs-color-scheme selector to allow matching in response to render done from widget
85+
index.
8686
*/
87-
body.vscode-dark,
88-
body.vscode-high-contrast:not(.vscode-high-contrast-light),
8987
body[data-vscode-theme-kind="vscode-dark"],
9088
body[data-vscode-theme-kind="vscode-high-contrast"],
91-
body[vscode-color-theme="dark"] {
89+
[data-qs-color-scheme="dark"] {
9290
--qs-histogram-bg: #1e1e1e;
9391
--qs-histogram-border: #3c3c3c;
9492
--qs-histogram-bar-fill: #4aa3ff;

source/widgets/js/index.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,126 @@ type RenderArgs = {
3838
el: HTMLElement;
3939
};
4040

41+
function getWidgetHost(el: HTMLElement): HTMLElement {
42+
const root = el.getRootNode();
43+
if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
44+
return root.host;
45+
}
46+
47+
return el;
48+
}
49+
50+
function computeColorScheme(doc: Document): "dark" | "light" {
51+
const body = doc.body;
52+
53+
// VS Code webviews
54+
const vscodeKind = body?.getAttribute("data-vscode-theme-kind");
55+
if (vscodeKind === "vscode-dark" || vscodeKind === "vscode-high-contrast") {
56+
return "dark";
57+
}
58+
if (
59+
vscodeKind === "vscode-light" ||
60+
vscodeKind === "vscode-high-contrast-light"
61+
) {
62+
return "light";
63+
}
64+
65+
// JupyterLab / Notebook 7
66+
if (body?.classList.contains("jp-mod-theme-dark")) {
67+
return "dark";
68+
}
69+
if (body?.classList.contains("jp-mod-theme-light")) {
70+
return "light";
71+
}
72+
73+
// Some JupyterLab themes expose data-jp-theme-light="true|false".
74+
const jpThemeLight = body?.getAttribute("data-jp-theme-light");
75+
if (jpThemeLight === "false") {
76+
return "dark";
77+
}
78+
if (jpThemeLight === "true") {
79+
return "light";
80+
}
81+
82+
// Fallback: OS/browser preference.
83+
return doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)")?.matches
84+
? "dark"
85+
: "light";
86+
}
87+
88+
const themeObserverByDocument = new WeakMap<Document, MutationObserver>();
89+
const themeHostsByDocument = new WeakMap<Document, Set<HTMLElement>>();
90+
91+
function applyAndObserveTheme(el: HTMLElement) {
92+
const doc = el.ownerDocument;
93+
const host = getWidgetHost(el);
94+
95+
let hosts = themeHostsByDocument.get(doc);
96+
if (!hosts) {
97+
hosts = new Set<HTMLElement>();
98+
themeHostsByDocument.set(doc, hosts);
99+
}
100+
hosts.add(host);
101+
102+
const apply = () => {
103+
const scheme = computeColorScheme(doc);
104+
if (scheme === "dark") {
105+
host.setAttribute("data-qs-color-scheme", "dark");
106+
} else {
107+
host.removeAttribute("data-qs-color-scheme");
108+
}
109+
};
110+
111+
apply();
112+
113+
// Ensure we only register one observer per document.
114+
if (!themeObserverByDocument.has(doc)) {
115+
const observer = new MutationObserver(() => {
116+
const currentHosts = themeHostsByDocument.get(doc);
117+
if (!currentHosts) return;
118+
119+
const scheme = computeColorScheme(doc);
120+
121+
// Apply to all known hosts; drop any that are no longer connected.
122+
for (const h of currentHosts) {
123+
if (!h.isConnected) {
124+
currentHosts.delete(h);
125+
continue;
126+
}
127+
128+
if (scheme === "dark") {
129+
h.setAttribute("data-qs-color-scheme", "dark");
130+
} else {
131+
h.removeAttribute("data-qs-color-scheme");
132+
}
133+
}
134+
});
135+
136+
// Observe theme changes on the body (VS Code and JupyterLab mutate class/attrs).
137+
if (doc.body) {
138+
observer.observe(doc.body, {
139+
attributes: true,
140+
attributeFilter: [
141+
"class",
142+
"data-vscode-theme-kind",
143+
"data-jp-theme-light",
144+
"data-jp-theme-name",
145+
],
146+
});
147+
}
148+
149+
themeObserverByDocument.set(doc, observer);
150+
}
151+
}
152+
41153
function render({ model, el }: RenderArgs) {
42154
const componentType = model.get("comp");
43155

156+
// Set a stable color-scheme marker for CSS (works in Jupyter-only and VS Code).
157+
// This is especially important when AnyWidget uses Shadow DOM, where selectors
158+
// like body.jp-mod-theme-dark won't match inside the widget stylesheet.
159+
applyAndObserveTheme(el);
160+
44161
// There is an existing issue where in VS Code it always shows the widget background as white.
45162
// (See https://github.com/microsoft/vscode-jupyter/issues/7161)
46163
// We tried to fix this in CSS by overriding the style, but there is a race condition whereby

0 commit comments

Comments
 (0)