/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/shared/StreamlitMarkdown/styled-components.ts
448 строк
13 KB
Maya Barnes
Add shadows to theming (#13525)
16 янв 2026, 01:23
Не верифицирован
16 янв 2026, 01:23
ce817f8
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Theme } from "@emotion/react" import styled from "@emotion/styled" import { roundFontSizeToNearestEighth } from "~lib/theme/utils" export interface StyledStreamlitMarkdownProps { isCaption: boolean isInDialog: boolean isLabel?: boolean inheritFont?: boolean boldLabel?: boolean largerLabel?: boolean isToast?: boolean } function convertRemToEm(s: string): string { return s.replace(/rem$/, "em") } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. function sharedMarkdownStyle(theme: Theme): any { return { a: { color: theme.colors.link, textDecoration: theme.linkUnderline ? "underline" : "none", "&:focus": { outline: "none", // Fallback for environments without :focus-visible support: boxShadow: theme.shadows.focusRing, borderRadius: theme.radii.default, }, // In browsers that support :focus-visible, avoid showing the focus ring on // mouse focus (while still keeping the fallback behavior in others). "&:focus:not(:focus-visible)": { boxShadow: theme.shadows.none, }, "&:focus-visible": { boxShadow: theme.shadows.focusRing, borderRadius: theme.radii.default, }, }, } } /** * Caption sizes taken from default styles, but using em instead of rem, so it * inherits the <small>'s shrunk size. Also handles reduced heading font sizes * in dialogs. */ function convertFontSizes( fontSize: string, isInDialog: boolean, isCaption: boolean ): string { // For headers in `st.caption`, we use `em` values, so the headers automatically // become a bit smaller by adapting to the font size of the caption. if (isInDialog) { // Dialogs also reduce the font size of the headings to 65% of the base font size // Round the font size to the nearest eighth of a rem to try to keep to round px values const roundedFontSize = roundFontSizeToNearestEighth( parseFloat(fontSize) * 0.65 ) // Ensure the font size is at least 0.75rem const dialogFontSize = `${Math.max(roundedFontSize, 0.75)}rem` return isCaption ? convertRemToEm(dialogFontSize) : dialogFontSize } return isCaption ? convertRemToEm(fontSize) : fontSize } function getMarkdownHeadingDefinitions( theme: Theme, isInDialog: boolean, isCaption: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. ): any { return { "h1, h2, h3, h4, h5, h6": { fontFamily: theme.genericFonts.headingFont, lineHeight: theme.lineHeights.headings, margin: 0, color: "inherit", }, h1: { fontSize: convertFontSizes( theme.fontSizes.h1FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h1FontWeight, padding: `${theme.spacing.xl} 0 ${theme.spacing.lg} 0`, }, "h1 b, h1 strong": { // Per Pull Request #9395, setting text to bold in headers // should NOT change its font-weight fontWeight: theme.fontWeights.h1FontWeight, }, "h2, h3": { letterSpacing: "-0.005em", }, h2: { fontSize: convertFontSizes( theme.fontSizes.h2FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h2FontWeight, padding: `${theme.spacing.lg} 0 ${theme.spacing.lg} 0`, }, h3: { fontSize: convertFontSizes( theme.fontSizes.h3FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h3FontWeight, padding: `${theme.spacing.md} 0 ${theme.spacing.lg} 0`, }, h4: { fontSize: convertFontSizes( theme.fontSizes.h4FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h4FontWeight, padding: `${theme.spacing.sm} 0 ${theme.spacing.lg} 0`, }, h5: { fontSize: convertFontSizes( theme.fontSizes.h5FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h5FontWeight, padding: `${theme.spacing.xs} 0 ${theme.spacing.lg} 0`, }, h6: { fontSize: convertFontSizes( theme.fontSizes.h6FontSize, isInDialog, isCaption ), fontWeight: theme.fontWeights.h6FontWeight, padding: `${theme.spacing.twoXS} 0 ${theme.spacing.lg} 0`, }, } } export const StyledStreamlitMarkdown = styled.div<StyledStreamlitMarkdownProps>( ({ theme, isCaption, isInDialog, isLabel, inheritFont, boldLabel, largerLabel, isToast, }) => { // Widget Labels have smaller font size with exception of Button/Checkbox/Radio Button labels // Toasts also have smaller font size as well as pills and segmented controls. const useSmallerFontSize = (isLabel && !largerLabel) || isToast || isCaption return { fontFamily: inheritFont ? "inherit" : theme.genericFonts.bodyFont, fontSize: inheritFont ? "inherit" : useSmallerFontSize ? theme.fontSizes.sm : theme.fontSizes.md, marginBottom: isLabel ? "" : `-${theme.spacing.lg}`, opacity: isCaption ? 0.6 : undefined, color: "inherit", // Always respect the width of the parent container: maxWidth: "100%", width: isLabel ? "" : "100%", // Break long words to prevent them from overflowing the container: overflowWrap: "break-word", ...sharedMarkdownStyle(theme), ...getMarkdownHeadingDefinitions(theme, isInDialog, isCaption), // This is required so that long Latex formulas in `st.latex` are scrollable // when `help` is set (see below). "&:has(.katex-display)": { overflowX: "hidden", }, p: { wordBreak: "break-word", marginBottom: isLabel ? theme.spacing.none : "", fontWeight: inheritFont ? "inherit" : boldLabel ? theme.fontWeights.bold : "", marginTop: theme.spacing.none, marginLeft: theme.spacing.none, marginRight: theme.spacing.none, }, img: { // Images in markdown should never be wider // than the content area. maxWidth: "100%", // In labels, widgets should never be taller than the text. maxHeight: isLabel ? "1em" : undefined, verticalAlign: "middle", // Ensure that images are not distorted: objectFit: "scale-down", }, li: { margin: "0.2em 0 0.2em 1.15em", padding: "0 0 0 0.3em", }, "li.task-list-item": { listStyleType: "none", padding: "0", marginLeft: "0", "input[type='checkbox']": { verticalAlign: "middle", margin: "0 0.35em 0.1em 0", }, }, // Handles quotes: blockquote: { margin: "1em 0 1em 0", padding: `0 0 0 0.75em`, borderLeft: `0.15em solid ${theme.colors.borderColor}`, opacity: 0.6, }, "b, strong": { fontWeight: theme.fontWeights.bold, }, // Issue #11976: Handle bolded inline code "b code, strong code": { fontWeight: theme.fontWeights.codeBold, }, // Handles the horizontal divider: hr: { margin: "2em 0", padding: 0, // Reset Firefox's gray color: color: "inherit", backgroundColor: "transparent", border: "none", borderBottom: `${theme.sizes.borderWidth} solid ${theme.colors.borderColor}`, // Set correct height and prevent the size attribute // to make the hr look like an input field: "&:not([size])": { height: theme.sizes.borderWidth, }, }, table: { display: "table", // Prevent double borders borderCollapse: "collapse", marginBottom: theme.spacing.lg, }, tr: { borderTop: `${theme.sizes.borderWidth} solid ${theme.colors.dataframeBorderColor}`, }, th: { // TODO: check whether this should be adjusted // defaults to font-weight: "bold" (700) textAlign: "inherit", }, "th, td": { padding: `${theme.spacing.xs} ${theme.spacing.md}`, border: `${theme.sizes.borderWidth} solid ${theme.colors.dataframeBorderColor}`, }, "span.stMarkdownColoredBackground": { borderRadius: theme.radii.md, padding: `${theme.spacing.threeXS} ${theme.spacing.twoXS}`, margin: theme.spacing.none, boxDecorationBreak: "clone", }, "span.stMarkdownBadge": { borderRadius: theme.radii.md, // Since we're using inline-block below, we're not using vertical padding here, // because inline-block already makes the element look a bit taller. padding: `0 ${theme.spacing.twoXS}`, // Add a small margin to separate it a bit from other text. margin: `0 ${theme.spacing.px}`, // Make badges not wrap and ellipsize them if they are too long for the // parent container. whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "100%", display: "inline-block", verticalAlign: "middle", }, "p, ol, ul, dl, li": { fontSize: "inherit", }, "& > ul, & > ol": { display: "block", width: "fit-content", textAlign: "left", }, // Ensure nested lists stay as block elements "li > ul, li > ol": { display: "block", }, // Allow long Latex formulas that are not inline (i.e. either from `st.latex` // or in their own paragraph inside `st.markdown`) to scroll horizontally. ".katex-display": { overflowX: "auto", overflowY: "hidden", }, ".katex": { fontWeight: theme.fontWeights.normal, }, } } ) export const StyledLinkIcon = styled.a(({ theme }) => ({ // center icon lineHeight: 0, display: "inline-flex", alignItems: "center", justifyContent: "center", opacity: 0, pointerEvents: "none", transition: "opacity 150ms ease-in-out", svg: { // same color as the tooltip-icon stroke: theme.colors.fadedText60, strokeWidth: theme.sizes.defaultStrokeWidth, }, "&:hover svg": { stroke: theme.colors.bodyText, }, "&:focus-visible": { opacity: 1, pointerEvents: "auto", }, })) export const StyledHeadingWithActionElements = styled.div(({ theme }) => ({ "h1, h2, h3, h4, h5, h6, span": { scrollMarginTop: theme.sizes.headerHeight, }, ...sharedMarkdownStyle(theme), // break-word & pretty makes most headings break in a nicer way than break-all while still // preventing overflowing of the container to the side. Long headings without whitespaces or hyphens might still look weird wordBreak: "break-word", textWrap: "pretty", // Show link icon when hovering or when focus is within the heading container. // We use opacity instead of visibility so the link remains in the tab order. "&:hover, &:focus-within": { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. [StyledLinkIcon as any]: { opacity: 1, pointerEvents: "auto", }, }, })) export const StyledHeadingActionElements = styled.span(({ theme }) => ({ marginLeft: theme.spacing.sm, display: "inline-flex", gap: theme.spacing.sm, verticalAlign: "middle", "& > *": { // remove margins of inner elements as they are wrapped in a container that applies the margin marginLeft: "0 !important", }, })) export interface StyledDividerProps { rainbow: boolean color: string } export const StyledHeaderDivider = styled.hr<StyledDividerProps>( ({ theme, rainbow, color }) => { return { // Height needs to be !important due to globalStyles.tsx hr height override - line #170 height: `${theme.spacing.threeXS} !important`, marginTop: theme.spacing.sm, marginBottom: theme.spacing.none, border: "none", borderRadius: theme.radii.full, ...(rainbow ? { background: color } : { backgroundColor: color }), } } ) export const StyledPreWrapper = styled.div(({ theme }) => ({ // Set spacing between pre-elements inside of markdown similar to our gap spacing between elements marginBottom: theme.spacing.lg, })) export const StyledHelpIconWrapper = styled.span({ display: "inline-block", verticalAlign: "middle", transform: "translateY(-0.1em)", })