@@ -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+
41153function 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