Spaces:
Sleeping
Sleeping
| # dashboard_theme/theme.py | |
| # 다중 팔레트 지원 (기본: Navy×Amber / 대안: Obsidian×Cyan / Graphite×Warm Orange) | |
| import streamlit as st | |
| PALETTES = { | |
| # 1) 기존 네이비 × 앰버 | |
| "navy_amber": { | |
| "bg": "#0B1220", | |
| "panel": "#0F1B2D", | |
| "panel2": "#0B1627", | |
| "border": "rgba(255,255,255,.06)", | |
| "shadow": "0 12px 28px rgba(0,0,0,.40)", | |
| "text": "#FFFFFF", | |
| "muted": "#9FB1C5", | |
| "accent": "#F59E0B", | |
| "accent_soft": "rgba(245,158,11,.12)", | |
| "disabled_bg": "#2A2F3A", | |
| "disabled_fg": "#E0E6ED", | |
| "placeholder": "rgba(255,255,255,.9)", | |
| "sidebar_bg": "#0C1828", | |
| "radio_bg": "#0E1A2B", | |
| "radio_hover": "#112642", | |
| }, | |
| # 2) 오브시디언 × 시안 | |
| "obsidian_cyan": { | |
| "bg": "#0B0F14", | |
| "panel": "#0F1720", | |
| "panel2": "#0B131C", | |
| "border": "rgba(255,255,255,.08)", | |
| "shadow": "0 16px 32px rgba(0,0,0,.55)", | |
| "text": "#F8FAFC", | |
| "muted": "#A7C4D6", | |
| "accent": "#22D3EE", | |
| "accent_soft": "rgba(34,211,238,.14)", | |
| "disabled_bg": "#26313A", | |
| "disabled_fg": "#E6F6FA", | |
| "placeholder": "rgba(240,249,255,.92)", | |
| "sidebar_bg": "#0F141A", | |
| "radio_bg": "#0F1720", | |
| "radio_hover": "#13202B", | |
| }, | |
| # 3) Graphite × Warm Orange (Simulation 페이지 톤과 일치) | |
| "graphite_gold": { | |
| "bg": "#2F3136", # 전체 배경(짙은 그레이) | |
| "panel": "#3B3E44", # 카드/패널 | |
| "panel2": "#2B2D31", # 표/보조 | |
| "border": "rgba(255,255,255,.10)", | |
| "shadow": "0 16px 40px rgba(0,0,0,.45)", | |
| "text": "#F6F7F9", | |
| "muted": "#C9CDD2", | |
| "accent": "#F59E0B", # 따뜻한 주황(눈부심 적음) | |
| "accent_soft": "rgba(245,158,11,.18)", | |
| "disabled_bg": "#4A4E55", | |
| "disabled_fg": "#F1F3F6", | |
| "placeholder": "rgba(250,250,252,.85)", | |
| "sidebar_bg": "#3A3D42", | |
| "radio_bg": "#3B3E44", | |
| "radio_hover": "#454950", | |
| }, | |
| } | |
| def setup_page(): | |
| st.set_page_config(page_title="시뮬레이션", layout="wide", initial_sidebar_state="expanded") | |
| def _css_template(c): | |
| return f""" | |
| <style> | |
| @font-face {{ | |
| font-family:'Pretendard'; | |
| src:url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.woff2') format('woff2'); | |
| font-weight:100 900; font-style:normal; font-display:swap; | |
| }} | |
| html, body, [class*="css"] {{ | |
| font-family:Pretendard, Inter, system-ui, -apple-system, Segoe UI, Roboto, Noto Sans KR, Apple SD Gothic Neo, sans-serif; | |
| }} | |
| :root{{ | |
| --bg:{c['bg']}; | |
| --panel:{c['panel']}; | |
| --panel-2:{c['panel2']}; | |
| --border:{c['border']}; | |
| --shadow:{c['shadow']}; | |
| --text:{c['text']}; | |
| --muted:{c['muted']}; | |
| --accent:{c['accent']}; | |
| --accent-soft:{c['accent_soft']}; | |
| }} | |
| html, body, .stApp, .stAppViewContainer, [data-testid="stAppViewContainer"]{{ background:var(--bg)!important; color:var(--text)!important; }} | |
| [data-testid="stHeader"]{{ background:transparent!important; }} | |
| section.main{{ background:transparent!important; }} | |
| /* 사이드바 */ | |
| section[data-testid="stSidebar"]{{ background:{c['sidebar_bg']}!important; border-right:1px solid var(--border)!important; }} | |
| section[data-testid="stSidebar"] *{{ color:var(--text)!important; }} | |
| section[data-testid="stSidebar"] .stMarkdown{{ color:var(--muted)!important; }} | |
| /* 라디오/토글형 */ | |
| div[role="radiogroup"] > label{{ | |
| border:1px solid var(--border)!important; border-radius:12px!important; | |
| padding:10px 14px!important; margin-right:8px!important; background:{c['radio_bg']}!important; | |
| cursor:pointer; font-weight:800; color:var(--text)!important; | |
| transition:transform .08s ease, background .2s ease, box-shadow .2s ease, filter .2s ease; | |
| }} | |
| div[role="radiogroup"] input{{ display:none!important; }} | |
| div[role="radiogroup"] > label:hover{{ background:{c['radio_hover']}!important; transform:translateY(-1px); }} | |
| div[role="radiogroup"] > label[data-checked="true"]{{ | |
| color:#1b1b1b!important; background:var(--accent)!important; border-color:var(--accent)!important; | |
| box-shadow:0 8px 22px rgba(0,0,0,.18), 0 6px 18px color-mix(in oklab, var(--accent) 30%, transparent)!important; | |
| }} | |
| /* Expander */ | |
| div[data-testid="stExpander"] > details{{ padding:0; border:none; background:transparent; }} | |
| div[data-testid="stExpander"] > details > summary{{ | |
| list-style:none; height:44px; line-height:44px; | |
| background:linear-gradient(180deg,var(--panel) 0%, var(--panel-2) 100%)!important; | |
| border:1px solid var(--border)!important; border-radius:12px!important; padding:0 14px!important; | |
| display:flex; align-items:center; color:var(--text)!important; font-weight:900; | |
| }} | |
| div[data-testid="stExpander"] > details > div{{ | |
| margin-top:10px; padding:14px; border:1px solid var(--border)!important; | |
| border-radius:12px; background:var(--panel-2); box-shadow:var(--shadow); | |
| }} | |
| /* 입력 컴포넌트 */ | |
| div[data-baseweb="select"] > div{{ border:1.6px solid var(--border)!important; border-radius:12px!important; background:var(--panel-2)!important; color:var(--text)!important; }} | |
| div[data-testid="stNumberInput"] > div{{ border:1.6px solid var(--border)!important; border-radius:12px!important; background:var(--panel-2)!important; }} | |
| div[data-testid="stNumberInput"] input{{ background:var(--panel-2)!important; color:var(--text)!important; border:none!important; }} | |
| div[data-testid="stTextInput"] input, div[data-testid="stTextArea"] textarea{{ | |
| background:var(--panel-2)!important; color:var(--text)!important; border:1.6px solid var(--border)!important; border-radius:12px!important; | |
| }} | |
| :where(input,textarea,select):focus{{ | |
| outline:2px solid color-mix(in oklab, var(--accent) 48%, transparent)!important; | |
| box-shadow:0 0 0 3px color-mix(in oklab, var(--accent) 18%, transparent)!important; | |
| }} | |
| /* 버튼 - 기본(강조) */ | |
| .stButton > button{{ | |
| background:var(--accent)!important; border:1px solid var(--accent)!important; | |
| color:#1b1b1b!important; font-weight:1000!important; border-radius:14px!important; padding:10px 16px!important; | |
| box-shadow:0 8px 18px rgba(0,0,0,.22), 0 8px 18px color-mix(in oklab, var(--accent) 28%, transparent)!important; | |
| transition:transform .06s ease, filter .2s ease, box-shadow .2s ease; | |
| }} | |
| .stButton > button:hover{{ filter:brightness(1.03)!important; transform:translateY(-1px)!important; }} | |
| /* 버튼 - 비활성/Secondary */ | |
| .stButton > button:disabled, .stButton > button[disabled]{{ | |
| background:{c['disabled_bg']}!important; color:{c['disabled_fg']}!important; | |
| border:1px solid var(--border)!important; font-weight:600!important; opacity:1!important; | |
| }} | |
| /* 패널 */ | |
| .panel{{ | |
| border:1px solid var(--border)!important; border-radius:18px!important; | |
| padding:18px 18px 14px!important; | |
| background:linear-gradient(180deg,var(--panel) 0%, var(--panel-2) 100%)!important; | |
| box-shadow:var(--shadow)!important; margin:10px 0 22px!important; backdrop-filter:blur(6px); | |
| }} | |
| .panel-title{{ font-weight:1000!important; color:var(--text)!important; margin:0 0 10px 2px!important; font-size:18px!important; }} | |
| /* Metric 카드 */ | |
| .metric{{ | |
| border:1px solid var(--border)!important; border-radius:18px!important; | |
| background:linear-gradient(180deg,var(--panel) 0%, var(--panel-2) 100%)!important; | |
| box-shadow:var(--shadow)!important; padding:22px!important; min-height:164px!important; | |
| display:flex; flex-direction:column; justify-content:center; | |
| }} | |
| .metric .label{{ font-weight:900; color:var(--muted); font-size:14px; }} | |
| .metric .value{{ font-size:46px; font-weight:1000; color:var(--accent); margin:6px 0 8px; }} | |
| .metric .chip{{ | |
| display:inline-block; padding:5px 12px; border-radius:999px; font-weight:1000; font-size:12px; | |
| background:var(--accent-soft); color:var(--accent); | |
| border:1px solid color-mix(in oklab, var(--accent) 26%, transparent); | |
| }} | |
| .metric.ok .chip{{ background:rgba(34,197,94,.16); color:#A9F0BF; }} | |
| .metric.bad .chip{{ background:rgba(239,68,68,.16); color:#F7B4B4; }} | |
| .metric .range{{ color:#E8EEF8; font-size:15px; font-weight:800; }} | |
| /* DataFrame */ | |
| [data-testid="stDataFrame"] thead th{{ background:var(--panel)!important; color:var(--text)!important; border-bottom:1px solid var(--border)!important; }} | |
| [data-testid="stDataFrame"] tbody td{{ background:var(--panel-2)!important; color:var(--text)!important; }} | |
| [data-testid="stDataFrame"] tbody tr:nth-child(even) td{{ background:color-mix(in oklab, var(--panel-2) 90%, black)!important; }} | |
| /* 구분선/캡션/배지 */ | |
| hr, .stDivider{{ border-color:var(--border)!important; }} | |
| small, .stCaption{{ color:var(--muted)!important; }} | |
| .badge{{ | |
| display:inline-block; padding:4px 10px; border-radius:999px; font-size:12px; font-weight:900; | |
| border:1px solid var(--border); background:var(--panel); color:var(--text); margin-right:8px; margin-bottom:6px; | |
| }} | |
| .badge.em{{ background:var(--accent-soft); color:var(--accent); border-color:color-mix(in oklab, var(--accent) 28%, transparent); }} | |
| .badge.ok{{ background:rgba(34,197,94,.16); color:#A9F0BF; }} | |
| .badge.ng{{ background:rgba(239,68,68,.16); color:#F7B4B4; }} | |
| /* 라벨/텍스트 고정 패치 */ | |
| [data-testid="stWidgetLabel"], [data-testid="stWidgetLabel"] *, label[data-testid="stWidgetLabel"], label[data-testid="stWidgetLabel"] *{{ color:var(--text)!important; opacity:1!important; }} | |
| .stRadio label, .stRadio label *, div[role="radiogroup"] > label, div[role="radiogroup"] > label *{{ color:var(--text)!important; opacity:1!important; }} | |
| .stCheckbox label, .stCheckbox label *, [data-testid="stCheckbox"] label, [data-testid="stCheckbox"] label *{{ color:var(--text)!important; opacity:1!important; }} | |
| [data-baseweb="checkbox"] label, [data-baseweb="checkbox"] label *, [data-baseweb="radio"] label, [data-baseweb="radio"] label *{{ color:var(--text)!important; opacity:1!important; }} | |
| [data-testid="stMarkdownContainer"], [data-testid="stMarkdownContainer"] *{{ color:var(--text)!important; opacity:1!important; }} | |
| /* Tabs */ | |
| div[role="tablist"]{{ border-bottom:1px solid var(--border)!important; }} | |
| div[role="tablist"] button{{ background:transparent!important; border:none!important; cursor:pointer!important; }} | |
| div[role="tablist"] button[aria-selected="true"], div[role="tablist"] button[aria-selected="true"] span, div[role="tablist"] button[aria-selected="true"] p{{ color:var(--text)!important; }} | |
| div[role="tablist"] button[aria-selected="false"], div[role="tablist"] button[aria-selected="false"] span, div[role="tablist"] button[aria-selected="false"] p{{ color:var(--muted)!important; }} | |
| div[role="tablist"] button[aria-selected="true"]::after{{ content:""; display:block; margin-top:6px; border-bottom:2px solid var(--accent)!important; }} | |
| div[role="tablist"] button:hover{{ filter:brightness(1.08)!important; }} | |
| /* placeholder */ | |
| ::placeholder{{ color:{c['placeholder']}!important; opacity:1!important; }} | |
| /* 정렬 보정: 캡션 & 익스팬더 */ | |
| .tight-block .stCaption, .tight-block small {{ | |
| margin-top: 0 !important; | |
| display: block; | |
| }} | |
| .tight-block [data-testid="stExpander"] > details {{ | |
| margin-top: 6px !important; | |
| }} | |
| </style> | |
| """ | |
| def inject_css(palette: str = "navy_amber"): | |
| c = PALETTES.get(palette, PALETTES["navy_amber"]) | |
| st.markdown(_css_template(c), unsafe_allow_html=True) | |
| def inject(palette: str = "navy_amber"): | |
| """예) inject('graphite_gold')""" | |
| setup_page() | |
| inject_css(palette) | |