WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit a2a8a40

Browse files
Add theme context
1 parent 2ed49ae commit a2a8a40

File tree

5 files changed

+183
-79
lines changed

5 files changed

+183
-79
lines changed

components/blocks/autofunction.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { publicRuntimeConfig } = getConfig();
1818

1919
import styles from "./autofunction.module.css";
2020
import { looksLikeVersionAndPlatformString } from "../../lib/next/utils";
21+
import { getThemeFromDOM } from "../../lib/next/ThemeContext";
2122

2223
const LATEST_VERSION = publicRuntimeConfig.LATEST_VERSION;
2324
const DEFAULT_VERSION = publicRuntimeConfig.DEFAULT_VERSION;
@@ -50,19 +51,43 @@ const Autofunction = ({
5051
}, [streamlitFunction]);
5152

5253
// Code to destroy and regenerate iframes on each new autofunction render.
54+
// Also updates the theme in iframe URLs to match the current site theme.
5355
const regenerateIframes = () => {
5456
const iframes = Array.prototype.slice.call(
5557
blockRef.current.getElementsByTagName("iframe"),
5658
);
5759
if (!iframes) return;
5860

61+
// Get current theme from DOM
62+
const currentTheme = getThemeFromDOM();
63+
5964
iframes.forEach((iframe) => {
6065
const parent = iframe.parentElement;
6166
const newFrame = iframe.cloneNode();
6267

68+
// Update the iframe src with the correct theme
69+
const url = new URL(iframe.src);
70+
71+
// Get all existing embed_options
72+
const existingEmbedOptions = url.searchParams.getAll("embed_options");
73+
74+
// Remove only theme-related embed_options (light_theme or dark_theme)
75+
const nonThemeOptions = existingEmbedOptions.filter(
76+
(option) => option !== "light_theme" && option !== "dark_theme",
77+
);
78+
79+
// Clear all embed_options and re-add the non-theme ones
80+
url.searchParams.delete("embed_options");
81+
nonThemeOptions.forEach((option) =>
82+
url.searchParams.append("embed_options", option),
83+
);
84+
85+
// Add current theme parameter
86+
url.searchParams.append("embed_options", `${currentTheme}_theme`);
87+
6388
newFrame.src = "";
6489
newFrame.classList.add("new");
65-
newFrame.src = iframe.src;
90+
newFrame.src = url.toString();
6691

6792
parent.replaceChild(newFrame, iframe);
6893
});

components/blocks/cloud.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import React, { useEffect, useRef } from "react";
1+
import React from "react";
22
import classNames from "classnames";
3+
import {
4+
useThemeContextSafe,
5+
getThemeFromDOM,
6+
} from "../../lib/next/ThemeContext";
37

48
// Arguments:
59
//
@@ -34,17 +38,11 @@ import classNames from "classnames";
3438
// -> https://foo.streamlit.app/bar/?embed=true&embed_options=show_padding&embed_options=show_colored_line
3539
//
3640
const Cloud = ({ name, path, query, height, domain, stylePlaceholder }) => {
37-
// Get the current theme embed option directly (with SSR safety)
38-
const getThemeEmbedOption = () => {
39-
if (typeof document !== "undefined") {
40-
return document.documentElement.classList.contains("dark")
41-
? "embed_options=dark_theme"
42-
: "embed_options=light_theme";
43-
}
44-
return "embed_options=light_theme"; // Default fallback for SSR
45-
};
46-
47-
const themeEmbedOption = getThemeEmbedOption();
41+
// Try to get theme from context, fall back to DOM reading
42+
// Context may not be available when rendered via ReactDOMServer.renderToString in table.js
43+
const themeContext = useThemeContextSafe();
44+
const theme = themeContext?.theme ?? getThemeFromDOM();
45+
const themeEmbedOption = `embed_options=${theme}_theme`;
4846

4947
if (!domain) domain = `${name}.streamlit.app`;
5048
if (domain.endsWith("/")) domain = domain.slice(0, -1);

components/utilities/themeToggle.js

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,14 @@
1-
import React, { useState, useEffect } from "react";
1+
import React from "react";
22
import classNames from "classnames";
33
import styles from "./themeToggle.module.css";
4+
import { useThemeContext } from "../../lib/next/ThemeContext";
45

56
const ThemeToggle = () => {
6-
const [activeTheme, setActiveTheme] = useState("light");
7-
let inactiveTheme;
8-
inactiveTheme = activeTheme === "light" ? "dark" : "light";
7+
const { theme, setTheme } = useThemeContext();
8+
const inactiveTheme = theme === "light" ? "dark" : "light";
99

10-
const getUserPreference = () => {
11-
if (window.localStorage.getItem("theme")) {
12-
return window.localStorage.getItem("theme");
13-
}
14-
return window.matchMedia("(prefers-color-scheme: dark)").matches
15-
? "dark"
16-
: "light";
17-
};
18-
19-
const changeTailwindTheme = (theme) => {
20-
inactiveTheme = theme === "light" ? "dark" : "light";
21-
document.documentElement.classList.add(theme);
22-
document.documentElement.classList.remove(inactiveTheme);
23-
setActiveTheme(theme);
24-
localStorage.setItem("theme", theme);
25-
26-
// Force reload all Cloud iframes on the current page
27-
const iframes = document.querySelectorAll('iframe[src*="streamlit.app"]');
28-
iframes.forEach((iframe) => {
29-
const currentSrc = iframe.src;
30-
const url = new URL(currentSrc);
31-
32-
// Get all existing embed_options
33-
const existingEmbedOptions = url.searchParams.getAll("embed_options");
34-
35-
// Remove only theme-related embed_options (light_theme or dark_theme)
36-
const nonThemeOptions = existingEmbedOptions.filter(
37-
(option) => option !== "light_theme" && option !== "dark_theme",
38-
);
39-
40-
// Clear all embed_options and re-add the non-theme ones
41-
url.searchParams.delete("embed_options");
42-
nonThemeOptions.forEach((option) =>
43-
url.searchParams.append("embed_options", option),
44-
);
45-
46-
// Add new theme parameter
47-
url.searchParams.append("embed_options", `${theme}_theme`);
48-
49-
// Force reload iframe with new theme
50-
iframe.src = url.toString();
51-
});
10+
const toggleTheme = () => {
11+
setTheme(inactiveTheme);
5212
};
5313

5414
const showTooltip = () => {
@@ -59,23 +19,11 @@ const ThemeToggle = () => {
5919
document.getElementsByClassName(styles.Tooltip)[0].style.display = "none";
6020
};
6121

62-
useEffect(() => {
63-
if (getUserPreference() === "dark") {
64-
changeTailwindTheme("dark");
65-
} else {
66-
changeTailwindTheme("light");
67-
}
68-
}, [activeTheme]);
69-
7022
return (
7123
<React.Fragment>
7224
<button
7325
type="button"
74-
onClick={
75-
activeTheme === "light"
76-
? () => changeTailwindTheme("dark")
77-
: () => changeTailwindTheme("light")
78-
}
26+
onClick={toggleTheme}
7927
onMouseOver={showTooltip}
8028
onMouseOut={hideTooltip}
8129
className={styles.Container}

lib/next/ThemeContext.jsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
useEffect,
6+
useCallback,
7+
} from "react";
8+
9+
const ThemeContext = createContext();
10+
11+
/**
12+
* Updates all Streamlit Cloud iframes on the page with the new theme.
13+
* This is extracted as a shared function so it can be called from multiple places.
14+
*/
15+
export function updateIframesTheme(theme) {
16+
if (typeof document === "undefined") return;
17+
18+
const iframes = document.querySelectorAll('iframe[src*="streamlit.app"]');
19+
iframes.forEach((iframe) => {
20+
const currentSrc = iframe.src;
21+
const url = new URL(currentSrc);
22+
23+
// Get all existing embed_options
24+
const existingEmbedOptions = url.searchParams.getAll("embed_options");
25+
26+
// Remove only theme-related embed_options (light_theme or dark_theme)
27+
const nonThemeOptions = existingEmbedOptions.filter(
28+
(option) => option !== "light_theme" && option !== "dark_theme",
29+
);
30+
31+
// Clear all embed_options and re-add the non-theme ones
32+
url.searchParams.delete("embed_options");
33+
nonThemeOptions.forEach((option) =>
34+
url.searchParams.append("embed_options", option),
35+
);
36+
37+
// Add new theme parameter
38+
url.searchParams.append("embed_options", `${theme}_theme`);
39+
40+
// Force reload iframe with new theme
41+
iframe.src = url.toString();
42+
});
43+
}
44+
45+
/**
46+
* Gets the user's theme preference from localStorage or system preference.
47+
* Returns "light" as default for SSR.
48+
*/
49+
function getUserPreference() {
50+
if (typeof window === "undefined") {
51+
return "light";
52+
}
53+
if (window.localStorage.getItem("theme")) {
54+
return window.localStorage.getItem("theme");
55+
}
56+
return window.matchMedia("(prefers-color-scheme: dark)").matches
57+
? "dark"
58+
: "light";
59+
}
60+
61+
export function ThemeContextProvider({ children }) {
62+
// Initialize with "light" for SSR, will be updated on mount
63+
const [theme, setThemeState] = useState("light");
64+
const [isInitialized, setIsInitialized] = useState(false);
65+
66+
// Apply theme to DOM and localStorage
67+
const applyTheme = useCallback((newTheme) => {
68+
if (typeof document === "undefined") return;
69+
70+
const inactiveTheme = newTheme === "light" ? "dark" : "light";
71+
document.documentElement.classList.add(newTheme);
72+
document.documentElement.classList.remove(inactiveTheme);
73+
localStorage.setItem("theme", newTheme);
74+
}, []);
75+
76+
// Set theme and update everything
77+
const setTheme = useCallback(
78+
(newTheme) => {
79+
setThemeState(newTheme);
80+
applyTheme(newTheme);
81+
updateIframesTheme(newTheme);
82+
},
83+
[applyTheme],
84+
);
85+
86+
// Initialize theme on mount (client-side only)
87+
useEffect(() => {
88+
const preferredTheme = getUserPreference();
89+
setThemeState(preferredTheme);
90+
applyTheme(preferredTheme);
91+
setIsInitialized(true);
92+
}, [applyTheme]);
93+
94+
return (
95+
<ThemeContext.Provider value={{ theme, setTheme, isInitialized }}>
96+
{children}
97+
</ThemeContext.Provider>
98+
);
99+
}
100+
101+
export function useThemeContext() {
102+
const context = useContext(ThemeContext);
103+
if (context === undefined) {
104+
throw new Error(
105+
"useThemeContext must be used within a ThemeContextProvider",
106+
);
107+
}
108+
return context;
109+
}
110+
111+
/**
112+
* Safe version of useThemeContext that returns null if not within a provider.
113+
* Useful for components that may be rendered outside the provider (e.g., SSR).
114+
*/
115+
export function useThemeContextSafe() {
116+
return useContext(ThemeContext);
117+
}
118+
119+
/**
120+
* Gets the current theme from DOM (for use in non-React contexts or SSR fallback).
121+
* Returns "light" as default if document is not available.
122+
*/
123+
export function getThemeFromDOM() {
124+
if (typeof document !== "undefined") {
125+
return document.documentElement.classList.contains("dark")
126+
? "dark"
127+
: "light";
128+
}
129+
return "light";
130+
}

pages/_app.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import NProgress from "nprogress";
1111
import { useEffect } from "react";
1212

1313
import { VersionContextProvider } from "../lib/next/VersionContext";
14+
import { ThemeContextProvider } from "../lib/next/ThemeContext";
1415

1516
Router.events.on("routeChangeStart", () => NProgress.start());
1617
Router.events.on("routeChangeComplete", () => {
@@ -29,13 +30,15 @@ function StreamlitDocs({ Component, pageProps }) {
2930
}, []);
3031

3132
return (
32-
<VersionContextProvider
33-
versionFromSlug={pageProps.versionFromSlug}
34-
platformFromSlug={pageProps.platformFromSlug}
35-
currentItem={pageProps.currentItem}
36-
>
37-
<Component {...pageProps} />
38-
</VersionContextProvider>
33+
<ThemeContextProvider>
34+
<VersionContextProvider
35+
versionFromSlug={pageProps.versionFromSlug}
36+
platformFromSlug={pageProps.platformFromSlug}
37+
currentItem={pageProps.currentItem}
38+
>
39+
<Component {...pageProps} />
40+
</VersionContextProvider>
41+
</ThemeContextProvider>
3942
);
4043
}
4144

0 commit comments

Comments
 (0)