diff --git a/CLAUDE.md b/CLAUDE.md index 11bb167..405a70f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,21 @@ Generate report with defaults: python generate_report.py ``` -Generate report with custom options: +Generate report with modern style: ```bash -python generate_report.py --db pga.db --output report.html --top 10 --background background.png --template templates/platinum.html +python generate_report.py --style glassmorphism --output report.html +python generate_report.py --style brutalism --output report.html +python generate_report.py --style neumorphism --output report.html +``` + +Generate report with legacy Platinum template: +```bash +python generate_report.py --template templates/platinum.html --output report.html +``` + +All options: +```bash +python generate_report.py --db pga.db --output report.html --top 10 --background background.png --style glassmorphism ``` ## Architecture @@ -26,13 +38,16 @@ python generate_report.py --db pga.db --output report.html --top 10 --background - Loads HTML template from `templates/` folder (default: `templates/platinum.html`) **HTML templates (`templates/`):** -- **platinum.html**: Mac OS 9 Platinum visual style -- **brutalism.html**: Bold industrial brutalist design with hard shadows -- **glassmorphism.html**: Modern frosted glass effect -- **neumorphism.html**: Soft 3D neumorphic style +- **modern.html**: Unified template for modern styles (brutalism, glassmorphism, neumorphism) +- **platinum.html**: Legacy Mac OS 9 Platinum visual style (separate template due to unique structure) - All templates use Chart.js doughnut charts and dynamic JavaScript filtering - Placeholder tokens like `__ALL_GAMES__`, `__BACKGROUND_IMAGE__` are replaced at generation time -- Support light/dark mode with theme toggle button +- Modern templates support light/dark/auto theme toggle button + +**Style system (`styles.py`):** +- CSS definitions for each modern style (brutalism, glassmorphism, neumorphism) +- Theme configurations (colors, fonts, chart options) injected via `__THEME_CSS__` and `__THEME_CONFIG__` +- Use `--style` argument to select a modern style instead of `--template` **Database schema (`schema.py`):** - Reference file documenting Lutris database structure diff --git a/generate_report.py b/generate_report.py index c76b812..978e79f 100644 --- a/generate_report.py +++ b/generate_report.py @@ -21,9 +21,14 @@ import json import sqlite3 from pathlib import Path +from styles import get_theme_css, get_theme_config + # Directory where this script is located (for finding template.html) SCRIPT_DIR = Path(__file__).parent +# Modern styles that use the unified template +MODERN_STYLES = ["brutalism", "glassmorphism", "neumorphism"] + def load_template(template_file: str) -> str: """Load the HTML template from the specified file.""" @@ -83,7 +88,7 @@ def get_all_games(db_path: str) -> tuple[list[dict], int]: return games, total_library -def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, template_file: str, bg_image_path: str = None) -> None: +def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, template_file: str, bg_image_path: str = None, style: str = None) -> None: """Generate the HTML report.""" all_games, total_library = get_all_games(db_path) @@ -104,50 +109,67 @@ def generate_report(db_path: str, output_path: str, top_n: int, assets_dir: str, background_image = load_asset_as_base64(assets_path / "Others" / "stripes.png", "image/png") background_image_custom = "none" # For templates that prefer no default background - # Load fonts - font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype") - font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype") + # Check if using modern unified template + if style and style in MODERN_STYLES: + html = load_template("templates/modern.html") - # Load images - titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") - title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") - close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png") - hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png") - shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png") - check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png") - check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png") + # Inject theme CSS and config + theme_css = get_theme_css(style) + theme_config = get_theme_config(style) - # Load scrollbar images - scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png") - scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png") - scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png") - scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png") + html = html.replace("__THEME_CSS__", theme_css) + html = html.replace("__THEME_CONFIG__", theme_config) + html = html.replace("__ALL_GAMES__", json.dumps(all_games)) + html = html.replace("__TOP_N__", str(top_n)) + html = html.replace("__TOTAL_LIBRARY__", str(total_library)) + html = html.replace("__BACKGROUND_IMAGE__", background_image) + html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom) + else: + # Legacy template handling (platinum and others) + # Load fonts + font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype") + font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype") - # Load tab images - tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png") - tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png") + # Load images + titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") + title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png") + close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png") + hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png") + shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png") + check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png") + check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png") - html = load_template(template_file) - html = html.replace("__ALL_GAMES__", json.dumps(all_games)) - html = html.replace("__TOP_N__", str(top_n)) - html = html.replace("__TOTAL_LIBRARY__", str(total_library)) - html = html.replace("__FONT_CHARCOAL__", font_charcoal) - html = html.replace("__FONT_MONACO__", font_monaco) - html = html.replace("__BACKGROUND_IMAGE__", background_image) - html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom) - html = html.replace("__TITLEBAR_BG__", titlebar_bg) - html = html.replace("__TITLE_STRIPES__", title_stripes) - html = html.replace("__CLOSE_BTN__", close_btn) - html = html.replace("__HIDE_BTN__", hide_btn) - html = html.replace("__SHADE_BTN__", shade_btn) - html = html.replace("__CHECK_OFF__", check_off) - html = html.replace("__CHECK_ON__", check_on) - html = html.replace("__SCROLLBAR_TROUGH_V__", scrollbar_trough_v) - html = html.replace("__SCROLLBAR_THUMB_V__", scrollbar_thumb_v) - html = html.replace("__SCROLLBAR_UP__", scrollbar_up) - html = html.replace("__SCROLLBAR_DOWN__", scrollbar_down) - html = html.replace("__TAB_ACTIVE__", tab_active) - html = html.replace("__TAB_INACTIVE__", tab_inactive) + # Load scrollbar images + scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png") + scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png") + scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png") + scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png") + + # Load tab images + tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png") + tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png") + + html = load_template(template_file) + html = html.replace("__ALL_GAMES__", json.dumps(all_games)) + html = html.replace("__TOP_N__", str(top_n)) + html = html.replace("__TOTAL_LIBRARY__", str(total_library)) + html = html.replace("__FONT_CHARCOAL__", font_charcoal) + html = html.replace("__FONT_MONACO__", font_monaco) + html = html.replace("__BACKGROUND_IMAGE__", background_image) + html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom) + html = html.replace("__TITLEBAR_BG__", titlebar_bg) + html = html.replace("__TITLE_STRIPES__", title_stripes) + html = html.replace("__CLOSE_BTN__", close_btn) + html = html.replace("__HIDE_BTN__", hide_btn) + html = html.replace("__SHADE_BTN__", shade_btn) + html = html.replace("__CHECK_OFF__", check_off) + html = html.replace("__CHECK_ON__", check_on) + html = html.replace("__SCROLLBAR_TROUGH_V__", scrollbar_trough_v) + html = html.replace("__SCROLLBAR_THUMB_V__", scrollbar_thumb_v) + html = html.replace("__SCROLLBAR_UP__", scrollbar_up) + html = html.replace("__SCROLLBAR_DOWN__", scrollbar_down) + html = html.replace("__TAB_ACTIVE__", tab_active) + html = html.replace("__TAB_INACTIVE__", tab_inactive) Path(output_path).write_text(html, encoding="utf-8") print(f"Report generated: {output_path}") @@ -189,7 +211,13 @@ def main(): parser.add_argument( "--template", default="templates/platinum.html", - help="HTML template file to use (default: templates/platinum.html)" + help="HTML template file to use (default: templates/platinum.html). Ignored if --style is used." + ) + parser.add_argument( + "--style", + choices=["brutalism", "glassmorphism", "neumorphism"], + default=None, + help="Modern style to use (brutalism, glassmorphism, neumorphism). Overrides --template." ) args = parser.parse_args() @@ -202,12 +230,17 @@ def main(): print(f"Error: Assets directory not found: {args.assets}") return 1 - template_path = SCRIPT_DIR / args.template + # Validate template only if not using modern style + if args.style and args.style in MODERN_STYLES: + template_path = SCRIPT_DIR / "templates" / "modern.html" + else: + template_path = SCRIPT_DIR / args.template + if not template_path.exists(): print(f"Error: Template file not found: {template_path}") return 1 - generate_report(args.db, args.output, args.top, args.assets, args.template, args.background) + generate_report(args.db, args.output, args.top, args.assets, args.template, args.background, args.style) return 0 diff --git a/styles.py b/styles.py new file mode 100644 index 0000000..a1b5dfd --- /dev/null +++ b/styles.py @@ -0,0 +1,1903 @@ +"""CSS styles and theme configurations for modern templates.""" + +import json + +# Theme configurations for Chart.js +THEME_CONFIGS = { + "brutalism": { + "colors": [ + "#ff0000", "#0000ff", "#ffff00", "#00ff00", "#ff00ff", + "#00ffff", "#ff8800", "#8800ff", "#0088ff", "#88ff00", + "#888888" + ], + "fontFamily": "'Courier New', monospace", + "fontWeight": "bold", + "pointStyle": "rect", + "textColorLight": "#000000", + "textColorDark": "#ffffff", + "borderColorLight": "#000000", + "borderColorDark": "#ffffff", + "borderWidth": 3, + "tooltipBg": "#000000", + "tooltipTitleColor": "#ffffff", + "tooltipBodyColor": "#ffffff", + "tooltipBorderColor": "#ffffff", + "tooltipBorderWidth": 2, + "tooltipCornerRadius": 0, + "uppercaseTooltip": True + }, + "glassmorphism": { + "colors": [ + "#6366f1", "#8b5cf6", "#ec4899", "#f43f5e", "#f97316", + "#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6", + "#64748b" + ], + "fontFamily": "'Inter', sans-serif", + "fontWeight": "normal", + "pointStyle": "circle", + "textColorLight": "#1a1a2e", + "textColorDark": "#f0f0f5", + "borderColorLight": "rgba(255, 255, 255, 0.2)", + "borderColorDark": "rgba(255, 255, 255, 0.2)", + "borderWidth": 2, + "tooltipBg": "rgba(0, 0, 0, 0.8)", + "tooltipTitleColor": "#ffffff", + "tooltipBodyColor": "#ffffff", + "tooltipBorderColor": "transparent", + "tooltipBorderWidth": 0, + "tooltipCornerRadius": 8, + "uppercaseTooltip": False + }, + "neumorphism": { + "colors": [ + "#6366f1", "#8b5cf6", "#ec4899", "#f43f5e", "#f97316", + "#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6", + "#64748b" + ], + "fontFamily": "'Inter', sans-serif", + "fontWeight": "normal", + "pointStyle": "circle", + "textColorLight": "#2d3436", + "textColorDark": "#f0f0f5", + "borderColorLight": "rgba(255, 255, 255, 0.3)", + "borderColorDark": "rgba(255, 255, 255, 0.3)", + "borderWidth": 3, + "tooltipBg": "rgba(0, 0, 0, 0.8)", + "tooltipTitleColor": "#ffffff", + "tooltipBodyColor": "#ffffff", + "tooltipBorderColor": "transparent", + "tooltipBorderWidth": 0, + "tooltipCornerRadius": 8, + "uppercaseTooltip": False + } +} + +# CSS styles for each theme +THEME_CSS = { + "brutalism": """ + :root { + --bg-base: #ffffff; + --bg-primary: #ffffff; + --bg-secondary: #f0f0f0; + --bg-tertiary: #e0e0e0; + --text-primary: #000000; + --text-secondary: #333333; + --text-muted: #666666; + --border-color: #000000; + --border-width: 3px; + --accent-color: #ff0000; + --accent-secondary: #0000ff; + --accent-tertiary: #ffff00; + --selection-bg: #ffff00; + --selection-text: #000000; + --shadow-offset: 6px; + } + + [data-theme="dark"] { + --bg-base: #000000; + --bg-primary: #000000; + --bg-secondary: #111111; + --bg-tertiary: #222222; + --text-primary: #ffffff; + --text-secondary: #cccccc; + --text-muted: #888888; + --border-color: #ffffff; + --accent-color: #ff3333; + --accent-secondary: #3333ff; + --accent-tertiary: #ffff33; + --selection-bg: #ff3333; + --selection-text: #000000; + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-base: #000000; + --bg-primary: #000000; + --bg-secondary: #111111; + --bg-tertiary: #222222; + --text-primary: #ffffff; + --text-secondary: #cccccc; + --text-muted: #888888; + --border-color: #ffffff; + --accent-color: #ff3333; + --accent-secondary: #3333ff; + --accent-tertiary: #ffff33; + --selection-bg: #ff3333; + --selection-text: #000000; + } + } + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: var(--bg-base); + background-image: __BACKGROUND_IMAGE_CUSTOM__; + background-size: cover; + background-position: center; + background-attachment: fixed; + min-height: 100vh; + color: var(--text-primary); + line-height: 1.4; + } + + /* Theme Toggle Button */ + .theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + width: 50px; + height: 50px; + border: var(--border-width) solid var(--border-color); + background: var(--bg-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color); + transition: transform 0.1s, box-shadow 0.1s; + } + + .theme-toggle:hover { + transform: translate(-2px, -2px); + box-shadow: calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color); + } + + .theme-toggle:active { + transform: translate(2px, 2px); + box-shadow: calc(var(--shadow-offset) - 2px) calc(var(--shadow-offset) - 2px) 0 var(--border-color); + } + + .theme-toggle .icon { + line-height: 1; + } + + .theme-toggle .icon-auto { + position: relative; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + } + + .theme-toggle .icon-auto::before, + .theme-toggle .icon-auto::after { + position: absolute; + font-size: 20px; + line-height: 1; + } + + .theme-toggle .icon-auto::before { + content: '\\2600\\FE0F'; + clip-path: inset(0 50% 0 0); + } + + .theme-toggle .icon-auto::after { + content: '\\D83C\\DF19'; + clip-path: inset(0 0 0 50%); + } + + /* Card Style */ + .card { + background: var(--bg-primary); + border: var(--border-width) solid var(--border-color); + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color); + margin-bottom: 24px; + overflow: hidden; + } + + .card-header { + padding: 12px 16px; + border-bottom: var(--border-width) solid var(--border-color); + background: var(--accent-tertiary); + } + + .card-title { + font-size: 18px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--text-primary); + } + + [data-theme="dark"] .card-title { + color: #000000; + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .card-title { + color: #000000; + } + } + + .card-content { + padding: 20px; + } + + .summaries-content { + padding: 0; + padding-top: 16px; + } + + /* Filters */ + .filters { + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; + } + + .filter-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + padding: 8px 12px; + background: var(--bg-secondary); + border: 2px solid var(--border-color); + font-weight: 700; + text-transform: uppercase; + font-size: 12px; + transition: transform 0.1s, box-shadow 0.1s; + } + + .filter-label:hover { + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0 var(--border-color); + } + + .filter-label input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + background: var(--bg-primary); + cursor: pointer; + position: relative; + } + + .filter-label input[type="checkbox"]:checked { + background: var(--accent-color); + } + + .filter-label input[type="checkbox"]:checked::after { + content: '\\2715'; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 14px; + font-weight: 900; + color: var(--bg-primary); + } + + .filter-label .service-name { + text-transform: uppercase; + font-weight: 700; + color: var(--text-primary); + } + + .filter-label .service-count { + color: var(--text-muted); + font-size: 11px; + } + + /* Stats */ + .stats { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; + } + + .stat { + text-align: center; + padding: 20px 30px; + background: var(--bg-secondary); + border: var(--border-width) solid var(--border-color); + min-width: 150px; + } + + .stat-value { + font-size: 36px; + font-weight: 900; + color: var(--accent-color); + font-variant-numeric: tabular-nums; + } + + .stat-label { + font-size: 11px; + color: var(--text-muted); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 700; + } + + /* Charts */ + .charts-wrapper { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; + } + + .chart-container { + flex: 1; + min-width: 280px; + max-width: 450px; + padding: 16px; + background: var(--bg-secondary); + border: var(--border-width) solid var(--border-color); + } + + .chart-title { + font-size: 14px; + font-weight: 900; + text-align: center; + margin-bottom: 12px; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 1px; + } + + @media (max-width: 700px) { + .charts-wrapper { + flex-direction: column; + align-items: center; + } + .chart-container { + max-width: 100%; + width: 100%; + } + } + + /* Tabs */ + .tabs { + display: flex; + gap: 0; + padding: 0 16px; + margin-bottom: -3px; + position: relative; + z-index: 1; + } + + .tab { + padding: 12px 24px; + cursor: pointer; + color: var(--text-secondary); + font-weight: 900; + text-transform: uppercase; + font-size: 12px; + letter-spacing: 1px; + background: var(--bg-tertiary); + border: var(--border-width) solid var(--border-color); + border-bottom: none; + margin-right: -3px; + transition: none; + } + + .tab:hover { + color: var(--text-primary); + background: var(--bg-secondary); + } + + .tab.active { + color: var(--accent-color); + background: var(--bg-secondary); + position: relative; + } + + .tab.active::after { + content: ''; + position: absolute; + bottom: -3px; + left: 0; + right: 0; + height: 3px; + background: var(--bg-secondary); + } + + .tab-content { + background: var(--bg-secondary); + border: var(--border-width) solid var(--border-color); + padding: 16px; + } + + .tab-panel { + display: none; + } + + .tab-panel.active { + display: block; + } + + /* Tables */ + .table-wrapper { + overflow-x: auto; + } + + table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + } + + th { + padding: 12px 16px; + text-align: left; + font-weight: 900; + color: var(--text-primary); + border-bottom: var(--border-width) solid var(--border-color); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; + background: var(--bg-tertiary); + } + + td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + } + + tr:hover td { + background: var(--selection-bg); + color: var(--selection-text); + } + + tr:hover .service-badge { + background: var(--border-color); + color: var(--bg-primary); + } + + tr:last-child td { + border-bottom: none; + } + + .time { + font-variant-numeric: tabular-nums; + font-weight: 700; + } + + .percent { + font-variant-numeric: tabular-nums; + text-align: right; + color: var(--text-muted); + font-weight: 700; + } + + .color-box { + display: inline-block; + width: 14px; + height: 14px; + margin-right: 8px; + vertical-align: middle; + border: 2px solid var(--border-color); + } + + .service-badge { + display: inline-block; + font-size: 10px; + padding: 2px 6px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-muted); + margin-left: 8px; + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.5px; + } + + .category-badge { + display: inline-block; + font-size: 10px; + padding: 2px 6px; + background: var(--accent-secondary); + border: 1px solid var(--border-color); + color: var(--bg-primary); + margin-left: 4px; + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.5px; + } + + .no-data { + text-align: center; + padding: 40px; + color: var(--text-muted); + font-weight: 700; + text-transform: uppercase; + } + + /* Others row expansion */ + .others-row { + cursor: pointer; + } + + .others-row td:first-child::before { + content: '\\25B6'; + display: inline-block; + margin-right: 8px; + font-size: 10px; + transition: transform 0.1s; + } + + .others-row.expanded td:first-child::before { + transform: rotate(90deg); + } + + .others-detail { + display: none; + background: var(--bg-tertiary); + } + + .others-detail.visible { + display: table-row; + } + + .others-detail td { + padding-left: 32px; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 12px; + height: 12px; + } + + ::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + } + + ::-webkit-scrollbar-thumb { + background: var(--text-muted); + border: 1px solid var(--border-color); + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); + } + + /* Responsive */ + @media (max-width: 600px) { + body { + padding: 12px; + } + + .stats { + gap: 12px; + } + + .stat { + padding: 16px 20px; + min-width: 100px; + } + + .stat-value { + font-size: 28px; + } + + .filter-label { + padding: 6px 10px; + font-size: 11px; + } + + .tabs { + padding: 0 12px; + } + + .tab { + padding: 10px 16px; + font-size: 11px; + } + + .theme-toggle { + width: 44px; + height: 44px; + font-size: 18px; + } + } + + /* Scroll to Top Button */ + .scroll-top { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + width: 50px; + height: 50px; + border: var(--border-width) solid var(--border-color); + background: var(--bg-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--border-color); + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s, transform 0.1s, box-shadow 0.1s; + } + + .scroll-top.visible { + opacity: 1; + visibility: visible; + } + + .scroll-top:hover { + transform: translate(-2px, -2px); + box-shadow: calc(var(--shadow-offset) + 2px) calc(var(--shadow-offset) + 2px) 0 var(--border-color); + } + + .scroll-top:active { + transform: translate(2px, 2px); + box-shadow: calc(var(--shadow-offset) - 2px) calc(var(--shadow-offset) - 2px) 0 var(--border-color); + } + + .scroll-top-arrow { + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 12px solid var(--text-primary); + } + + @media (max-width: 600px) { + .scroll-top { + width: 44px; + height: 44px; + } + + .scroll-top-arrow { + border-left-width: 8px; + border-right-width: 8px; + border-bottom-width: 10px; + } + } + """, + "glassmorphism": """ + :root { + --bg-primary: rgba(255, 255, 255, 0.25); + --bg-secondary: rgba(255, 255, 255, 0.15); + --bg-tertiary: rgba(255, 255, 255, 0.1); + --text-primary: #1a1a2e; + --text-secondary: #4a4a6a; + --text-muted: #6a6a8a; + --border-color: rgba(255, 255, 255, 0.3); + --shadow-color: rgba(0, 0, 0, 0.1); + --accent-color: #6366f1; + --accent-hover: #4f46e5; + --selection-bg: rgba(99, 102, 241, 0.3); + --glass-blur: 20px; + --card-radius: 16px; + } + + [data-theme="dark"] { + --bg-primary: rgba(30, 30, 50, 0.6); + --bg-secondary: rgba(40, 40, 70, 0.5); + --bg-tertiary: rgba(50, 50, 80, 0.4); + --text-primary: #f0f0f5; + --text-secondary: #c0c0d0; + --text-muted: #9090a0; + --border-color: rgba(255, 255, 255, 0.1); + --shadow-color: rgba(0, 0, 0, 0.3); + --accent-color: #818cf8; + --accent-hover: #6366f1; + --selection-bg: rgba(129, 140, 248, 0.3); + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-primary: rgba(30, 30, 50, 0.6); + --bg-secondary: rgba(40, 40, 70, 0.5); + --bg-tertiary: rgba(50, 50, 80, 0.4); + --text-primary: #f0f0f5; + --text-secondary: #c0c0d0; + --text-muted: #9090a0; + --border-color: rgba(255, 255, 255, 0.1); + --shadow-color: rgba(0, 0, 0, 0.3); + --accent-color: #818cf8; + --accent-hover: #6366f1; + --selection-bg: rgba(129, 140, 248, 0.3); + } + } + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + font-size: 14px; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #667eea; + background-image: url('__BACKGROUND_IMAGE__'); + background-size: cover; + background-position: center; + background-attachment: fixed; + min-height: 100vh; + color: var(--text-primary); + line-height: 1.5; + } + + /* Theme Toggle Button */ + .theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid var(--border-color); + background: var(--bg-primary); + backdrop-filter: blur(var(--glass-blur)); + -webkit-backdrop-filter: blur(var(--glass-blur)); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + box-shadow: 0 8px 32px var(--shadow-color); + transition: all 0.3s ease; + } + + .theme-toggle:hover { + transform: scale(1.1); + background: var(--bg-secondary); + } + + .theme-toggle .icon { + transition: transform 0.3s ease; + } + + .theme-toggle .icon-auto { + position: relative; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + } + + .theme-toggle .icon-auto::before, + .theme-toggle .icon-auto::after { + position: absolute; + font-size: 20px; + line-height: 1; + } + + .theme-toggle .icon-auto::before { + content: '\\2600\\FE0F'; + clip-path: inset(0 50% 0 0); + } + + .theme-toggle .icon-auto::after { + content: '\\D83C\\DF19'; + clip-path: inset(0 0 0 50%); + } + + /* Card Style */ + .card { + background: var(--bg-primary); + backdrop-filter: blur(var(--glass-blur)); + -webkit-backdrop-filter: blur(var(--glass-blur)); + border-radius: var(--card-radius); + border: 1px solid var(--border-color); + box-shadow: 0 8px 32px var(--shadow-color); + margin-bottom: 24px; + overflow: hidden; + transition: all 0.3s ease; + } + + .card:hover { + box-shadow: 0 12px 40px var(--shadow-color); + } + + .card-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-tertiary); + } + + .card-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + } + + .card-content { + padding: 20px; + } + + .summaries-content { + padding: 0; + padding-top: 16px; + } + + /* Filters */ + .filters { + display: flex; + justify-content: center; + gap: 16px; + flex-wrap: wrap; + } + + .filter-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + padding: 8px 16px; + border-radius: 24px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + transition: all 0.2s ease; + } + + .filter-label:hover { + background: var(--bg-tertiary); + } + + .filter-label input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 4px; + border: 2px solid var(--text-muted); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + } + + .filter-label input[type="checkbox"]:checked { + background: var(--accent-color); + border-color: var(--accent-color); + } + + .filter-label input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + + .filter-label .service-name { + text-transform: capitalize; + font-weight: 500; + color: var(--text-primary); + } + + .filter-label .service-count { + color: var(--text-muted); + font-size: 12px; + } + + /* Stats */ + .stats { + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; + } + + .stat { + text-align: center; + padding: 20px 32px; + background: var(--bg-secondary); + border-radius: 12px; + border: 1px solid var(--border-color); + min-width: 140px; + } + + .stat-value { + font-size: 28px; + font-weight: 700; + color: var(--accent-color); + font-variant-numeric: tabular-nums; + } + + .stat-label { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* Charts */ + .charts-wrapper { + display: flex; + gap: 24px; + justify-content: center; + flex-wrap: wrap; + } + + .chart-container { + flex: 1; + min-width: 280px; + max-width: 450px; + padding: 16px; + background: var(--bg-secondary); + border-radius: 12px; + border: 1px solid var(--border-color); + } + + .chart-title { + font-size: 14px; + font-weight: 600; + text-align: center; + margin-bottom: 12px; + color: var(--text-primary); + } + + @media (max-width: 700px) { + .charts-wrapper { + flex-direction: column; + align-items: center; + } + .chart-container { + max-width: 100%; + width: 100%; + } + } + + /* Tabs */ + .tabs { + display: flex; + gap: 8px; + padding: 0 20px; + margin-bottom: -1px; + position: relative; + z-index: 1; + } + + .tab { + padding: 12px 24px; + cursor: pointer; + color: var(--text-secondary); + font-weight: 500; + border-radius: 12px 12px 0 0; + background: var(--bg-tertiary); + border: 1px solid transparent; + border-bottom: none; + transition: all 0.2s ease; + } + + .tab:hover { + color: var(--text-primary); + background: var(--bg-secondary); + } + + .tab.active { + color: var(--accent-color); + background: var(--bg-secondary); + border-color: var(--border-color); + } + + .tab-content { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0 12px 12px 12px; + padding: 16px; + } + + .tab-panel { + display: none; + } + + .tab-panel.active { + display: block; + } + + /* Tables */ + .table-wrapper { + overflow-x: auto; + } + + table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + } + + th { + padding: 12px 16px; + text-align: left; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 2px solid var(--border-color); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.5px; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + } + + tr:hover td { + background: var(--selection-bg); + } + + tr:last-child td { + border-bottom: none; + } + + .time { + font-variant-numeric: tabular-nums; + font-weight: 500; + } + + .percent { + font-variant-numeric: tabular-nums; + text-align: right; + color: var(--text-muted); + } + + .color-box { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 8px; + vertical-align: middle; + border-radius: 3px; + } + + .service-badge { + display: inline-block; + font-size: 10px; + padding: 2px 8px; + background: var(--bg-tertiary); + border-radius: 12px; + color: var(--text-muted); + margin-left: 8px; + text-transform: capitalize; + font-weight: 500; + } + + .category-badge { + display: inline-block; + font-size: 10px; + padding: 2px 8px; + background: rgba(99, 102, 241, 0.2); + border-radius: 12px; + color: var(--accent-color); + margin-left: 4px; + text-transform: capitalize; + font-weight: 500; + } + + .no-data { + text-align: center; + padding: 40px; + color: var(--text-muted); + } + + /* Others row expansion */ + .others-row { + cursor: pointer; + } + + .others-row td:first-child::before { + content: ''; + display: inline-block; + width: 0; + height: 0; + border-left: 6px solid var(--text-muted); + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + margin-right: 8px; + transition: transform 0.2s ease; + } + + .others-row.expanded td:first-child::before { + transform: rotate(90deg); + } + + .others-detail { + display: none; + background: var(--bg-tertiary); + } + + .others-detail.visible { + display: table-row; + } + + .others-detail td { + padding-left: 32px; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); + } + + /* Responsive */ + @media (max-width: 600px) { + body { + padding: 12px; + } + + .stats { + gap: 12px; + } + + .stat { + padding: 16px 20px; + min-width: 100px; + } + + .stat-value { + font-size: 22px; + } + + .filter-label { + padding: 6px 12px; + font-size: 13px; + } + + .tabs { + padding: 0 12px; + } + + .tab { + padding: 10px 16px; + font-size: 13px; + } + + .theme-toggle { + width: 40px; + height: 40px; + font-size: 16px; + } + } + + /* Scroll to Top Button */ + .scroll-top { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid var(--border-color); + background: var(--bg-primary); + backdrop-filter: blur(var(--glass-blur)); + -webkit-backdrop-filter: blur(var(--glass-blur)); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 32px var(--shadow-color); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + } + + .scroll-top.visible { + opacity: 1; + visibility: visible; + } + + .scroll-top:hover { + transform: scale(1.1); + background: var(--bg-secondary); + } + + .scroll-top-arrow { + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 10px solid var(--text-primary); + } + + @media (max-width: 600px) { + .scroll-top { + width: 40px; + height: 40px; + } + + .scroll-top-arrow { + border-left-width: 6px; + border-right-width: 6px; + border-bottom-width: 8px; + } + } + """, + "neumorphism": """ + :root { + --bg-base: #e0e5ec; + --bg-primary: #e0e5ec; + --bg-secondary: #e0e5ec; + --bg-tertiary: #d1d9e6; + --text-primary: #2d3436; + --text-secondary: #4a5568; + --text-muted: #718096; + --accent-color: #6366f1; + --accent-hover: #4f46e5; + --selection-bg: rgba(99, 102, 241, 0.15); + --shadow-light: rgba(255, 255, 255, 0.8); + --shadow-dark: rgba(163, 177, 198, 0.6); + --shadow-inset-light: rgba(255, 255, 255, 0.7); + --shadow-inset-dark: rgba(163, 177, 198, 0.5); + --card-radius: 20px; + --border-color: transparent; + } + + [data-theme="dark"] { + --bg-base: #2d3436; + --bg-primary: #2d3436; + --bg-secondary: #2d3436; + --bg-tertiary: #353b3d; + --text-primary: #f0f0f5; + --text-secondary: #b0b8c0; + --text-muted: #8090a0; + --accent-color: #818cf8; + --accent-hover: #6366f1; + --selection-bg: rgba(129, 140, 248, 0.2); + --shadow-light: rgba(255, 255, 255, 0.05); + --shadow-dark: rgba(0, 0, 0, 0.4); + --shadow-inset-light: rgba(255, 255, 255, 0.03); + --shadow-inset-dark: rgba(0, 0, 0, 0.3); + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-base: #2d3436; + --bg-primary: #2d3436; + --bg-secondary: #2d3436; + --bg-tertiary: #353b3d; + --text-primary: #f0f0f5; + --text-secondary: #b0b8c0; + --text-muted: #8090a0; + --accent-color: #818cf8; + --accent-hover: #6366f1; + --selection-bg: rgba(129, 140, 248, 0.2); + --shadow-light: rgba(255, 255, 255, 0.05); + --shadow-dark: rgba(0, 0, 0, 0.4); + --shadow-inset-light: rgba(255, 255, 255, 0.03); + --shadow-inset-dark: rgba(0, 0, 0, 0.3); + } + } + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + font-size: 14px; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: var(--bg-base); + min-height: 100vh; + color: var(--text-primary); + line-height: 1.5; + } + + /* Theme Toggle Button */ + .theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + width: 48px; + height: 48px; + border-radius: 50%; + border: none; + background: var(--bg-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + box-shadow: + 6px 6px 12px var(--shadow-dark), + -6px -6px 12px var(--shadow-light); + transition: all 0.3s ease; + } + + .theme-toggle:hover { + transform: scale(1.05); + } + + .theme-toggle:active { + box-shadow: + inset 4px 4px 8px var(--shadow-inset-dark), + inset -4px -4px 8px var(--shadow-inset-light); + } + + .theme-toggle .icon { + transition: transform 0.3s ease; + } + + .theme-toggle .icon-auto { + position: relative; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + } + + .theme-toggle .icon-auto::before, + .theme-toggle .icon-auto::after { + position: absolute; + font-size: 20px; + line-height: 1; + } + + .theme-toggle .icon-auto::before { + content: '\\2600\\FE0F'; + clip-path: inset(0 50% 0 0); + } + + .theme-toggle .icon-auto::after { + content: '\\D83C\\DF19'; + clip-path: inset(0 0 0 50%); + } + + /* Card Style */ + .card { + background: var(--bg-primary); + border-radius: var(--card-radius); + box-shadow: + 8px 8px 16px var(--shadow-dark), + -8px -8px 16px var(--shadow-light); + margin-bottom: 24px; + overflow: hidden; + transition: all 0.3s ease; + } + + .card-header { + padding: 16px 20px; + background: var(--bg-tertiary); + border-radius: var(--card-radius) var(--card-radius) 0 0; + } + + .card-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + } + + .card-content { + padding: 20px; + } + + .summaries-content { + padding: 0; + padding-top: 16px; + } + + /* Filters */ + .filters { + display: flex; + justify-content: center; + gap: 16px; + flex-wrap: wrap; + } + + .filter-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + padding: 10px 18px; + border-radius: 30px; + background: var(--bg-secondary); + box-shadow: + 4px 4px 8px var(--shadow-dark), + -4px -4px 8px var(--shadow-light); + transition: all 0.2s ease; + } + + .filter-label:hover { + transform: translateY(-2px); + } + + .filter-label:active { + box-shadow: + inset 3px 3px 6px var(--shadow-inset-dark), + inset -3px -3px 6px var(--shadow-inset-light); + } + + .filter-label input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 20px; + height: 20px; + border-radius: 6px; + background: var(--bg-secondary); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + box-shadow: + inset 2px 2px 4px var(--shadow-inset-dark), + inset -2px -2px 4px var(--shadow-inset-light); + } + + .filter-label input[type="checkbox"]:checked { + background: var(--accent-color); + box-shadow: + inset 2px 2px 4px rgba(0, 0, 0, 0.2), + inset -2px -2px 4px rgba(255, 255, 255, 0.1); + } + + .filter-label input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 6px; + top: 3px; + width: 5px; + height: 9px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + + .filter-label .service-name { + text-transform: capitalize; + font-weight: 500; + color: var(--text-primary); + } + + .filter-label .service-count { + color: var(--text-muted); + font-size: 12px; + } + + /* Stats */ + .stats { + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; + } + + .stat { + text-align: center; + padding: 24px 36px; + background: var(--bg-secondary); + border-radius: 16px; + min-width: 140px; + box-shadow: + 6px 6px 12px var(--shadow-dark), + -6px -6px 12px var(--shadow-light); + } + + .stat-value { + font-size: 28px; + font-weight: 700; + color: var(--accent-color); + font-variant-numeric: tabular-nums; + } + + .stat-label { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* Charts */ + .charts-wrapper { + display: flex; + gap: 24px; + justify-content: center; + flex-wrap: wrap; + } + + .chart-container { + flex: 1; + min-width: 280px; + max-width: 450px; + padding: 20px; + background: var(--bg-secondary); + border-radius: 16px; + box-shadow: + inset 4px 4px 8px var(--shadow-inset-dark), + inset -4px -4px 8px var(--shadow-inset-light); + } + + .chart-title { + font-size: 14px; + font-weight: 600; + text-align: center; + margin-bottom: 12px; + color: var(--text-primary); + } + + @media (max-width: 700px) { + .charts-wrapper { + flex-direction: column; + align-items: center; + } + .chart-container { + max-width: 100%; + width: 100%; + } + } + + /* Tabs */ + .tabs { + display: flex; + gap: 8px; + padding: 0 20px; + margin-bottom: -1px; + position: relative; + z-index: 1; + } + + .tab { + padding: 12px 24px; + cursor: pointer; + color: var(--text-secondary); + font-weight: 500; + border-radius: 16px 16px 0 0; + background: var(--bg-tertiary); + transition: all 0.2s ease; + box-shadow: + 4px -4px 8px var(--shadow-dark), + -4px -4px 8px var(--shadow-light); + } + + .tab:hover { + color: var(--text-primary); + } + + .tab.active { + color: var(--accent-color); + background: var(--bg-secondary); + box-shadow: + inset 2px 2px 4px var(--shadow-inset-dark), + inset -2px -2px 4px var(--shadow-inset-light); + } + + .tab-content { + background: var(--bg-secondary); + border-radius: 0 16px 16px 16px; + padding: 16px; + box-shadow: + inset 4px 4px 8px var(--shadow-inset-dark), + inset -4px -4px 8px var(--shadow-inset-light); + } + + .tab-panel { + display: none; + } + + .tab-panel.active { + display: block; + } + + /* Tables */ + .table-wrapper { + overflow-x: auto; + } + + table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + } + + th { + padding: 12px 16px; + text-align: left; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 2px solid var(--bg-tertiary); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.5px; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid var(--bg-tertiary); + color: var(--text-primary); + } + + tr:hover td { + background: var(--selection-bg); + } + + tr:last-child td { + border-bottom: none; + } + + .time { + font-variant-numeric: tabular-nums; + font-weight: 500; + } + + .percent { + font-variant-numeric: tabular-nums; + text-align: right; + color: var(--text-muted); + } + + .color-box { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 8px; + vertical-align: middle; + border-radius: 4px; + box-shadow: + 2px 2px 4px var(--shadow-dark), + -2px -2px 4px var(--shadow-light); + } + + .service-badge { + display: inline-block; + font-size: 10px; + padding: 3px 10px; + background: var(--bg-tertiary); + border-radius: 12px; + color: var(--text-muted); + margin-left: 8px; + text-transform: capitalize; + font-weight: 500; + box-shadow: + inset 1px 1px 2px var(--shadow-inset-dark), + inset -1px -1px 2px var(--shadow-inset-light); + } + + .category-badge { + display: inline-block; + font-size: 10px; + padding: 3px 10px; + background: var(--bg-tertiary); + border-radius: 12px; + color: var(--accent-color); + margin-left: 4px; + text-transform: capitalize; + font-weight: 500; + box-shadow: + inset 1px 1px 2px var(--shadow-inset-dark), + inset -1px -1px 2px var(--shadow-inset-light); + } + + .no-data { + text-align: center; + padding: 40px; + color: var(--text-muted); + } + + /* Others row expansion */ + .others-row { + cursor: pointer; + } + + .others-row td:first-child::before { + content: ''; + display: inline-block; + width: 0; + height: 0; + border-left: 6px solid var(--text-muted); + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + margin-right: 8px; + transition: transform 0.2s ease; + } + + .others-row.expanded td:first-child::before { + transform: rotate(90deg); + } + + .others-detail { + display: none; + background: var(--bg-tertiary); + } + + .others-detail.visible { + display: table-row; + } + + .others-detail td { + padding-left: 32px; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); + } + + /* Responsive */ + @media (max-width: 600px) { + body { + padding: 12px; + } + + .stats { + gap: 12px; + } + + .stat { + padding: 16px 20px; + min-width: 100px; + } + + .stat-value { + font-size: 22px; + } + + .filter-label { + padding: 8px 14px; + font-size: 13px; + } + + .tabs { + padding: 0 12px; + } + + .tab { + padding: 10px 16px; + font-size: 13px; + } + + .theme-toggle { + width: 40px; + height: 40px; + font-size: 16px; + } + } + + /* Scroll to Top Button */ + .scroll-top { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + width: 48px; + height: 48px; + border-radius: 50%; + border: none; + background: var(--bg-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 6px 6px 12px var(--shadow-dark), + -6px -6px 12px var(--shadow-light); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + } + + .scroll-top.visible { + opacity: 1; + visibility: visible; + } + + .scroll-top:hover { + transform: scale(1.05); + } + + .scroll-top:active { + box-shadow: + inset 4px 4px 8px var(--shadow-inset-dark), + inset -4px -4px 8px var(--shadow-inset-light); + } + + .scroll-top-arrow { + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 10px solid var(--text-primary); + } + + @media (max-width: 600px) { + .scroll-top { + width: 40px; + height: 40px; + } + + .scroll-top-arrow { + border-left-width: 6px; + border-right-width: 6px; + border-bottom-width: 8px; + } + } + """ +} + + +def get_theme_css(style: str) -> str: + """Get the CSS for a given style.""" + return THEME_CSS.get(style, THEME_CSS["glassmorphism"]) + + +def get_theme_config(style: str) -> str: + """Get the theme config as JSON string for a given style.""" + config = THEME_CONFIGS.get(style, THEME_CONFIGS["glassmorphism"]) + return json.dumps(config) diff --git a/templates/brutalism.html b/templates/brutalism.html deleted file mode 100644 index f01d4c1..0000000 --- a/templates/brutalism.html +++ /dev/null @@ -1,1335 +0,0 @@ - - - - - - Lutris Playtime Report - - - - - - - - - - -
-
-

Lutris Playtime Report

-
-
-
-
-
- -
-
-

Statistics

-
-
-
-
-
__TOTAL_LIBRARY__
-
Games in Library
-
-
-
0
-
Games Played
-
-
-
0h
-
Total Playtime
-
-
-
-
- -
-
-

Playtime Distribution

-
-
-
-
-
Top Games
- -
-
-
By Category
- -
-
-
-
- -
-
-

Summaries

-
-
-
-
Top Games
-
By Category
-
By Runner
-
-
-
-
- - - - - - - - - - -
#GamePlaytime%
-
-
-
-
- - - - - - - - - - -
#CategoryPlaytime%
-
-
-
-
- - - - - - - - - - -
#RunnerPlaytime%
-
-
-
-
-
- - - - diff --git a/templates/glassmorphism.html b/templates/modern.html similarity index 62% rename from templates/glassmorphism.html rename to templates/modern.html index 49701c0..cdda685 100644 --- a/templates/glassmorphism.html +++ b/templates/modern.html @@ -6,578 +6,8 @@ Lutris Playtime Report @@ -591,7 +21,7 @@ -
+

Lutris Playtime Report

@@ -600,7 +30,7 @@
-
+

Statistics

@@ -622,7 +52,7 @@
-
+

Playtime Distribution

@@ -640,11 +70,11 @@
-
+

Summaries

-
+
Top Games
By Category
@@ -718,11 +148,11 @@ themeIcon.className = 'icon icon-auto'; themeToggle.title = 'Theme: Auto (click to change)'; } else if (theme === 'light') { - themeIcon.textContent = '☀️'; + themeIcon.textContent = '\u2600\uFE0F'; themeIcon.className = 'icon'; themeToggle.title = 'Theme: Light (click to change)'; } else { - themeIcon.textContent = '🌙'; + themeIcon.textContent = '\uD83C\uDF19'; themeIcon.className = 'icon'; themeToggle.title = 'Theme: Dark (click to change)'; } @@ -765,11 +195,8 @@ const allGames = __ALL_GAMES__; const topN = __TOP_N__; - const colors = [ - '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316', - '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', - '#64748b' - ]; + // Theme-specific configuration + const themeConfig = __THEME_CONFIG__; function formatTime(hours) { const h = Math.floor(hours); @@ -890,9 +317,16 @@ function getChartTextColor() { const theme = themes[currentThemeIndex]; - if (theme === 'dark') return '#f0f0f5'; - if (theme === 'light') return '#1a1a2e'; - return getSystemTheme() === 'dark' ? '#f0f0f5' : '#1a1a2e'; + if (theme === 'dark') return themeConfig.textColorDark; + if (theme === 'light') return themeConfig.textColorLight; + return getSystemTheme() === 'dark' ? themeConfig.textColorDark : themeConfig.textColorLight; + } + + function getChartBorderColor() { + const theme = themes[currentThemeIndex]; + if (theme === 'dark') return themeConfig.borderColorDark; + if (theme === 'light') return themeConfig.borderColorLight; + return getSystemTheme() === 'dark' ? themeConfig.borderColorDark : themeConfig.borderColorLight; } function updateChartColors() { @@ -932,6 +366,7 @@ } const textColor = getChartTextColor(); + const borderColor = getChartBorderColor(); chart = new Chart(ctx, { type: 'doughnut', @@ -939,9 +374,9 @@ labels: chartData.map(g => g.name), datasets: [{ data: chartData.map(g => g.playtime), - backgroundColor: colors.slice(0, chartData.length), - borderColor: 'rgba(255, 255, 255, 0.2)', - borderWidth: 2 + backgroundColor: themeConfig.colors.slice(0, chartData.length), + borderColor: borderColor, + borderWidth: themeConfig.borderWidth }] }, options: { @@ -952,29 +387,34 @@ labels: { color: textColor, font: { - family: "'Inter', sans-serif", - size: 11 + family: themeConfig.fontFamily, + size: 11, + weight: themeConfig.fontWeight }, padding: 12, usePointStyle: true, - pointStyle: 'circle' + pointStyle: themeConfig.pointStyle } }, tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - titleFont: { weight: 'bold' }, - bodyFont: { weight: 'normal' }, - cornerRadius: 8, + backgroundColor: themeConfig.tooltipBg, + titleColor: themeConfig.tooltipTitleColor, + bodyColor: themeConfig.tooltipBodyColor, + borderColor: themeConfig.tooltipBorderColor, + borderWidth: themeConfig.tooltipBorderWidth, + titleFont: { weight: 'bold', family: themeConfig.fontFamily }, + bodyFont: { weight: 'normal', family: themeConfig.fontFamily }, + cornerRadius: themeConfig.tooltipCornerRadius, padding: 12, callbacks: { title: function(context) { - return context[0].label; + return themeConfig.uppercaseTooltip ? context[0].label.toUpperCase() : context[0].label; }, beforeBody: function(context) { const index = context[0].dataIndex; const service = chartData[index].service; if (service && service !== 'others') { - return service.charAt(0).toUpperCase() + service.slice(1); + return themeConfig.uppercaseTooltip ? service.toUpperCase() : service.charAt(0).toUpperCase() + service.slice(1); } return ''; }, @@ -1010,9 +450,9 @@ labels: categoriesChartData.map(c => c.name), datasets: [{ data: categoriesChartData.map(c => c.playtime), - backgroundColor: colors.slice(0, categoriesChartData.length), - borderColor: 'rgba(255, 255, 255, 0.2)', - borderWidth: 2 + backgroundColor: themeConfig.colors.slice(0, categoriesChartData.length), + borderColor: borderColor, + borderWidth: themeConfig.borderWidth }] }, options: { @@ -1023,18 +463,25 @@ labels: { color: textColor, font: { - family: "'Inter', sans-serif", - size: 11 + family: themeConfig.fontFamily, + size: 11, + weight: themeConfig.fontWeight }, padding: 12, usePointStyle: true, - pointStyle: 'circle' + pointStyle: themeConfig.pointStyle } }, tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - cornerRadius: 8, + backgroundColor: themeConfig.tooltipBg, + titleColor: themeConfig.tooltipTitleColor, + bodyColor: themeConfig.tooltipBodyColor, + borderColor: themeConfig.tooltipBorderColor, + borderWidth: themeConfig.tooltipBorderWidth, + cornerRadius: themeConfig.tooltipCornerRadius, padding: 12, + titleFont: { family: themeConfig.fontFamily }, + bodyFont: { family: themeConfig.fontFamily }, callbacks: { label: function(context) { const value = context.raw; @@ -1069,7 +516,7 @@ row.innerHTML = ` ${index + 1} - + ${game.name}${serviceBadge}${categoriesBadges} ${formatTime(game.playtime)} @@ -1123,7 +570,7 @@ row.innerHTML = ` ${index + 1} - + ${cat.name} ${cat.gameCount} games ${formatTime(cat.playtime)} @@ -1142,7 +589,7 @@ othersRow.innerHTML = ` ${othersIndex + 1} - + Others (${otherCategories.length} categories) ${formatTime(othersPlaytime)} @@ -1188,7 +635,7 @@ row.innerHTML = ` ${index + 1} - + ${runner.name} ${runner.gameCount} games ${formatTime(runner.playtime)} @@ -1207,7 +654,7 @@ othersRow.innerHTML = ` ${othersIndex + 1} - + Others (${otherRunners.length} runners) ${formatTime(othersPlaytime)} diff --git a/templates/neumorphism.html b/templates/neumorphism.html deleted file mode 100644 index 6712ba5..0000000 --- a/templates/neumorphism.html +++ /dev/null @@ -1,1319 +0,0 @@ - - - - - - Lutris Playtime Report - - - - - - - - - - -
-
-

Lutris Playtime Report

-
-
-
-
-
- -
-
-

Statistics

-
-
-
-
-
__TOTAL_LIBRARY__
-
Games in Library
-
-
-
0
-
Games Played
-
-
-
0h
-
Total Playtime
-
-
-
-
- -
-
-

Playtime Distribution

-
-
-
-
-
Top Games
- -
-
-
By Category
- -
-
-
-
- -
-
-

Summaries

-
-
-
-
Top Games
-
By Category
-
By Runner
-
-
-
-
- - - - - - - - - - -
#GamePlaytime%
-
-
-
-
- - - - - - - - - - -
#CategoryPlaytime%
-
-
-
-
- - - - - - - - - - -
#RunnerPlaytime%
-
-
-
-
-
- - - -