""" 도전과제 다이얼로그에서 사용한 디자인 톤을 다른 다이얼로그에도 재사용. 핵심 원칙: - 다이얼로그 배경: #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