diff --git a/components/blocks/autofunction.module.css b/components/blocks/autofunction.module.css index fcc258988..155ac3f70 100644 --- a/components/blocks/autofunction.module.css +++ b/components/blocks/autofunction.module.css @@ -63,16 +63,16 @@ } .Header { - @apply flex items-center px-3 py-1 bg-gray-20 text-gray-70 text-xs font-medium tracking-wide rounded-t-xl border-b border-gray-30; + @apply flex items-center px-3 py-1 bg-gray-10 text-gray-70 text-sm font-mono font-medium tracking-wide rounded-t-xl border-b border-gray-30; min-height: 2.5rem; } :global(.dark) .Header { - @apply bg-gray-90 text-gray-50 border-gray-80; + @apply bg-gray-90 text-gray-60 border-gray-80; } .Language { - @apply uppercase tracking-wider pr-4; + @apply uppercase pr-4 font-mono; } .CodeBlockContainer pre, diff --git a/components/blocks/code.js b/components/blocks/code.js index e8efad6f7..a5038c395 100644 --- a/components/blocks/code.js +++ b/components/blocks/code.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import classNames from "classnames"; import Prism from "prismjs"; import "prismjs/plugins/line-numbers/prism-line-numbers"; @@ -12,6 +12,78 @@ import Image from "./image"; import styles from "./code.module.css"; +// Compress code with gzip and encode as base64 for Streamlit Playground +async function compressCodeForPlayground(code) { + const encoder = new TextEncoder(); + const data = encoder.encode(code); + + const cs = new CompressionStream("gzip"); + const writer = cs.writable.getWriter(); + writer.write(data); + writer.close(); + + const compressedChunks = []; + const reader = cs.readable.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + compressedChunks.push(value); + } + + // Combine all chunks into a single Uint8Array + const totalLength = compressedChunks.reduce( + (acc, chunk) => acc + chunk.length, + 0, + ); + const compressed = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of compressedChunks) { + compressed.set(chunk, offset); + offset += chunk.length; + } + + // Convert to URL-safe base64 (uses - and _ instead of + and /) + let binary = ""; + for (let i = 0; i < compressed.length; i++) { + binary += String.fromCharCode(compressed[i]); + } + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_"); +} + +// TryMeButton component +const TryMeButton = ({ code }) => { + const [playgroundUrl, setPlaygroundUrl] = useState(null); + + useEffect(() => { + async function generateUrl() { + if (code) { + const encoded = await compressCodeForPlayground(code.trim()); + setPlaygroundUrl( + `https://streamlit.io/playground?example=blank&code=${encoded}`, + ); + } + } + generateUrl(); + }, [code]); + + if (!playgroundUrl) return null; + + return ( + + Try it + + arrow_outward + + + ); +}; + // Initialize the cache for imported languages. const languageImports = new Map(); @@ -61,7 +133,8 @@ const Code = ({ lines, hideCopyButton = false, filename, - filenameOnly = true, + showAll = false, + try: tryIt = false, }) => { // Create a ref for the code element. const codeRef = useRef(null); @@ -127,7 +200,7 @@ const Code = ({ const langId = languageClass?.substring(9) || language || "python"; const displayLanguage = languageDisplayNames[langId] || langId; const showLanguage = - langId.toLowerCase() !== "none" && !(filenameOnly && filename); + langId.toLowerCase() !== "none" && (showAll || !filename); const Header = (
@@ -135,6 +208,7 @@ const Code = ({ {displayLanguage} )} {filename && {filename}} + {tryIt && }
); diff --git a/components/blocks/code.module.css b/components/blocks/code.module.css index 6f00a5e47..97b9716ed 100644 --- a/components/blocks/code.module.css +++ b/components/blocks/code.module.css @@ -4,20 +4,20 @@ } .Header { - @apply flex items-center px-3 py-1 bg-gray-20 text-gray-70 text-xs font-medium tracking-wide rounded-t-md border-b border-gray-30; + @apply flex items-center px-3 py-1 bg-gray-10 text-gray-70 text-sm font-mono font-medium tracking-wide rounded-t-xl border-b border-gray-30; min-height: 2.5rem; } :global(.dark) .Header { - @apply bg-gray-90 text-gray-50 border-gray-80; + @apply bg-gray-90 text-gray-60 border-gray-80; } .Language { - @apply uppercase tracking-wider pr-4; + @apply uppercase pr-4 font-mono; } .Filename { - @apply font-mono text-sm leading-6 pr-4; + @apply leading-6 pr-4; } .HasHeader { @@ -30,7 +30,7 @@ } .Pre { - @apply p-6 bg-gray-10 text-gray-80 font-medium rounded-md relative leading-relaxed; + @apply p-6 bg-gray-10 text-gray-80 font-medium rounded-xl relative leading-relaxed; } /* Dark mode background and text for Pre */ @@ -54,15 +54,25 @@ } .Container button::before { - @apply z-10 transition-all duration-75 hover:opacity-40; + @apply z-10 transition-all duration-75 hover:opacity-60; content: url("/clipboard.svg"); } .Container button span { - @apply absolute -top-0.5 right-10 text-gray-80 font-mono text-sm tracking-tight font-normal opacity-0; + @apply absolute -top-0.5 right-10 text-gray-70 font-mono text-sm tracking-tight font-normal opacity-0; } -.Container button:hover span { +:global(.dark) .Container button span { + @apply text-gray-70 !important; +} + +/* Hide "Copy" text completely */ +.Container button[data-copy-state="copy"] span { + @apply hidden; +} + +/* Show "Copied!" text */ +.Container button[data-copy-state="copy-success"] span { @apply opacity-100; } @@ -72,7 +82,26 @@ /* Dark mode button text */ :global(.dark) .Container button span { - @apply text-white; + @apply text-gray-40; } /* Syntax highlighting is now handled globally via styles/syntax-highlighting.scss */ + +/* Try Me Button */ +.TryMeButton { + @apply flex items-center text-gray-90 gap-1 pr-4 rounded text-sm font-medium transition-all duration-150; + @apply hover:opacity-60; + text-decoration: none !important; +} + +:global(.dark) .TryMeButton { + @apply text-white; +} + +.TryMeIcon { + @apply text-lg leading-none; +} + +.TryMeLabel { + @apply hidden sm:inline tracking-tight; +} diff --git a/content/develop/concepts/configuration/theming-fonts.md b/content/develop/concepts/configuration/theming-fonts.md index edcd6ed7e..5d5abbf63 100644 --- a/content/develop/concepts/configuration/theming-fonts.md +++ b/content/develop/concepts/configuration/theming-fonts.md @@ -15,7 +15,7 @@ Streamlit comes with [Source Sans](https://fonts.adobe.com/fonts/source-sans), [ To use these default faults, you can set each of the following configuration options to `"sans-serif"` (Source Sans), `"serif"` (Source Serif), or `"monospace"` (Source Code) in `config.toml`: -```toml +```toml filename=".streamlit/config.toml" [theme] font = "sans-serif" headingFont = "sans-serif" @@ -45,7 +45,7 @@ When fonts are not declared in `[theme.sidebar]`, Streamlit will inherit each op In the following `config.toml` example, Streamlit uses Source Serif in the main body of the app and Source Sans in the sidebar. -```toml +```toml filename=".streamlit/config.toml" [theme] font = "serif" [theme.sidebar] @@ -56,7 +56,7 @@ font = "sans-serif" If you use a font service like Google Fonts or Adobe Fonts, you can use those fonts directly by encoding their font family (name) and CSS URL into a single string of the form `{font_name}:{css_url}`. If your font family includes a space, use inner quotes on the font family. In the following `config.toml` example, Streamlit uses Nunito font for all text except code, which is Space Mono instead. Space Mono has inner quotes because it has a space. -```toml +```toml filename=".streamlit/config.toml" [theme] font = "Nunito:https://fonts.googleapis.com/css2?family=Nunito&display=swap" codeFont = "'Space Mono':https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" @@ -98,9 +98,7 @@ The following example uses static file serving to host Google's [Noto Sans](http A line-by-line explanation of this example is available in a [tutorial](/develop/tutorials/configuration-and-theming/variable-fonts). -`.streamlit/config.toml`: - -```toml +```toml filename=".streamlit/config.toml" [server] enableStaticServing = true @@ -121,9 +119,7 @@ font="noto-sans" codeFont="noto-mono" ``` -Directory structure: - -```none +```none filename="Directory structure" project_directory/ ├── .streamlit/ │ └── config.toml @@ -147,9 +143,7 @@ If your app uses a font without a matching weight-style definition, the user's b A line-by-line explanation of this example is available in a [tutorial](/develop/tutorials/configuration-and-theming/static-fonts). -`.streamlit/config.toml`: - -```toml +```toml filename=".streamlit/config.toml" [server] enableStaticServing = true @@ -178,9 +172,7 @@ weight=700 font="tuffy" ``` -Directory structure: - -```none +```none filename="Directory structure" project_directory/ ├── .streamlit/ │ └── config.toml @@ -204,9 +196,7 @@ You can always include one of Streamlit's default fonts as a final fallback. The A line-by-line explanation of this example is available in a [tutorial](/develop/tutorials/configuration-and-theming/external-fonts). -`.streamlit/config.toml`: - -```toml +```toml filename=".streamlit/config.toml" [theme] font="Nunito:https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000, sans-serif" codeFont="'Space Mono':https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap, monospace" @@ -222,14 +212,14 @@ If any of your font family names contain spaces and you are declaring a fallback You can set the base font size for your app in pixels. You must specify the base font size as an integer. The following configuration is equivalent to the default base font size of 16 pixels: -```toml +```toml filename=".streamlit/config.toml" [theme] baseFontSize=16 ``` Additionally, you can set the font size for code blocks. The font size can be declared in pixels or rem. The following configuration is equivalent to the default code font size of 0.875rem. -```toml +```toml filename=".streamlit/config.toml" [theme] codeFontSize="0.875rem" ``` diff --git a/content/get-started/fundamentals/main-concepts.md b/content/get-started/fundamentals/main-concepts.md index 8d30bd826..e826b501e 100644 --- a/content/get-started/fundamentals/main-concepts.md +++ b/content/get-started/fundamentals/main-concepts.md @@ -113,7 +113,7 @@ You can also write to your app without calling any Streamlit methods. Streamlit supports "[magic commands](/develop/api-reference/write-magic/magic)," which means you don't have to use [`st.write()`](/develop/api-reference/write-magic/st.write) at all! To see this in action try this snippet: -```python +```python try """ # My first app Here's our first attempt at using data to create a table: diff --git a/pages/[...slug].js b/pages/[...slug].js index 37d30cb6c..22664ecb1 100644 --- a/pages/[...slug].js +++ b/pages/[...slug].js @@ -14,6 +14,29 @@ import { useRouter } from "next/router"; import rehypeSlug from "rehype-slug"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import getConfig from "next/config"; + +// Custom remark plugin to pass code fence metadata through to the HTML +// e.g., ```python try filename="app.py"``` adds data-meta attribute +function remarkCodeMeta() { + // Simple recursive tree walker (avoids ESM import issues with unist-util-visit) + function walkTree(node, callback) { + callback(node); + if (node.children) { + node.children.forEach((child) => walkTree(child, callback)); + } + } + + return (tree) => { + walkTree(tree, (node) => { + if (node.type === "code" && node.meta) { + // Store meta in data.hProperties which remark-rehype passes to the element + node.data = node.data || {}; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties["data-meta"] = node.meta; + } + }); + }; +} const { serverRuntimeConfig, publicRuntimeConfig } = getConfig(); // Site Components @@ -167,7 +190,37 @@ export default function Article({ goToLatest={goToLatest} /> ), - pre: (props) => , + pre: (props) => { + // Extract metadata from code fence (e.g., ```python try filename="app.py" showAll) + // The metadata is passed via data-meta attribute from our remark plugin + const codeElement = props.children; + const metaString = codeElement?.props?.["data-meta"] || ""; + + // Parse metadata into props + const codeProps = {}; + + if (metaString) { + // Supported boolean flags (standalone words) + const booleanFlags = ["try", "showAll", "hideCopyButton"]; + + // Extract key="value" pairs (e.g., filename="app.py") + const keyValueRegex = /(\w+)=["']([^"']+)["']/g; + let match; + while ((match = keyValueRegex.exec(metaString)) !== null) { + codeProps[match[1]] = match[2]; + } + + // Check for boolean flags (standalone words like `try` or `showAll`) + const cleanedMeta = metaString.replace(keyValueRegex, ""); + booleanFlags.forEach((flag) => { + if (new RegExp(`\\b${flag}\\b`).test(cleanedMeta)) { + codeProps[flag] = true; + } + }); + } + + return ; + }, h1: H1, h2: H2, h3: H3, @@ -410,7 +463,7 @@ export async function getStaticProps(context) { scope: data, mdxOptions: { rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings], - remarkPlugins: [remarkUnwrapImages, remarkGfm], + remarkPlugins: [remarkUnwrapImages, remarkGfm, remarkCodeMeta], }, }); diff --git a/styles/syntax-highlighting.scss b/styles/syntax-highlighting.scss index 86ee774ce..3f50992b3 100644 --- a/styles/syntax-highlighting.scss +++ b/styles/syntax-highlighting.scss @@ -9,7 +9,7 @@ --syntax-decorator: theme("colors.orange.80"); --syntax-keyword: theme("colors.darkBlue.80"); --syntax-builtin: theme("colors.lightBlue.80"); - --syntax-string: theme("colors.acqua.80"); + --syntax-string: theme("colors.indigo.80"); --syntax-number: theme("colors.green.70"); --syntax-boolean: theme("colors.green.70"); --syntax-function: theme("colors.red.70"); @@ -59,7 +59,7 @@ --syntax-decorator: theme("colors.yellow.80"); --syntax-keyword: theme("colors.darkBlue.50"); --syntax-builtin: theme("colors.lightBlue.60"); - --syntax-string: theme("colors.darkBlue.30"); + --syntax-string: theme("colors.indigo.30"); --syntax-number: theme("colors.green.40"); --syntax-boolean: theme("colors.green.40"); --syntax-function: theme("colors.red.60"); diff --git a/tailwind.config.js b/tailwind.config.js index a47b6c9fe..2dd96ee75 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -156,7 +156,7 @@ module.exports = { 100: "#3f3163", }, gray: { - 10: "#fafafa", + 10: "#F5F7F9", 20: "#f0f2f6", 30: "#e6eaf1", 40: "#d5dae5",