|
import colorsys |
|
|
|
import streamlit as st |
|
|
|
import fragments |
|
import util |
|
from util import ThemeColor |
|
|
|
|
|
preset_colors: list[tuple[str, ThemeColor]] = [ |
|
("Default light", ThemeColor( |
|
primaryColor="#ff4b4b", |
|
backgroundColor="#ffffff", |
|
secondaryBackgroundColor="#f0f2f6", |
|
textColor="#31333F", |
|
)), |
|
("Default dark", ThemeColor( |
|
primaryColor="#ff4b4b", |
|
backgroundColor="#0e1117", |
|
secondaryBackgroundColor="#262730", |
|
textColor="#fafafa", |
|
)) |
|
] |
|
|
|
theme_from_initial_config = util.get_config_theme_color() |
|
if theme_from_initial_config: |
|
preset_colors.append(("From the config", theme_from_initial_config)) |
|
|
|
default_color = preset_colors[0][1] |
|
|
|
|
|
def sync_rgb_to_hls(key: str): |
|
|
|
rgb = util.parse_hex(st.session_state[key]) |
|
hls = colorsys.rgb_to_hls(rgb[0], rgb[1], rgb[2]) |
|
st.session_state[f"{key}H"] = round(hls[0] * 360) |
|
st.session_state[f"{key}L"] = round(hls[1] * 100) |
|
st.session_state[f"{key}S"] = round(hls[2] * 100) |
|
|
|
|
|
def sync_hls_to_rgb(key: str): |
|
h = st.session_state[f"{key}H"] |
|
l = st.session_state[f"{key}L"] |
|
s = st.session_state[f"{key}S"] |
|
r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100) |
|
st.session_state[key] = f"#{round(r * 255):02x}{round(g * 255):02x}{round(b * 255):02x}" |
|
|
|
|
|
def set_color(key: str, color: str): |
|
st.session_state[key] = color |
|
sync_rgb_to_hls(key) |
|
|
|
|
|
if '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: |
|
set_color('primaryColor', default_color.primaryColor) |
|
set_color('backgroundColor', default_color.backgroundColor) |
|
set_color('secondaryBackgroundColor', default_color.secondaryBackgroundColor) |
|
set_color('textColor', default_color.textColor) |
|
|
|
|
|
st.title("Streamlit color theme editor") |
|
|
|
|
|
def on_preset_color_selected(): |
|
_, color = preset_colors[st.session_state.preset_color] |
|
set_color('primaryColor', color.primaryColor) |
|
set_color('backgroundColor', color.backgroundColor) |
|
set_color('secondaryBackgroundColor', color.secondaryBackgroundColor) |
|
set_color('textColor', color.textColor) |
|
|
|
|
|
st.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) |
|
|
|
if st.button("🎨 Generate a random color scheme 🎲"): |
|
primary_color, text_color, basic_background, secondary_background = util.generate_color_scheme() |
|
set_color('primaryColor', primary_color) |
|
set_color('backgroundColor', basic_background) |
|
set_color('secondaryBackgroundColor', secondary_background) |
|
set_color('textColor', text_color) |
|
|
|
|
|
def color_picker(label: str, key: str, default_color: str, l_only: bool) -> None: |
|
col1, col2 = st.columns([1, 3]) |
|
with col1: |
|
color = st.color_picker(label, key=key, on_change=sync_rgb_to_hls, kwargs={"key": key}) |
|
with col2: |
|
r,g,b = util.parse_hex(default_color) |
|
h,l,s = colorsys.rgb_to_hls(r,g,b) |
|
if l_only: |
|
if f"{key}H" not in st.session_state: |
|
st.session_state[f"{key}H"] = round(h * 360) |
|
else: |
|
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}) |
|
|
|
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}) |
|
|
|
if l_only: |
|
if f"{key}S" not in st.session_state: |
|
st.session_state[f"{key}S"] = round(s * 100) |
|
else: |
|
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}) |
|
|
|
return color |
|
|
|
|
|
primary_color = color_picker('Primary color', key="primaryColor", default_color=default_color.primaryColor, l_only=True) |
|
text_color = color_picker('Text color', key="textColor", default_color=default_color.textColor, l_only=True) |
|
background_color = color_picker('Background color', key="backgroundColor", default_color=default_color.backgroundColor, l_only=True) |
|
secondary_background_color = color_picker('Secondary background color', key="secondaryBackgroundColor", default_color=default_color.secondaryBackgroundColor, l_only=True) |
|
|
|
|
|
st.header("WCAG contrast ratio") |
|
st.markdown(""" |
|
Check if the color contrasts of the selected colors are enough to the WCAG guidelines recommendation. |
|
For 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).""") |
|
|
|
def synced_color_picker(label: str, value: str, key: str): |
|
def on_change(): |
|
st.session_state[key] = st.session_state[key + "2"] |
|
sync_rgb_to_hls(key) |
|
st.color_picker(label, value=value, key=key + "2", on_change=on_change) |
|
|
|
col1, col2, col3 = st.columns(3) |
|
with col2: |
|
synced_color_picker("Background color", value=background_color, key="backgroundColor") |
|
with col3: |
|
synced_color_picker("Secondary background color", value=secondary_background_color, key="secondaryBackgroundColor") |
|
|
|
col1, col2, col3 = st.columns(3) |
|
with col1: |
|
synced_color_picker("Primary color", value=primary_color, key="primaryColor") |
|
with col2: |
|
fragments.contrast_summary("Primary/Background", primary_color, background_color) |
|
with col3: |
|
fragments.contrast_summary("Primary/Secondary background", primary_color, secondary_background_color) |
|
|
|
col1, col2, col3 = st.columns(3) |
|
with col1: |
|
synced_color_picker("Text color", value=text_color, key="textColor") |
|
with col2: |
|
fragments.contrast_summary("Text/Background", text_color, background_color) |
|
with col3: |
|
fragments.contrast_summary("Text/Secondary background", text_color, secondary_background_color) |
|
|
|
|
|
st.header("Config") |
|
|
|
st.subheader("Config file (`.streamlit/config.toml`)") |
|
st.code(f""" |
|
[theme] |
|
primaryColor="{primary_color}" |
|
backgroundColor="{background_color}" |
|
secondaryBackgroundColor="{secondary_background_color}" |
|
textColor="{text_color}" |
|
""", language="toml") |
|
|
|
st.subheader("Command line argument") |
|
st.code(f""" |
|
streamlit run app.py \\ |
|
--theme.primaryColor="{primary_color}" \\ |
|
--theme.backgroundColor="{background_color}" \\ |
|
--theme.secondaryBackgroundColor="{secondary_background_color}" \\ |
|
--theme.textColor="{text_color}" |
|
""") |
|
|
|
|
|
if st.checkbox("Apply theme to this page"): |
|
st.info("Select 'Custom Theme' in the settings dialog to see the effect") |
|
|
|
def reconcile_theme_config(): |
|
keys = ['primaryColor', 'backgroundColor', 'secondaryBackgroundColor', 'textColor'] |
|
has_changed = False |
|
for key in keys: |
|
if st._config.get_option(f'theme.{key}') != st.session_state[key]: |
|
st._config.set_option(f'theme.{key}', st.session_state[key]) |
|
has_changed = True |
|
if has_changed: |
|
st.experimental_rerun() |
|
|
|
reconcile_theme_config() |
|
|
|
fragments.sample_components("body") |
|
with st.sidebar: |
|
fragments.sample_components("sidebar") |
|
|