Antonio0616's picture
Upload 2 files
672ba7b verified
# 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)