@marimo-team/islands 0.25.3-dev272 → 0.25.3-dev275
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/dist/{ConnectedDataExplorerComponent-eLHPh6uu.js → ConnectedDataExplorerComponent-CErm132k.js} +25 -26
- package/dist/{ErrorBoundary-vPW0qTmm.js → ErrorBoundary-BuIXiuc5.js} +43 -44
- package/dist/{abnfDiagram-VCTEODGH-hqd0IBtX.js → abnfDiagram-VCTEODGH-d-Qn2fwb.js} +2 -2
- package/dist/{any-language-editor-DnG26QXh.js → any-language-editor-6k6F12Z3.js} +3 -3
- package/dist/{architectureDiagram-5GKGNRK7-BzLAFcc0.js → architectureDiagram-5GKGNRK7-DxragHYv.js} +1 -1
- package/dist/{assertNever-DIU3JJbt.js → assertNever-B-YzWzbL.js} +14 -14
- package/dist/{button-0-RqY1DW.js → button-DZuNnIZO.js} +220 -223
- package/dist/{capabilities-B6yy_CBN.js → capabilities-D9FskwsK.js} +1 -1
- package/dist/{chat-ui-BWIkve2R.js → chat-ui-SZ6lSH52.js} +144 -145
- package/dist/{check-gUSZbRsl.js → check-XFzZeH-y.js} +1 -1
- package/dist/{chunk-IMKFNOWR-DOeVvuyE.js → chunk-IMKFNOWR-C0x0feFg.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DZoFI8wR.js → chunk-SHT3W25Y-D0ZdASqB.js} +1 -1
- package/dist/{chunk-SVP7TREG-CWu86PSW.js → chunk-SVP7TREG-BHjL96zx.js} +1 -1
- package/dist/{chunk-TICWLB2K-BrM0GoP3.js → chunk-TICWLB2K-CzcrwGfp.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-DYi4V2bB.js → classDiagram-ZZMXUADV-BTqFAZTP.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-tN7FIk-m.js → classDiagram-v2-VYDZK3BY-EPXb0Elt.js} +2 -2
- package/dist/{code-visibility-9gqV0vno.js → code-visibility-BEAPMrC-.js} +20 -20
- package/dist/{common-CT0KuTWr.js → common-CEmzaoJ8.js} +799 -801
- package/dist/{config-BsPruWMR.js → config-C8MFHQ2E.js} +23 -22
- package/dist/{copy-icon-C2I8sw4h.js → copy-icon-CtllOdmg.js} +3 -3
- package/dist/{createReducer-DdtT_7ji.js → createReducer-DRqwmaTc.js} +2 -2
- package/dist/{cynefinDiagram-5FMLGOSQ-D5nCnDc-.js → cynefinDiagram-5FMLGOSQ-CBSD344q.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-BHkd7f_i.js → dagre-GXQ25YYZ-JItjh3Ge.js} +1 -1
- package/dist/{data-grid-overlay-editor-DKDADhfM.js → data-grid-overlay-editor-CUo25_W8.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-D1ALppT2.js → diagram-S7CK7UJ4-CTD9xTVb.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DLv0b83L.js → diagram-UQ7AKVKN-DTfnFai8.js} +1 -1
- package/dist/{diagram-VX7I27RA-7XJ7J6qg.js → diagram-VX7I27RA-BcSnucV-.js} +1 -1
- package/dist/{diagram-Z3DM3KII-7M7zbHp_.js → diagram-Z3DM3KII-C2ATIE8A.js} +1 -1
- package/dist/{dialog-DzCj1qQv.js → dialog-CaYm5imE.js} +33 -33
- package/dist/{dropdown-menu-CdXE15an.js → dropdown-menu-Bb0bFOIk.js} +149 -149
- package/dist/{ebnfDiagram-PWID7BFC-DHH1qlmV.js → ebnfDiagram-PWID7BFC-B7B9JPxc.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-fI_0dGef.js → erDiagram-RLTQ6QDP-v6hDno1O.js} +1 -1
- package/dist/{error-banner-DPc9eIw5.js → error-banner-CHCdHVFB.js} +44 -45
- package/dist/{error-entries-CxN-Nq3V.js → error-entries-B-1yUdcH.js} +406 -407
- package/dist/{feedback-button-Bo7swAlq.js → feedback-button-BqiR7zMO.js} +11 -11
- package/dist/{flowDiagram-HODETNUW-Brg9MA6i.js → flowDiagram-HODETNUW-aIFlAtOy.js} +2 -2
- package/dist/{formats-DbSvAPY-.js → formats-PzBQMUnr.js} +3 -3
- package/dist/{glide-data-editor-CSrAhQAR.js → glide-data-editor-B7dguvqk.js} +14 -16
- package/dist/{glide-utils-BSw3SSHF.js → glide-utils-BSQr4d90.js} +51 -59
- package/dist/{globals-CQZo8Fcj.js → globals-CYykXfGI.js} +3 -3
- package/dist/{html-to-image-Bc2AvXHG.js → html-to-image-DC_pmZLs.js} +546 -548
- package/dist/{infoDiagram-27XIBGKW-E9e403YT.js → infoDiagram-27XIBGKW-D6YHLeCP.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-C-RlYDRo.js → ishikawaDiagram-5VMMS53U-D1ZvprOX.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CLFl_uqK.js → kanban-definition-UXKFOSKX-Bna_3BMr.js} +1 -1
- package/dist/{loader-BVEtQ4F3.js → loader-DZvtzQoD.js} +1 -1
- package/dist/main.js +6772 -6771
- package/dist/{mermaid-rd09e1JL.js → mermaid-A36unKXJ.js} +50 -50
- package/dist/{mindmap-definition-YA3MSWOX-BQ8TWtyB.js → mindmap-definition-YA3MSWOX-CyVCs__G.js} +1 -1
- package/dist/{number-overlay-editor-noy9X-05.js → number-overlay-editor-C4u7SJAJ.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-CN8UHFPi.js → pegDiagram-XKGWAZYB-9hNBD2wZ.js} +2 -2
- package/dist/{pencil-DcT8AE0h.js → pencil-DYUlSKxZ.js} +1 -1
- package/dist/{pieDiagram-E7YTZNPT-DGuJ0EBd.js → pieDiagram-E7YTZNPT-8tr-NYo7.js} +1 -1
- package/dist/{process-output-qecD7Dk8.js → process-output-DYu84VfZ.js} +4 -4
- package/dist/{railroadDiagram-O6MQD6OU-BPDA8wy8.js → railroadDiagram-O6MQD6OU-Dzg1dyyp.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-ChzLGbGW.js → requirementDiagram-BXWQKSXE-B7sNqwGG.js} +1 -1
- package/dist/{reveal-component-C6H7NzHW.js → reveal-component-DGi-irHV.js} +589 -589
- package/dist/{select-OxgozOC8.js → select-DsoP1qe8.js} +290 -290
- package/dist/{sequenceDiagram-WJ2MYXX4-DFX-bE6O.js → sequenceDiagram-WJ2MYXX4-oP9shbaG.js} +1 -1
- package/dist/{spec-oJn0RhpQ.js → spec-BZMDxAu7.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-BR7cI3wK.js → stateDiagram-D77RDMKH-CjCv4ogv.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-oGqB5UDI.js → stateDiagram-v2-MP3YSRHH-BW9X162a.js} +2 -2
- package/dist/{static-state-CuV6Pc3a.js → static-state-D-Q-Ajaa.js} +8 -8
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-DwSZl1yC.js → swimlanes-42K2YHIH-Ckfs9Wrn.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js → swimlanesDiagram-VR7AAH4N-DpTNG9YP.js} +2 -2
- package/dist/{swiper-component-Dvj-goaD.js → swiper-component-DQbK_ZWm.js} +7 -7
- package/dist/{timeline-definition-24CTP7MA-DMmP-G2O.js → timeline-definition-24CTP7MA-BXaBlV6U.js} +1 -1
- package/dist/{toast-DbBHmxmB.js → toast-BhjIy3Tq.js} +158 -158
- package/dist/{tooltip-BfcDQd2j.js → tooltip-DMMiOn2f.js} +64 -64
- package/dist/use-restore-focus-CosX7-gx.js +7568 -0
- package/dist/{useAsyncData-BYIAoMqn.js → useAsyncData-Ci4AvJff.js} +4 -4
- package/dist/{useDateFormatter-ButWkdcK.js → useDateFormatter-CPlava52.js} +2 -2
- package/dist/{useDeepCompareMemoize-Bz0c5GN6.js → useDeepCompareMemoize-NaaWoFlX.js} +1 -1
- package/dist/useEvent-BRH-aCqi.js +19 -0
- package/dist/{useIframeCapabilities-BIolGsF1.js → useIframeCapabilities-BXhDDoCG.js} +1 -1
- package/dist/{useLifecycle-JjxCC7Vu.js → useLifecycle-SniivYmC.js} +2 -2
- package/dist/{vega-component-rxIxAAh9.js → vega-component-DIxaDQNs.js} +40 -40
- package/dist/{vennDiagram-4TSXK5OY-Db8GtH1G.js → vennDiagram-4TSXK5OY-C-hQ4iNJ.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DmIiC3FU.js → wardleyDiagram-VM6X3IG4-C2MQ_Umv.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DS9OPrNq.js → xychartDiagram-S5SC5T6Z-BtTTBn7Q.js} +1 -1
- package/package.json +1 -1
- package/src/core/islands/components/__tests__/web-components.test.tsx +63 -0
- package/src/core/islands/components/web-components.tsx +11 -0
- package/src/css/globals.css +6 -2
- package/src/plugins/impl/DataEditorPlugin.tsx +0 -4
- package/src/plugins/impl/data-editor/__tests__/column-types.test.ts +5 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +9 -5
- package/src/plugins/impl/data-editor/column-types.ts +3 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +10 -32
- package/src/plugins/impl/data-editor/types.ts +0 -2
- package/src/theme/__tests__/islands-theme.test.tsx +276 -0
- package/src/theme/namespace.tsx +4 -3
- package/src/theme/useTheme.ts +71 -41
- package/dist/schemas-CdscWNy_.js +0 -4747
- package/dist/use-restore-focus-CmflVLWJ.js +0 -1672
- package/dist/useTheme-DPLobSFs.js +0 -339
- package/dist/utils-EBklsC5R.js +0 -813
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { createStore } from "jotai";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { JsonOutput } from "@/components/editor/output/JsonOutput";
|
|
7
|
+
import {
|
|
8
|
+
marimoDarkTheme,
|
|
9
|
+
marimoLightTheme,
|
|
10
|
+
} from "@/components/editor/output/json-output/themes";
|
|
11
|
+
import { userConfigAtom } from "@/core/config/config";
|
|
12
|
+
import { defaultUserConfig } from "@/core/config/config-schema";
|
|
13
|
+
import { StyleNamespace } from "../namespace";
|
|
14
|
+
import { resolvedThemeAtom } from "../useTheme";
|
|
15
|
+
|
|
16
|
+
let store: ReturnType<typeof createStore>;
|
|
17
|
+
let unsubscribe: (() => void) | undefined;
|
|
18
|
+
let media: EventTarget & { matches: boolean };
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.stubEnv("VITE_MARIMO_ISLANDS", "true");
|
|
22
|
+
media = Object.assign(new EventTarget(), { matches: false });
|
|
23
|
+
vi.stubGlobal(
|
|
24
|
+
"matchMedia",
|
|
25
|
+
vi.fn(() => media),
|
|
26
|
+
);
|
|
27
|
+
store = createStore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
stopObserving();
|
|
32
|
+
for (const element of [document.body, document.documentElement]) {
|
|
33
|
+
for (const attribute of [
|
|
34
|
+
"class",
|
|
35
|
+
"style",
|
|
36
|
+
"data-theme",
|
|
37
|
+
"data-mode",
|
|
38
|
+
"data-vscode-theme-kind",
|
|
39
|
+
]) {
|
|
40
|
+
element.removeAttribute(attribute);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
vi.unstubAllEnvs();
|
|
44
|
+
vi.unstubAllGlobals();
|
|
45
|
+
vi.restoreAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function subscribe() {
|
|
49
|
+
unsubscribe = store.sub(resolvedThemeAtom, vi.fn());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stopObserving() {
|
|
53
|
+
unsubscribe?.();
|
|
54
|
+
unsubscribe = undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getTheme() {
|
|
58
|
+
return store.get(resolvedThemeAtom);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setSystemTheme(matches: boolean) {
|
|
62
|
+
media.matches = matches;
|
|
63
|
+
media.dispatchEvent(new Event("change"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("islands host theme", () => {
|
|
67
|
+
it("defaults to light on a transparent page even if the system is dark", () => {
|
|
68
|
+
media.matches = true;
|
|
69
|
+
subscribe();
|
|
70
|
+
expect(getTheme()).toBe("light");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it.each(["body", "html"])(
|
|
74
|
+
"follows %s theme attributes in both directions",
|
|
75
|
+
async (tag) => {
|
|
76
|
+
const host = document.querySelector(tag)!;
|
|
77
|
+
subscribe();
|
|
78
|
+
for (const [attribute, dark, light] of [
|
|
79
|
+
["class", "dark", "light"],
|
|
80
|
+
["class", "dark-mode", "light-mode"],
|
|
81
|
+
["class", "dark-theme", "light-theme"],
|
|
82
|
+
["data-theme", "dark", "light"],
|
|
83
|
+
["data-mode", "dark", "light"],
|
|
84
|
+
]) {
|
|
85
|
+
host.setAttribute(attribute, dark);
|
|
86
|
+
await expect.poll(getTheme).toBe("dark");
|
|
87
|
+
host.setAttribute(attribute, light);
|
|
88
|
+
await expect.poll(getTheme).toBe("light");
|
|
89
|
+
host.removeAttribute(attribute);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it("reads the initial host theme and follows color-scheme changes", async () => {
|
|
95
|
+
document.body.style.colorScheme = "dark";
|
|
96
|
+
subscribe();
|
|
97
|
+
expect(getTheme()).toBe("dark");
|
|
98
|
+
document.body.style.colorScheme = "light";
|
|
99
|
+
await expect.poll(getTheme).toBe("light");
|
|
100
|
+
document.body.style.colorScheme = "dark";
|
|
101
|
+
await expect.poll(getTheme).toBe("dark");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("uses the background when color-scheme is normal", async () => {
|
|
105
|
+
document.body.style.colorScheme = "normal";
|
|
106
|
+
document.body.style.backgroundColor = "rgb(30, 30, 30)";
|
|
107
|
+
subscribe();
|
|
108
|
+
expect(getTheme()).toBe("dark");
|
|
109
|
+
document.body.style.backgroundColor = "white";
|
|
110
|
+
await expect.poll(getTheme).toBe("light");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("resolves a host supporting both color schemes when the system changes", async () => {
|
|
114
|
+
document.body.style.colorScheme = "light dark";
|
|
115
|
+
subscribe();
|
|
116
|
+
expect(getTheme()).toBe("light");
|
|
117
|
+
for (const matches of [true, false]) {
|
|
118
|
+
setSystemTheme(matches);
|
|
119
|
+
await expect.poll(getTheme).toBe(matches ? "dark" : "light");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("shares a subscription, skips unchanged themes, and cleans up on unmount", async () => {
|
|
124
|
+
const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect");
|
|
125
|
+
const removeListener = vi.spyOn(media, "removeEventListener");
|
|
126
|
+
const listener = vi.fn();
|
|
127
|
+
subscribe();
|
|
128
|
+
const unsubscribeSecond = store.sub(resolvedThemeAtom, listener);
|
|
129
|
+
document.body.classList.add("unrelated");
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
131
|
+
expect(listener).not.toHaveBeenCalled();
|
|
132
|
+
unsubscribeSecond();
|
|
133
|
+
expect(disconnect).not.toHaveBeenCalled();
|
|
134
|
+
stopObserving();
|
|
135
|
+
expect(disconnect).toHaveBeenCalledOnce();
|
|
136
|
+
expect(removeListener).toHaveBeenCalledWith("change", expect.any(Function));
|
|
137
|
+
document.body.classList.add("dark");
|
|
138
|
+
subscribe();
|
|
139
|
+
expect(getTheme()).toBe("dark");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("ignores unrecognized theme attributes and transparent body backgrounds", async () => {
|
|
143
|
+
document.body.dataset.theme = "custom-theme";
|
|
144
|
+
document.body.dataset.mode = "auto";
|
|
145
|
+
document.body.style.backgroundColor = "rgba(0, 0, 0, 0)";
|
|
146
|
+
document.documentElement.style.backgroundColor = "white";
|
|
147
|
+
subscribe();
|
|
148
|
+
expect(getTheme()).toBe("light");
|
|
149
|
+
document.documentElement.style.backgroundColor = "black";
|
|
150
|
+
await expect.poll(getTheme).toBe("dark");
|
|
151
|
+
document.documentElement.style.backgroundColor = "transparent";
|
|
152
|
+
await expect.poll(getTheme).toBe("light");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("falls back after a theme attribute is removed", async () => {
|
|
156
|
+
document.body.dataset.theme = "dark";
|
|
157
|
+
document.body.style.colorScheme = "light";
|
|
158
|
+
subscribe();
|
|
159
|
+
expect(getTheme()).toBe("dark");
|
|
160
|
+
delete document.body.dataset.theme;
|
|
161
|
+
await expect.poll(getTheme).toBe("light");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("continues to observe the host without matchMedia", async () => {
|
|
165
|
+
vi.stubGlobal("matchMedia", undefined);
|
|
166
|
+
document.body.style.colorScheme = "light dark";
|
|
167
|
+
subscribe();
|
|
168
|
+
expect(getTheme()).toBe("light");
|
|
169
|
+
document.body.style.colorScheme = "only dark";
|
|
170
|
+
await expect.poll(getTheme).toBe("dark");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it.each(["light", "dark"] as const)(
|
|
174
|
+
"does not override an explicit %s host when system preferences change",
|
|
175
|
+
(theme) => {
|
|
176
|
+
document.body.style.colorScheme = theme;
|
|
177
|
+
subscribe();
|
|
178
|
+
for (const matches of [true, false]) {
|
|
179
|
+
setSystemTheme(matches);
|
|
180
|
+
expect(getTheme()).toBe(theme);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
it("re-reads host CSS when a media query changes its color-scheme", () => {
|
|
186
|
+
const computedStyle = window.getComputedStyle(document.body);
|
|
187
|
+
const scheme = vi.spyOn(computedStyle, "colorScheme", "get");
|
|
188
|
+
vi.spyOn(window, "getComputedStyle").mockReturnValue(computedStyle);
|
|
189
|
+
scheme.mockReturnValue("light");
|
|
190
|
+
subscribe();
|
|
191
|
+
expect(getTheme()).toBe("light");
|
|
192
|
+
scheme.mockReturnValue("dark");
|
|
193
|
+
media.dispatchEvent(new Event("change"));
|
|
194
|
+
expect(getTheme()).toBe("dark");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("discards queued mutations and media events after the last unsubscribe", async () => {
|
|
198
|
+
const readStyle = vi.spyOn(window, "getComputedStyle");
|
|
199
|
+
subscribe();
|
|
200
|
+
document.body.style.colorScheme = "dark";
|
|
201
|
+
stopObserving();
|
|
202
|
+
readStyle.mockClear();
|
|
203
|
+
media.dispatchEvent(new Event("change"));
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
205
|
+
expect(readStyle).not.toHaveBeenCalled();
|
|
206
|
+
subscribe();
|
|
207
|
+
expect(getTheme()).toBe("dark");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("keeps the VS Code override and returns to the host after it is removed", async () => {
|
|
211
|
+
document.body.classList.add("dark");
|
|
212
|
+
const { container, unmount } = render(
|
|
213
|
+
<StyleNamespace>Widget</StyleNamespace>,
|
|
214
|
+
);
|
|
215
|
+
for (const [kind, theme] of [
|
|
216
|
+
["vscode-light", "light"],
|
|
217
|
+
["vscode-high-contrast", "dark"],
|
|
218
|
+
["vscode-light", "light"],
|
|
219
|
+
]) {
|
|
220
|
+
document.body.dataset.vscodeThemeKind = kind;
|
|
221
|
+
await waitFor(() => expect(container.firstChild).toHaveClass(theme));
|
|
222
|
+
}
|
|
223
|
+
delete document.body.dataset.vscodeThemeKind;
|
|
224
|
+
await waitFor(() => expect(container.firstChild).toHaveClass("dark"));
|
|
225
|
+
unmount();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does not subscribe to host themes outside Islands", async () => {
|
|
229
|
+
vi.stubEnv("VITE_MARIMO_ISLANDS", "false");
|
|
230
|
+
const config = defaultUserConfig();
|
|
231
|
+
store.set(userConfigAtom, {
|
|
232
|
+
...config,
|
|
233
|
+
display: { ...config.display, theme: "light" },
|
|
234
|
+
});
|
|
235
|
+
const observe = vi.spyOn(MutationObserver.prototype, "observe");
|
|
236
|
+
const addListener = vi.spyOn(media, "addEventListener");
|
|
237
|
+
subscribe();
|
|
238
|
+
document.body.classList.add("dark");
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
240
|
+
expect(getTheme()).toBe("light");
|
|
241
|
+
expect(observe).not.toHaveBeenCalled();
|
|
242
|
+
expect(addListener).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("updates the namespace root used by shadow DOM widgets", async () => {
|
|
246
|
+
const { container } = render(<StyleNamespace>Widget</StyleNamespace>);
|
|
247
|
+
expect(container.firstChild).toHaveClass("marimo", "light");
|
|
248
|
+
document.body.classList.add("dark");
|
|
249
|
+
await waitFor(() =>
|
|
250
|
+
expect(container.firstChild).toHaveClass("marimo", "dark"),
|
|
251
|
+
);
|
|
252
|
+
document.body.classList.remove("dark");
|
|
253
|
+
await waitFor(() =>
|
|
254
|
+
expect(container.firstChild).toHaveClass("marimo", "light"),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("updates the dictionary viewer palette without remounting", async () => {
|
|
259
|
+
const { container } = render(
|
|
260
|
+
<JsonOutput data={{ relu: 0, note: "text" }} />,
|
|
261
|
+
);
|
|
262
|
+
const viewer = container.querySelector<HTMLElement>(
|
|
263
|
+
".w-json-view-container",
|
|
264
|
+
);
|
|
265
|
+
expect(viewer).not.toBeNull();
|
|
266
|
+
for (const theme of ["dark", "light"] as const) {
|
|
267
|
+
document.body.dataset.theme = theme;
|
|
268
|
+
const palette = theme === "dark" ? marimoDarkTheme : marimoLightTheme;
|
|
269
|
+
await waitFor(() =>
|
|
270
|
+
expect(viewer?.style.getPropertyValue("--w-rjv-color")).toBe(
|
|
271
|
+
palette["--w-rjv-color"],
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/theme/namespace.tsx
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import React, { type PropsWithChildren } from "react";
|
|
3
|
+
import { useTheme } from "./useTheme";
|
|
3
4
|
|
|
4
5
|
const style = {
|
|
5
6
|
display: "contents",
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
* so that the styles are scoped to the DOM subtree.
|
|
10
|
+
* Scopes styles and applies the theme before descendants inherit text colors.
|
|
11
11
|
*/
|
|
12
12
|
export const StyleNamespace = React.forwardRef<
|
|
13
13
|
HTMLDivElement,
|
|
14
14
|
PropsWithChildren<{}>
|
|
15
15
|
>(({ children }, ref) => {
|
|
16
|
+
const { theme } = useTheme();
|
|
16
17
|
return (
|
|
17
|
-
<div ref={ref} className=
|
|
18
|
+
<div ref={ref} className={`marimo ${theme}`} style={style}>
|
|
18
19
|
{children}
|
|
19
20
|
</div>
|
|
20
21
|
);
|
package/src/theme/useTheme.ts
CHANGED
|
@@ -21,61 +21,92 @@ function themeFromQueryParam(): Theme | undefined {
|
|
|
21
21
|
return THEMES.includes(value as Theme) ? (value as Theme) : undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// If it has a dark mode class on the body, use dark mode.
|
|
28
|
-
if (
|
|
29
|
-
document.body.classList.contains("dark-mode") ||
|
|
30
|
-
document.body.classList.contains("dark")
|
|
31
|
-
) {
|
|
32
|
-
return "dark";
|
|
33
|
-
}
|
|
24
|
+
function getHostElements() {
|
|
25
|
+
return [document.body, document.documentElement];
|
|
26
|
+
}
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
) {
|
|
41
|
-
return "dark";
|
|
42
|
-
}
|
|
28
|
+
function getColorSchemeMedia() {
|
|
29
|
+
return typeof window === "undefined"
|
|
30
|
+
? undefined
|
|
31
|
+
: window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
32
|
+
}
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
/** Body markers override HTML markers. CSS provides the fallback. */
|
|
35
|
+
function getHostTheme(
|
|
36
|
+
prefersDark = getColorSchemeMedia()?.matches,
|
|
37
|
+
): ResolvedTheme {
|
|
38
|
+
const elements = getHostElements();
|
|
39
|
+
for (const element of elements) {
|
|
40
|
+
for (const theme of ["dark", "light"] as const) {
|
|
41
|
+
if (
|
|
42
|
+
element.classList.contains(theme) ||
|
|
43
|
+
element.classList.contains(`${theme}-mode`) ||
|
|
44
|
+
element.classList.contains(`${theme}-theme`) ||
|
|
45
|
+
element.dataset.theme === theme ||
|
|
46
|
+
element.dataset.mode === theme
|
|
47
|
+
) {
|
|
48
|
+
return theme;
|
|
49
|
+
}
|
|
49
50
|
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const styles = elements.map((element) => window.getComputedStyle(element));
|
|
54
|
+
const colorScheme = styles[0].colorScheme.split(/\s+/);
|
|
55
|
+
if (colorScheme.includes("dark")) {
|
|
56
|
+
return !colorScheme.includes("light") || prefersDark ? "dark" : "light";
|
|
57
|
+
}
|
|
58
|
+
if (colorScheme.includes("light")) {
|
|
59
|
+
return "light";
|
|
60
|
+
}
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const rgb =
|
|
54
|
-
if (rgb) {
|
|
55
|
-
const [r, g, b] = rgb
|
|
56
|
-
|
|
57
|
-
return brightness < 128 ? "dark" : "light";
|
|
62
|
+
// Transparent backgrounds do not indicate a dark host page.
|
|
63
|
+
for (const style of styles) {
|
|
64
|
+
const rgb = style.backgroundColor.match(/[\d.]+/g)?.map(Number);
|
|
65
|
+
if (rgb && (rgb.length === 3 || rgb[3] > 0)) {
|
|
66
|
+
const [r, g, b] = rgb;
|
|
67
|
+
return (r * 299 + g * 587 + b * 114) / 1000 < 128 ? "dark" : "light";
|
|
58
68
|
}
|
|
69
|
+
}
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
// A dark system preference does not imply a dark host.
|
|
72
|
+
return "light";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hostThemeAtom = atom<ResolvedTheme | undefined>(undefined);
|
|
76
|
+
hostThemeAtom.onMount = (setTheme) => {
|
|
77
|
+
const media = getColorSchemeMedia();
|
|
78
|
+
const updateTheme = () => setTheme(getHostTheme(media?.matches));
|
|
79
|
+
const observer = new MutationObserver(updateTheme);
|
|
80
|
+
for (const element of getHostElements()) {
|
|
81
|
+
observer.observe(element, {
|
|
82
|
+
attributes: true,
|
|
83
|
+
attributeFilter: ["class", "data-theme", "data-mode", "style"],
|
|
84
|
+
});
|
|
63
85
|
}
|
|
86
|
+
media?.addEventListener("change", updateTheme);
|
|
87
|
+
updateTheme();
|
|
88
|
+
return () => {
|
|
89
|
+
observer.disconnect();
|
|
90
|
+
media?.removeEventListener("change", updateTheme);
|
|
91
|
+
// A later mount must read the current host theme.
|
|
92
|
+
setTheme(undefined);
|
|
93
|
+
};
|
|
94
|
+
};
|
|
64
95
|
|
|
96
|
+
const themeAtom = atom((get) => {
|
|
97
|
+
if (isIslands()) {
|
|
98
|
+
return get(hostThemeAtom) ?? getHostTheme();
|
|
99
|
+
}
|
|
65
100
|
return themeFromQueryParam() ?? get(resolvedMarimoConfigAtom).display.theme;
|
|
66
101
|
});
|
|
67
102
|
|
|
68
103
|
const prefersDarkModeAtom = atom(false);
|
|
69
104
|
|
|
70
105
|
function setupThemeListener(): void {
|
|
71
|
-
|
|
106
|
+
const media = getColorSchemeMedia();
|
|
107
|
+
if (!media) {
|
|
72
108
|
return;
|
|
73
109
|
}
|
|
74
|
-
if (!window.matchMedia) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
79
110
|
store.set(prefersDarkModeAtom, media.matches);
|
|
80
111
|
media.addEventListener("change", (e) => {
|
|
81
112
|
store.set(prefersDarkModeAtom, e.matches);
|
|
@@ -126,8 +157,7 @@ export const resolvedThemeAtom = atom((get) => {
|
|
|
126
157
|
});
|
|
127
158
|
|
|
128
159
|
/**
|
|
129
|
-
*
|
|
130
|
-
* This is stored in the user config.
|
|
160
|
+
* Returns the resolved theme from VS Code, the Islands host, or notebook settings.
|
|
131
161
|
*/
|
|
132
162
|
export function useTheme(): { theme: ResolvedTheme } {
|
|
133
163
|
const theme = useAtomValue(resolvedThemeAtom, { store });
|