"""
도전과제 다이얼로그에서 사용한 디자인 톤을 다른 다이얼로그에도 재사용.
핵심 원칙:
- 다이얼로그 배경: #0e0e14 (깊은 다크)
- 카드: 그라디언트 (#bg_top → #bg_bot) + 강조 외곽선
- 헤더: 큰 숫자 + 그라디언트 progress bar
- 탭: 골드 강조 선택
- 모든 sub-label은 명시적 transparent + border:none 으로 글로벌 QSS 충돌 회피
모든 컴포넌트는 stand-alone — 부모가 dark 다이얼로그라고 가정.
"""
from __future__ import annotations
from typing import Optional, List, Tuple
from PyQt5.QtWidgets import (QFrame, QLabel, QVBoxLayout, QHBoxLayout,
QProgressBar, QPushButton, QWidget, QSizePolicy,
QTabWidget)
from PyQt5.QtCore import Qt
# ── 색상 팔레트 ────────────────────────────────────────────────
DARK_BG = '#0e0e14'
DARK_PANEL = '#14141c'
DARK_PANEL_2 = '#1c1c28'
DARK_BORDER = '#2a2a3a'
DARK_BORDER_STRONG = '#44446a'
DARK_TEXT = '#e8e8f4'
DARK_TEXT_DIM = '#a0a0b8'
DARK_TEXT_FAINT = '#666680'
ACCENT_GOLD = '#ffd24a'
ACCENT_BLUE = '#6b9eff'
ACCENT_CYAN = '#4adef0'
ACCENT_PINK = '#ff90b8'
ACCENT_GREEN = '#4ade80'
ACCENT_ORANGE = '#fcd34d'
ACCENT_RED = '#fb7185'
# 카드 테마 (등급/상태별)
CARD_THEMES = {
'gold': {
'border': '#ffb700', 'border_strong': '#ffd24a',
'bg_top': '#3a2e10', 'bg_bot': '#241c08',
'text': '#ffe9a0', 'accent': ACCENT_GOLD,
},
'blue': {
'border': '#5a8eff', 'border_strong': '#6b9eff',
'bg_top': '#1a2840', 'bg_bot': '#0e1828',
'text': '#c0d8ff', 'accent': ACCENT_BLUE,
},
'cyan': {
'border': '#3acce0', 'border_strong': '#4adef0',
'bg_top': '#0e3340', 'bg_bot': '#08222b',
'text': '#a8e8f0', 'accent': ACCENT_CYAN,
},
'green': {
'border': '#3ace70', 'border_strong': '#4ade80',
'bg_top': '#0e3324', 'bg_bot': '#082218',
'text': '#a8e8c0', 'accent': ACCENT_GREEN,
},
'pink': {
'border': '#ff5a8c', 'border_strong': '#ff90b8',
'bg_top': '#3a1a2a', 'bg_bot': '#26101a',
'text': '#ffc0d4', 'accent': ACCENT_PINK,
},
'red': {
'border': '#ea5566', 'border_strong': '#fb7185',
'bg_top': '#3a1620', 'bg_bot': '#260e16',
'text': '#ffb8c0', 'accent': ACCENT_RED,
},
'gray': {
'border': '#44446a', 'border_strong': '#666688',
'bg_top': '#1c1c28', 'bg_bot': '#14141c',
'text': '#c0c0d0', 'accent': DARK_TEXT_DIM,
},
}
# ── QSS 헬퍼 ───────────────────────────────────────────────────
def dialog_qss() -> str:
"""다이얼로그 전체 배경."""
return f"QDialog {{ background: {DARK_BG}; }}"
def tabs_qss(accent: str = ACCENT_GOLD) -> str:
return f"""
QTabWidget::pane {{
background: {DARK_PANEL};
border: 1px solid {DARK_BORDER};
border-radius: 10px;
top: -1px;
}}
QTabBar::tab {{
background: {DARK_PANEL_2};
color: {DARK_TEXT_DIM};
padding: 9px 18px;
border: 1px solid {DARK_BORDER};
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-right: 3px;
font-size: 10pt;
}}
QTabBar::tab:selected {{
background: {DARK_PANEL};
color: {accent};
font-weight: bold;
border-bottom: 2px solid {accent};
}}
QTabBar::tab:hover:!selected {{
background: #2a2a36;
color: {DARK_TEXT};
}}
"""
def scroll_qss() -> str:
return f"""
QScrollArea {{ background: transparent; border: none; }}
QScrollBar:vertical {{
background: {DARK_PANEL_2}; width: 10px; border-radius: 5px;
}}
QScrollBar::handle:vertical {{
background: {DARK_BORDER_STRONG}; border-radius: 5px; min-height: 30px;
}}
QScrollBar::handle:vertical:hover {{ background: {ACCENT_BLUE}; }}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
QScrollBar:horizontal {{
background: {DARK_PANEL_2}; height: 10px; border-radius: 5px;
}}
QScrollBar::handle:horizontal {{
background: {DARK_BORDER_STRONG}; border-radius: 5px; min-width: 30px;
}}
QScrollBar::handle:horizontal:hover {{ background: {ACCENT_BLUE}; }}
"""
def button_qss(variant: str = 'default') -> str:
""" variant: default | primary | success | danger | ghost """
if variant == 'primary':
return f"""
QPushButton {{
background: {ACCENT_BLUE}; color: white;
border: none; border-radius: 6px;
padding: 8px 18px; font-weight: bold; font-size: 10pt;
}}
QPushButton:hover {{ background: #82b0ff; }}
QPushButton:pressed {{ background: #5a8eee; }}
QPushButton:disabled {{ background: #2a2a3a; color: {DARK_TEXT_FAINT}; }}
"""
if variant == 'success':
return f"""
QPushButton {{
background: {ACCENT_GREEN}; color: #0e2a1a;
border: none; border-radius: 6px;
padding: 8px 18px; font-weight: bold; font-size: 10pt;
}}
QPushButton:hover {{ background: #6ae899; }}
"""
if variant == 'danger':
return f"""
QPushButton {{
background: {ACCENT_RED}; color: white;
border: none; border-radius: 6px;
padding: 8px 18px; font-weight: bold; font-size: 10pt;
}}
QPushButton:hover {{ background: #fc8896; }}
"""
if variant == 'ghost':
return f"""
QPushButton {{
background: transparent; color: {DARK_TEXT_DIM};
border: 1px solid {DARK_BORDER_STRONG}; border-radius: 6px;
padding: 6px 14px; font-size: 9.5pt;
}}
QPushButton:hover {{ background: {DARK_PANEL_2}; color: {DARK_TEXT};
border-color: {ACCENT_BLUE}; }}
"""
# default
return f"""
QPushButton {{
background: {DARK_PANEL_2}; color: {DARK_TEXT};
border: 1px solid {DARK_BORDER_STRONG}; border-radius: 6px;
padding: 8px 18px; font-size: 10pt;
}}
QPushButton:hover {{ background: #2a2a36; border-color: {ACCENT_BLUE}; }}
"""
# ── 컴포넌트 빌더 ──────────────────────────────────────────────
def build_gradient_header(title: str, big_value: str, subtitle: str = '',
big_color: str = ACCENT_GOLD,
extra_widgets: Optional[List[QWidget]] = None) -> QFrame:
"""그라디언트 헤더 — 좌측 큰 숫자/제목, 우측에 추가 위젯들.
Args:
title: 큰 숫자 위 작은 라벨 (예: "달성")
big_value: 큰 숫자/문자열 (RichText 가능 — HTML 사용)
subtitle: 큰 숫자 아래 부제 (예: "/ 153")
big_color: 큰 숫자 색
extra_widgets: 우측에 배치할 위젯 (예: 추가 통계, 토글)
"""
container = QFrame()
container.setStyleSheet(f"""
QFrame {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #1a1a30, stop:1 #2a1a3a);
border: 1px solid #3a3a5a;
border-radius: 12px;
}}
QLabel {{ background: transparent; border: none; color: {DARK_TEXT}; }}
""")
layout = QHBoxLayout()
layout.setContentsMargins(20, 14, 20, 14)
layout.setSpacing(20)
# 좌측: 제목 + 큰 숫자
left = QVBoxLayout()
left.setSpacing(2)
if title:
t = QLabel(title)
t.setStyleSheet(
f"font-size: 9pt; color: {DARK_TEXT_DIM}; "
f"background: transparent; border: none;"
)
left.addWidget(t)
big = QLabel(
f""
f"{big_value}" + (f""
f" {subtitle}" if subtitle else '')
)
big.setTextFormat(Qt.RichText)
big.setStyleSheet("background: transparent; border: none;")
left.addWidget(big)
layout.addLayout(left)
# 우측: extra widgets
if extra_widgets:
layout.addStretch()
for w in extra_widgets:
layout.addWidget(w)
else:
layout.addStretch()
container.setLayout(layout)
return container
def build_stat_card(title: str, value: str, subtitle: str = '',
theme: str = 'blue', icon: str = '') -> QFrame:
"""단일 통계 카드 — 제목, 큰 숫자, 부제, 좌측 큰 이모지."""
t = CARD_THEMES.get(theme, CARD_THEMES['blue'])
card = QFrame()
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
card.setStyleSheet(f"""
QFrame {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 {t['bg_top']}, stop:1 {t['bg_bot']});
border: 1px solid {t['border']};
border-radius: 10px;
}}
QLabel {{ background: transparent; border: none; color: {t['text']}; }}
""")
outer = QHBoxLayout()
outer.setContentsMargins(16, 12, 16, 12)
outer.setSpacing(12)
if icon:
icon_lbl = QLabel(icon)
icon_lbl.setStyleSheet(
f"font-size: 28pt; background: transparent; border: none; "
f"color: {t['border_strong']};"
)
icon_lbl.setMinimumWidth(48)
icon_lbl.setAlignment(Qt.AlignCenter)
outer.addWidget(icon_lbl)
text_box = QVBoxLayout()
text_box.setSpacing(2)
title_lbl = QLabel(title)
title_lbl.setStyleSheet(
f"font-size: 9.5pt; color: {DARK_TEXT_DIM}; "
f"background: transparent; border: none;"
)
text_box.addWidget(title_lbl)
val_lbl = QLabel(
f""
f"{value}"
)
val_lbl.setTextFormat(Qt.RichText)
val_lbl.setStyleSheet("background: transparent; border: none;")
text_box.addWidget(val_lbl)
if subtitle:
sub_lbl = QLabel(subtitle)
sub_lbl.setStyleSheet(
f"font-size: 9pt; color: {DARK_TEXT_DIM}; "
f"background: transparent; border: none;"
)
sub_lbl.setWordWrap(True)
text_box.addWidget(sub_lbl)
outer.addLayout(text_box, 1)
card.setLayout(outer)
return card
def build_section_card(title: str, content: QWidget,
theme: str = 'gray', icon: str = '') -> QFrame:
"""제목 + 내용 큰 카드 (세로 레이아웃)."""
t = CARD_THEMES.get(theme, CARD_THEMES['gray'])
card = QFrame()
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
card.setStyleSheet(f"""
QFrame {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 {t['bg_top']}, stop:1 {t['bg_bot']});
border: 1px solid {t['border']};
border-radius: 10px;
}}
QLabel {{ background: transparent; border: none; color: {t['text']}; }}
""")
layout = QVBoxLayout()
layout.setContentsMargins(16, 12, 16, 14)
layout.setSpacing(8)
head = QHBoxLayout()
if icon:
i = QLabel(icon)
i.setStyleSheet(
f"font-size: 16pt; color: {t['border_strong']}; "
f"background: transparent; border: none;"
)
head.addWidget(i)
title_lbl = QLabel(title)
title_lbl.setStyleSheet(
f"font-size: 12pt; font-weight: bold; color: {DARK_TEXT}; "
f"background: transparent; border: none;"
)
head.addWidget(title_lbl)
head.addStretch()
layout.addLayout(head)
layout.addWidget(content, 1)
card.setLayout(layout)
return card
def style_progressbar(pb: QProgressBar, theme: str = 'blue',
height: int = 10) -> None:
"""기본 progress bar에 다크 그라디언트 스타일 적용."""
t = CARD_THEMES.get(theme, CARD_THEMES['blue'])
pb.setMinimumHeight(height)
pb.setMaximumHeight(height)
pb.setTextVisible(False)
pb.setStyleSheet(f"""
QProgressBar {{
background: rgba(0,0,0,0.4);
border: none;
border-radius: {height // 2}px;
}}
QProgressBar::chunk {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {t['border']}, stop:1 {t['border_strong']});
border-radius: {height // 2}px;
}}
""")
def transparent_label(text: str, size: int = 10, weight: str = 'normal',
color: str = DARK_TEXT) -> QLabel:
"""글로벌 QSS와 격리된 다크 라벨 (배경 없음, 외곽선 없음)."""
lbl = QLabel(text)
weight_str = 'bold' if weight == 'bold' else 'normal'
lbl.setStyleSheet(
f"font-size: {size}pt; font-weight: {weight_str}; color: {color}; "
f"background: transparent; border: none; padding: 0; margin: 0;"
)
return lbl