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 290f9cc

Browse files
Set embedded app theme to match the site theme (#1387)
* Load site with apps to match the current theme * Get current theme from DOM * Reload Cloud components on theme change * Get embed string directly * Add theme context * Reuse code * Add comments --------- Co-authored-by: Thiago Teixeira <[email protected]>
1 parent 7a6ba35 commit 290f9cc

File tree

6 files changed

+181
-46
lines changed

6 files changed

+181
-46
lines changed

components/blocks/autofunction.js

Lines changed: 5 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 { getThemedUrl, getThemeFromDOM } from "../../lib/next/ThemeContext";
2122

2223
const LATEST_VERSION = publicRuntimeConfig.LATEST_VERSION;
2324
const DEFAULT_VERSION = publicRuntimeConfig.DEFAULT_VERSION;
@@ -50,19 +51,22 @@ 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+
const currentTheme = getThemeFromDOM();
62+
5963
iframes.forEach((iframe) => {
6064
const parent = iframe.parentElement;
6165
const newFrame = iframe.cloneNode();
6266

6367
newFrame.src = "";
6468
newFrame.classList.add("new");
65-
newFrame.src = iframe.src;
69+
newFrame.src = getThemedUrl(iframe.src, currentTheme);
6670

6771
parent.replaceChild(newFrame, iframe);
6872
});

components/blocks/cloud.js

Lines changed: 16 additions & 1 deletion
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,6 +38,12 @@ 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 }) => {
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`;
46+
3747
if (!domain) domain = `${name}.streamlit.app`;
3848
if (domain.endsWith("/")) domain = domain.slice(0, -1);
3949

@@ -44,6 +54,8 @@ const Cloud = ({ name, path, query, height, domain, stylePlaceholder }) => {
4454
path = "";
4555
}
4656

57+
// We'll process the query param using string processing rather than URLSearchParams because
58+
// when the `query` is a placeholder $3 the "$" gets mangled by URLSearchParams.
4759
let normalQueryStr = "";
4860
let embedQueryStr = "";
4961

@@ -68,6 +80,9 @@ const Cloud = ({ name, path, query, height, domain, stylePlaceholder }) => {
6880
normalQueryStr = "&" + normalQueryParams.join("&");
6981
}
7082

83+
// Add theme parameter to embed query string
84+
embedQueryStr += `&${themeEmbedOption}`;
85+
7186
if (!height) height = "10rem";
7287

7388
const style = stylePlaceholder

components/blocks/table.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,23 +122,27 @@ const Table = ({
122122
);
123123
};
124124

125-
// Regex capturing React components:
125+
// Regex capturing React components in the RST docs.
126126
const CLOUD_RE = new RegExp(
127127
[
128128
"<Cloud ",
129-
'name="([^<>]*)" ',
130-
'path="([^<>]*)" ',
131-
'query="([^<>]*)" ',
132-
'stylePlaceholder="([^<>]*)" ',
129+
'name="([^<>]*)" ', // $1
130+
'path="([^<>]*)" ', // $2
131+
'query="([^<>]*)" ', // $3
132+
'stylePlaceholder="([^<>]*)" ', // $4
133133
"\\/>",
134134
].join(""),
135135
"g",
136136
);
137137

138+
// Render <Cloud> component using placeholders "$1", etc. to be filled in later.
138139
const CLOUD_HTML = ReactDOMServer.renderToString(
139140
<Cloud name="$1" path="$2" query="$3" stylePlaceholder="$4" />,
140141
);
141142

143+
// Replace "<Cloud>" string with code for <Cloud> component,
144+
// except the "$x" placeholders should be filled in with values
145+
// captures by the RegEx.
142146
function insertCloud(htmlStr) {
143147
return htmlStr.replace(CLOUD_RE, CLOUD_HTML);
144148
}

components/utilities/themeToggle.js

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +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);
10+
const toggleTheme = () => {
11+
setTheme(inactiveTheme);
2512
};
2613

2714
const showTooltip = () => {
@@ -32,23 +19,11 @@ const ThemeToggle = () => {
3219
document.getElementsByClassName(styles.Tooltip)[0].style.display = "none";
3320
};
3421

35-
useEffect(() => {
36-
if (getUserPreference() === "dark") {
37-
changeTailwindTheme("dark");
38-
} else {
39-
changeTailwindTheme("light");
40-
}
41-
}, [activeTheme]);
42-
4322
return (
4423
<React.Fragment>
4524
<button
4625
type="button"
47-
onClick={
48-
activeTheme === "light"
49-
? () => changeTailwindTheme("dark")
50-
: () => changeTailwindTheme("light")
51-
}
26+
onClick={toggleTheme}
5227
onMouseOver={showTooltip}
5328
onMouseOut={hideTooltip}
5429
className={styles.Container}

lib/next/ThemeContext.jsx

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

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)