whitphx/streamlit-theme-editor
2
1import colorsys2 3import streamlit as st4 5import fragments6import util7from util import ThemeColor8 9 10preset_colors: list[tuple[str, ThemeColor]] = [11 ("Default light", ThemeColor(12 primaryColor="#ff4b4b",13 backgroundColor="#ffffff",14 secondaryBackgroundColor="#f0f2f6",15 textColor="#31333F",16 )),17 ("Default dark", ThemeColor(18 primaryColor="#ff4b4b",19 backgroundColor="#0e1117",20 secondaryBackgroundColor="#262730",21 textColor="#fafafa",22 ))23]24 25theme_from_initial_config = util.get_config_theme_color()26if theme_from_initial_config:27 preset_colors.append(("From the config", theme_from_initial_config))28 29default_color = preset_colors[0][1]30 31 32def sync_rgb_to_hls(key: str):33 # HLS states are necessary for the HLS sliders.34 rgb = util.parse_hex(st.session_state[key])35 hls = colorsys.rgb_to_hls(rgb[0], rgb[1], rgb[2])36 st.session_state[f"{key}H"] = round(hls[0] * 360)37 st.session_state[f"{key}L"] = round(hls[1] * 100)38 st.session_state[f"{key}S"] = round(hls[2] * 100)39 40 41def sync_hls_to_rgb(key: str):42 h = st.session_state[f"{key}H"]43 l = st.session_state[f"{key}L"]44 s = st.session_state[f"{key}S"]45 r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100)46 st.session_state[key] = f"#{round(r * 255):02x}{round(g * 255):02x}{round(b * 255):02x}"47 48 49def set_color(key: str, color: str):50 st.session_state[key] = color51 sync_rgb_to_hls(key)52 53 54if 'preset_color' not in st.session_state or 'backgroundColor' not in st.session_state or 'secondaryBackgroundColor' not in st.session_state or 'textColor' not in st.session_state:55 set_color('primaryColor', default_color.primaryColor)56 set_color('backgroundColor', default_color.backgroundColor)57 set_color('secondaryBackgroundColor', default_color.secondaryBackgroundColor)58 set_color('textColor', default_color.textColor)59 60 61st.title("Streamlit color theme editor")62 63 64def on_preset_color_selected():65 _, color = preset_colors[st.session_state.preset_color]66 set_color('primaryColor', color.primaryColor)67 set_color('backgroundColor', color.backgroundColor)68 set_color('secondaryBackgroundColor', color.secondaryBackgroundColor)69 set_color('textColor', color.textColor)70 71 72st.selectbox("Preset colors", key="preset_color", options=range(len(preset_colors)), format_func=lambda idx: preset_colors[idx][0], on_change=on_preset_color_selected)73 74if st.button("🎨 Generate a random color scheme 🎲"):75 primary_color, text_color, basic_background, secondary_background = util.generate_color_scheme()76 set_color('primaryColor', primary_color)77 set_color('backgroundColor', basic_background)78 set_color('secondaryBackgroundColor', secondary_background)79 set_color('textColor', text_color)80 81 82def color_picker(label: str, key: str, default_color: str, l_only: bool) -> None:83 col1, col2 = st.columns([1, 3])84 with col1:85 color = st.color_picker(label, key=key, on_change=sync_rgb_to_hls, kwargs={"key": key})86 with col2:87 r,g,b = util.parse_hex(default_color)88 h,l,s = colorsys.rgb_to_hls(r,g,b)89 if l_only:90 if f"{key}H" not in st.session_state:91 st.session_state[f"{key}H"] = round(h * 360)92 else:93 st.slider(f"H for {label}", key=f"{key}H", min_value=0, max_value=360, value=round(h * 360), format="%d°", label_visibility="collapsed", on_change=sync_hls_to_rgb, kwargs={"key": key})94 95 st.slider(f"L for {label}", key=f"{key}L", min_value=0, max_value=100, value=round(l * 100), format="%d%%", label_visibility="collapsed", on_change=sync_hls_to_rgb, kwargs={"key": key})96 97 if l_only:98 if f"{key}S" not in st.session_state:99 st.session_state[f"{key}S"] = round(s * 100)100 else:101 st.slider(f"S for {label}", key=f"{key}S", min_value=0, max_value=100, value=round(s * 100), format="%d%%", label_visibility="collapsed", on_change=sync_hls_to_rgb, kwargs={"key": key})102 103 return color104 105 106primary_color = color_picker('Primary color', key="primaryColor", default_color=default_color.primaryColor, l_only=True)107text_color = color_picker('Text color', key="textColor", default_color=default_color.textColor, l_only=True)108background_color = color_picker('Background color', key="backgroundColor", default_color=default_color.backgroundColor, l_only=True)109secondary_background_color = color_picker('Secondary background color', key="secondaryBackgroundColor", default_color=default_color.secondaryBackgroundColor, l_only=True)110 111 112st.header("WCAG contrast ratio")113st.markdown("""114Check if the color contrasts of the selected colors are enough to the WCAG guidelines recommendation.115For the details about it, see some resources such as the [WCAG document](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) or the [MDN page](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast).""")116 117def synced_color_picker(label: str, value: str, key: str):118 def on_change():119 st.session_state[key] = st.session_state[key + "2"]120 sync_rgb_to_hls(key)121 st.color_picker(label, value=value, key=key + "2", on_change=on_change)122 123col1, col2, col3 = st.columns(3)124with col2:125 synced_color_picker("Background color", value=background_color, key="backgroundColor")126with col3:127 synced_color_picker("Secondary background color", value=secondary_background_color, key="secondaryBackgroundColor")128 129col1, col2, col3 = st.columns(3)130with col1:131 synced_color_picker("Primary color", value=primary_color, key="primaryColor")132with col2:133 fragments.contrast_summary("Primary/Background", primary_color, background_color)134with col3:135 fragments.contrast_summary("Primary/Secondary background", primary_color, secondary_background_color)136 137col1, col2, col3 = st.columns(3)138with col1:139 synced_color_picker("Text color", value=text_color, key="textColor")140with col2:141 fragments.contrast_summary("Text/Background", text_color, background_color)142with col3:143 fragments.contrast_summary("Text/Secondary background", text_color, secondary_background_color)144 145 146st.header("Config")147 148st.subheader("Config file (`.streamlit/config.toml`)")149st.code(f"""150[theme]151primaryColor="{primary_color}"152backgroundColor="{background_color}"153secondaryBackgroundColor="{secondary_background_color}"154textColor="{text_color}"155""", language="toml")156 157st.subheader("Command line argument")158st.code(f"""159streamlit run app.py \\160 --theme.primaryColor="{primary_color}" \\161 --theme.backgroundColor="{background_color}" \\162 --theme.secondaryBackgroundColor="{secondary_background_color}" \\163 --theme.textColor="{text_color}"164""")165 166 167if st.checkbox("Apply theme to this page"):168 st.info("Select 'Custom Theme' in the settings dialog to see the effect")169 170 def reconcile_theme_config():171 keys = ['primaryColor', 'backgroundColor', 'secondaryBackgroundColor', 'textColor']172 has_changed = False173 for key in keys:174 if st._config.get_option(f'theme.{key}') != st.session_state[key]:175 st._config.set_option(f'theme.{key}', st.session_state[key])176 has_changed = True177 if has_changed:178 st.experimental_rerun()179 180 reconcile_theme_config()181 182 fragments.sample_components("body")183 with st.sidebar:184 fragments.sample_components("sidebar")185 