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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion components/blocks/autofunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { publicRuntimeConfig } = getConfig();

import styles from "./autofunction.module.css";
import { looksLikeVersionAndPlatformString } from "../../lib/next/utils";
import { getThemedUrl, getThemeFromDOM } from "../../lib/next/ThemeContext";

const LATEST_VERSION = publicRuntimeConfig.LATEST_VERSION;
const DEFAULT_VERSION = publicRuntimeConfig.DEFAULT_VERSION;
Expand Down Expand Up @@ -50,19 +51,22 @@ const Autofunction = ({
}, [streamlitFunction]);

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

const currentTheme = getThemeFromDOM();

iframes.forEach((iframe) => {
const parent = iframe.parentElement;
const newFrame = iframe.cloneNode();

newFrame.src = "";
newFrame.classList.add("new");
newFrame.src = iframe.src;
newFrame.src = getThemedUrl(iframe.src, currentTheme);

parent.replaceChild(newFrame, iframe);
});
Expand Down
17 changes: 16 additions & 1 deletion components/blocks/cloud.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, { useEffect, useRef } from "react";
import React from "react";
import classNames from "classnames";
import {
useThemeContextSafe,
getThemeFromDOM,
} from "../../lib/next/ThemeContext";

// Arguments:
//
Expand Down Expand Up @@ -34,6 +38,12 @@ import classNames from "classnames";
// -> https://foo.streamlit.app/bar/?embed=true&embed_options=show_padding&embed_options=show_colored_line
//
const Cloud = ({ name, path, query, height, domain, stylePlaceholder }) => {
// Try to get theme from context, fall back to DOM reading
// Context may not be available when rendered via ReactDOMServer.renderToString in table.js
const themeContext = useThemeContextSafe();
const theme = themeContext?.theme ?? getThemeFromDOM();
const themeEmbedOption = `embed_options=${theme}_theme`;

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

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

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

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

// Add theme parameter to embed query string
embedQueryStr += `&${themeEmbedOption}`;

if (!height) height = "10rem";

const style = stylePlaceholder
Expand Down
14 changes: 9 additions & 5 deletions components/blocks/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,23 +122,27 @@ const Table = ({
);
};

// Regex capturing React components:
// Regex capturing React components in the RST docs.
const CLOUD_RE = new RegExp(
[
"<Cloud ",
'name="([^<>]*)" ',
'path="([^<>]*)" ',
'query="([^<>]*)" ',
'stylePlaceholder="([^<>]*)" ',
'name="([^<>]*)" ', // $1
'path="([^<>]*)" ', // $2
'query="([^<>]*)" ', // $3
'stylePlaceholder="([^<>]*)" ', // $4
"\\/>",
].join(""),
"g",
);

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

// Replace "<Cloud>" string with code for <Cloud> component,
// except the "$x" placeholders should be filled in with values
// captures by the RegEx.
function insertCloud(htmlStr) {
return htmlStr.replace(CLOUD_RE, CLOUD_HTML);
}
Expand Down
39 changes: 7 additions & 32 deletions components/utilities/themeToggle.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
import React, { useState, useEffect } from "react";
import React from "react";
import classNames from "classnames";
import styles from "./themeToggle.module.css";
import { useThemeContext } from "../../lib/next/ThemeContext";

const ThemeToggle = () => {
const [activeTheme, setActiveTheme] = useState("light");
let inactiveTheme;
inactiveTheme = activeTheme === "light" ? "dark" : "light";
const { theme, setTheme } = useThemeContext();
const inactiveTheme = theme === "light" ? "dark" : "light";

const getUserPreference = () => {
if (window.localStorage.getItem("theme")) {
return window.localStorage.getItem("theme");
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};

const changeTailwindTheme = (theme) => {
inactiveTheme = theme === "light" ? "dark" : "light";
document.documentElement.classList.add(theme);
document.documentElement.classList.remove(inactiveTheme);
setActiveTheme(theme);
localStorage.setItem("theme", theme);
const toggleTheme = () => {
setTheme(inactiveTheme);
};

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

useEffect(() => {
if (getUserPreference() === "dark") {
changeTailwindTheme("dark");
} else {
changeTailwindTheme("light");
}
}, [activeTheme]);

return (
<React.Fragment>
<button
type="button"
onClick={
activeTheme === "light"
? () => changeTailwindTheme("dark")
: () => changeTailwindTheme("light")
}
onClick={toggleTheme}
onMouseOver={showTooltip}
onMouseOut={hideTooltip}
className={styles.Container}
Expand Down
134 changes: 134 additions & 0 deletions lib/next/ThemeContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
} from "react";

const ThemeContext = createContext();

/**
* Updates all Streamlit Cloud iframes on the page with the new theme.
* This is extracted as a shared function so it can be called from multiple places.
*/
export function updateIframeThemes(theme) {
if (typeof document === "undefined") return;

const iframes = document.querySelectorAll('iframe[src*="streamlit.app"]');
iframes.forEach((iframe) => {
iframe.src = getThemedUrl(iframe.src, theme);
});
}

/**
* Gets the user's theme preference from localStorage or system preference.
* Returns "light" as default for SSR.
*/
function getUserPreference() {
if (typeof window === "undefined") {
return "light";
}
if (window.localStorage.getItem("theme")) {
return window.localStorage.getItem("theme");
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}

export function ThemeContextProvider({ children }) {
// Initialize with "light" for SSR, will be updated on mount
const [theme, setThemeState] = useState("light");
const [isInitialized, setIsInitialized] = useState(false);

// Apply theme to DOM and localStorage
const applyTheme = useCallback((newTheme) => {
if (typeof document === "undefined") return;

const inactiveTheme = newTheme === "light" ? "dark" : "light";
document.documentElement.classList.add(newTheme);
document.documentElement.classList.remove(inactiveTheme);
localStorage.setItem("theme", newTheme);
}, []);

// Set theme and update everything
const setTheme = useCallback(
(newTheme) => {
setThemeState(newTheme);
applyTheme(newTheme);
updateIframeThemes(newTheme);
},
[applyTheme],
);

// Initialize theme on mount (client-side only)
useEffect(() => {
const preferredTheme = getUserPreference();
setThemeState(preferredTheme);
applyTheme(preferredTheme);
setIsInitialized(true);
}, [applyTheme]);

return (
<ThemeContext.Provider value={{ theme, setTheme, isInitialized }}>
{children}
</ThemeContext.Provider>
);
}

export function useThemeContext() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error(
"useThemeContext must be used within a ThemeContextProvider",
);
}
return context;
}

/**
* Safe version of useThemeContext that returns null if not within a provider.
* Useful for components that may be rendered outside the provider (e.g., SSR).
*/
export function useThemeContextSafe() {
return useContext(ThemeContext);
}

/**
* Gets the current theme from DOM (for use in non-React contexts or SSR fallback).
* Returns "light" as default if document is not available.
*/
export function getThemeFromDOM() {
if (typeof document !== "undefined") {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
return "light";
}

/**
* Adds a "light" or "dark" theme to a given Streamlit Cloud URL.
*/
export function getThemedUrl(url, theme) {
const themedUrl = new URL(url);
addThemeToSearchParams(themedUrl.searchParams, theme);
return themedUrl.toString();
}

export function addThemeToSearchParams(searchParams, theme) {
const existingEmbedOptions = searchParams.getAll("embed_options");

const nonThemeOptions = existingEmbedOptions.filter(
(option) => option !== "light_theme" && option !== "dark_theme",
);

// Clear all embed_options and re-add the non-theme ones
searchParams.delete("embed_options");
nonThemeOptions.forEach((option) =>
searchParams.append("embed_options", option),
);

searchParams.append("embed_options", `${theme}_theme`);
}
17 changes: 10 additions & 7 deletions pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import NProgress from "nprogress";
import { useEffect } from "react";

import { VersionContextProvider } from "../lib/next/VersionContext";
import { ThemeContextProvider } from "../lib/next/ThemeContext";

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

return (
<VersionContextProvider
versionFromSlug={pageProps.versionFromSlug}
platformFromSlug={pageProps.platformFromSlug}
currentItem={pageProps.currentItem}
>
<Component {...pageProps} />
</VersionContextProvider>
<ThemeContextProvider>
<VersionContextProvider
versionFromSlug={pageProps.versionFromSlug}
platformFromSlug={pageProps.platformFromSlug}
currentItem={pageProps.currentItem}
>
<Component {...pageProps} />
</VersionContextProvider>
</ThemeContextProvider>
);
}

Expand Down