diff --git a/.gitignore b/.gitignore index 6133971..1948e14 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ csv_files/ dist/ logs/ !config/deck.json -RELEASE_NOTES.md \ No newline at end of file +RELEASE_NOTES.md +*.bkp \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da8b1d..250583c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ## [Unreleased] +## [2.0.1] - 2025-08-28 + ### Added - Web UI performance: optional virtualized grids/lists in Step 5 and Owned (enable with `WEB_VIRTUALIZE=1`). - Virtualization diagnostics overlay (when `SHOW_DIAGNOSTICS=1`); press `v` to toggle per‑grid overlays and a global summary bubble with visible range, totals, render time, and counters. @@ -49,6 +51,7 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning - Request-ID middleware assigns `X-Request-ID` to all responses and includes it in JSON error payloads - `/status/logs?tail=N` endpoint (read-only) to fetch a recent log tail for quick diagnostics - Tooltip Copy action on chart tooltips (Pips/Sources) for quick sharing of per-color card lists +- Theme UX: Header includes a Reset Theme control to clear browser preference and reapply server default (THEME) or system mapping. Diagnostics page shows resolved theme and stored preference with a reset action. Roadmap and usage for Web UI features are tracked in `logs/web-ui-upgrade-outline.md`. @@ -77,6 +80,7 @@ Roadmap and usage for Web UI features are tracked in `logs/web-ui-upgrade-outlin - 404s from Starlette now render the HTML 404 page when requested from a browser (Accept: text/html) - Owned page UX: full-size preview now pops on thumbnail hover (not the name); selection highlight tightened to the thumbnail only and changed to white for better contrast; Themes in the hover popout render as a larger bullet list with a brighter "THEMES" label - Image robustness: standardized `data-card-name` on all Scryfall images and centralized retry logic (thumbnails + previews) with version fallbacks (small/normal/large) and a single cache-bust refresh on final failure; removed the previous hover-image cache to reduce complexity and overhead +- Layout polish: fixed sidebar remains full-height under the banner with a subtle right-edge shadow for depth; grid updated to prevent content squish; extra scroll removed; footer pinned when content is short. - Deck Summary list view: rows use fixed tracks for count, ×, name, and owned columns (monospace tabular numerals) to ensure perfect alignment; highlight is an inset box-shadow on the name to avoid layout shifts; long names ellipsize with a tooltip; list starts directly under the type header and remains stable on full-screen widths ### Fixed diff --git a/DOCKER.md b/DOCKER.md index 6d8cead..a819b9b 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -34,7 +34,7 @@ Then open http://localhost:8080 Volumes are the same as the CLI service, so deck exports/logs/configs persist in your working folder. The app serves a favicon at `/favicon.ico` and exposes a health endpoint at `/healthz`. -Compare view offers a Copy summary button to copy a plain-text diff of two runs. +Compare view offers a Copy summary button to copy a plain-text diff of two runs. The sidebar has a subtle depth shadow for clearer separation. Web UI feature highlights: - Locks: Click a card or the lock control in Step 5; locks persist across reruns. @@ -87,7 +87,7 @@ Docker Hub (PowerShell) example: ```powershell docker run --rm ` -p 8080:8080 ` - -e SHOW_LOGS=1 -e SHOW_DIAGNOSTICS=1 ` + -e SHOW_LOGS=1 -e SHOW_DIAGNOSTICS=1 -e ENABLE_THEMES=1 -e THEME=system ` -v "${PWD}/deck_files:/app/deck_files" ` -v "${PWD}/logs:/app/logs" ` -v "${PWD}/csv_files:/app/csv_files" ` @@ -125,6 +125,8 @@ Health check: GET http://localhost:8080/healthz -> { "status": "ok", "version": "dev", "uptime_seconds": 123 } ``` +Theme preference reset (client-side): use the header’s Reset Theme control to clear the saved browser preference; the server default (THEME) applies on next paint. + ## Volumes - `/app/deck_files` ↔ `./deck_files` - `/app/logs` ↔ `./logs` diff --git a/Dockerfile b/Dockerfile index 1d29417..5f9356d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,9 @@ WORKDIR /app/code # Run the application CMD ["python", "main.py"] +# Expose web port for the optional Web UI +EXPOSE 8080 + # Note: For the Web UI, start uvicorn in your orchestrator (compose/run) like: # uvicorn code.web.app:app --host 0.0.0.0 --port 8080 # Phase 9: enable web list virtualization with env WEB_VIRTUALIZE=1 diff --git a/README.md b/README.md index b721bb5..59c8267 100644 Binary files a/README.md and b/README.md differ diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 5514115..78e5827 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -6,6 +6,7 @@ - Headless improvements: `tag_mode` (AND/OR) accepted via JSON and environment; interactive exports include `tag_mode` in the run-config. - Owned cards workflow: Prompt after commander to "Use only owned cards?"; supports `.txt`/`.csv` lists in `owned_cards/`. Owned-only builds filter the pool; if the deck can't reach 100, it remains incomplete and notes it. When not owned-only, owned cards are marked with an `Owned` column in the final CSV. - Exports: CSV/TXT always; JSON run-config exported for interactive runs and optionally in headless (`HEADLESS_EXPORT_JSON=1`). +- Theming: Consolidated Light (Blend) as the only light palette; default THEME can be system|light|dark. Header includes a Reset Theme control to clear saved preference; diagnostics shows resolved theme and stored preference. - Data freshness: Auto-refreshes `cards.csv` if missing or older than 7 days and re-tags when needed using `.tagging_complete.json`. - Web setup speed: initial tagging runs in parallel by default for the Web UI. Configure with `WEB_TAG_PARALLEL=1|0` and `WEB_TAG_WORKERS=` (compose default: 4). Falls back to sequential if parallel init fails. - Phase 8 UI upgrade: Unified “New Deck” modal (steps 1–3), Locks, Replace flow, Compare builds, and shareable Permalinks. Optional Name field becomes the export filename stem and display name. @@ -30,6 +31,7 @@ - All responses include `X-Request-ID`; JSON error payloads include `request_id` for correlation. - Friendly HTML error pages for 404/4xx/500 with a "Go home" link (browser requests). - Feature flags: `SHOW_DIAGNOSTICS=1` to enable a diagnostics page with test tools; `SHOW_LOGS=1` to enable a logs page and `/status/logs?tail=N`. + - Diagnostics page surfaces resolved theme and preference, with a Reset preference action. ## What’s new - Web UI: Staged run with a new "Creatures: All-Theme" phase in AND mode; shows matched selected themes per card for explainability. Step 2 UI clarifies AND/OR with a tooltip and restyled Why panel. diff --git a/WINDOWS_DOCKER_GUIDE.md b/WINDOWS_DOCKER_GUIDE.md index 64d5796..030bb4d 100644 --- a/WINDOWS_DOCKER_GUIDE.md +++ b/WINDOWS_DOCKER_GUIDE.md @@ -47,6 +47,7 @@ Run the browser UI by mapping a port and starting uvicorn: docker run --rm ` -p 8080:8080 ` -e WEB_VIRTUALIZE=1 ` # optional virtualization + -e ENABLE_THEMES=1 -e THEME=system ` # optional theme selector and default -v "${PWD}/deck_files:/app/deck_files" ` -v "${PWD}/logs:/app/logs" ` -v "${PWD}/csv_files:/app/csv_files" ` @@ -57,6 +58,8 @@ docker run --rm ` ``` Then open http://localhost:8080 +Tip: The header includes a Reset Theme control to clear your browser’s saved preference and re-apply the server’s default (or OS when THEME=system). + ## Method 2: Command Prompt ```cmd REM Create and navigate to workspace diff --git a/code/tests/test_diagnostics.py b/code/tests/test_diagnostics.py index 2ac21dc..2ae5dfa 100644 --- a/code/tests/test_diagnostics.py +++ b/code/tests/test_diagnostics.py @@ -67,7 +67,11 @@ def test_status_sys_summary_and_flags(): SHOW_LOGS="1", SHOW_DIAGNOSTICS="1", SHOW_SETUP="1", + ENABLE_THEMES="1", + ENABLE_PWA="1", + ENABLE_PRESETS="1", APP_VERSION="testver", + THEME="dark", ) client = TestClient(app_module.app) r = client.get("/status/sys") @@ -80,3 +84,8 @@ def test_status_sys_summary_and_flags(): assert flags.get("SHOW_LOGS") is True assert flags.get("SHOW_DIAGNOSTICS") is True assert flags.get("SHOW_SETUP") is True + # Theme-related flags + assert flags.get("ENABLE_THEMES") is True + assert flags.get("ENABLE_PWA") is True + assert flags.get("ENABLE_PRESETS") is True + assert flags.get("DEFAULT_THEME") == "dark" diff --git a/code/web/app.py b/code/web/app.py index 61f73c3..382bba0 100644 --- a/code/web/app.py +++ b/code/web/app.py @@ -48,6 +48,15 @@ SHOW_LOGS = _as_bool(os.getenv("SHOW_LOGS"), False) SHOW_SETUP = _as_bool(os.getenv("SHOW_SETUP"), True) SHOW_DIAGNOSTICS = _as_bool(os.getenv("SHOW_DIAGNOSTICS"), False) SHOW_VIRTUALIZE = _as_bool(os.getenv("WEB_VIRTUALIZE"), False) +ENABLE_THEMES = _as_bool(os.getenv("ENABLE_THEMES"), False) +ENABLE_PWA = _as_bool(os.getenv("ENABLE_PWA"), False) +ENABLE_PRESETS = _as_bool(os.getenv("ENABLE_PRESETS"), False) + +# Theme default from environment: THEME=light|dark|system (case-insensitive). Defaults to system. +_THEME_ENV = (os.getenv("THEME") or "").strip().lower() +DEFAULT_THEME = "system" +if _THEME_ENV in {"light", "dark", "system"}: + DEFAULT_THEME = _THEME_ENV # Expose as Jinja globals so all templates can reference without passing per-view templates.env.globals.update({ @@ -55,6 +64,10 @@ templates.env.globals.update({ "show_setup": SHOW_SETUP, "show_diagnostics": SHOW_DIAGNOSTICS, "virtualize": SHOW_VIRTUALIZE, + "enable_themes": ENABLE_THEMES, + "enable_pwa": ENABLE_PWA, + "enable_presets": ENABLE_PRESETS, + "default_theme": DEFAULT_THEME, }) # --- Simple fragment cache for template partials (low-risk, TTL-based) --- @@ -132,6 +145,10 @@ async def status_sys(): "SHOW_LOGS": bool(SHOW_LOGS), "SHOW_SETUP": bool(SHOW_SETUP), "SHOW_DIAGNOSTICS": bool(SHOW_DIAGNOSTICS), + "ENABLE_THEMES": bool(ENABLE_THEMES), + "ENABLE_PWA": bool(ENABLE_PWA), + "ENABLE_PRESETS": bool(ENABLE_PRESETS), + "DEFAULT_THEME": DEFAULT_THEME, }, } except Exception: diff --git a/code/web/static/manifest.webmanifest b/code/web/static/manifest.webmanifest new file mode 100644 index 0000000..a687bcc --- /dev/null +++ b/code/web/static/manifest.webmanifest @@ -0,0 +1,13 @@ +{ + "name": "MTG Deckbuilder", + "short_name": "Deckbuilder", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0f0f10", + "theme_color": "#0f0f10", + "icons": [ + { "src": "/static/favicon.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/static/favicon.png", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/code/web/static/styles.css b/code/web/static/styles.css index 8f9e0f4..408233f 100644 --- a/code/web/static/styles.css +++ b/code/web/static/styles.css @@ -1,6 +1,7 @@ /* Base */ :root{ /* MTG color palette (approx from provided values) */ + --banner-h: 52px; --sidebar-w: 260px; --green-main: rgb(0,115,62); --green-light: rgb(196,211,202); @@ -21,16 +22,66 @@ --ok: #16a34a; /* success */ --warn: #f59e0b; /* warning */ --err: #ef4444; /* error */ + /* Surface overrides for specific regions (default to panel) */ + --surface-banner: var(--panel); + --surface-banner-text: var(--text); + --surface-sidebar: var(--panel); + --surface-sidebar-text: var(--text); +} + +/* Light blend between Slate and Parchment (leans gray) */ +[data-theme="light-blend"]{ + --bg: #e8e2d0; /* blend of slate (#dedfe0) and parchment (#f8e7b9), 60/40 gray */ + --panel: #ffffff; /* crisp panels for readability */ + --text: #0b0d12; + --muted: #6b655d; /* slightly warm muted */ + --border: #d6d1c7; /* neutral warm-gray border */ + /* Slightly darker banner/sidebar for separation */ + --surface-banner: #1a1b1e; + --surface-sidebar: #1a1b1e; + --surface-banner-text: #e8e8e8; + --surface-sidebar-text: #e8e8e8; +} + +[data-theme="dark"]{ + --bg: #0f0f10; + --panel: #1a1b1e; + --text: #e8e8e8; + --muted: #b6b8bd; + --border: #2a2b2f; +} +[data-theme="high-contrast"]{ + --bg: #000; + --panel: #000; + --text: #fff; + --muted: #e5e7eb; + --border: #fff; + --ring: #ff0; +} +[data-theme="cb-friendly"]{ + /* Tweak accents for color-blind friendliness */ + --green-main: #2e7d32; /* darker green */ + --red-main: #c62828; /* deeper red */ + --blue-main: #1565c0; /* balanced blue */ } *{box-sizing:border-box} html,body{height:100%} -body { font-family: system-ui, Arial, sans-serif; margin: 0; color: var(--text); background: var(--bg); } +body { + font-family: system-ui, Arial, sans-serif; + margin: 0; + color: var(--text); + background: var(--bg); + display: flex; + flex-direction: column; + min-height: 100vh; +} /* Honor HTML hidden attribute across the app */ [hidden] { display: none !important; } /* Accessible focus ring for keyboard navigation */ .focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } /* Top banner */ -.top-banner{ position:sticky; top:0; z-index:10; background:#0c0d0f; border-bottom:1px solid var(--border); } +.top-banner{ position:sticky; top:0; z-index:10; background: var(--surface-banner); color: var(--surface-banner-text); border-bottom:1px solid var(--border); } +.top-banner{ min-height: var(--banner-h); } .top-banner .top-inner{ margin:0; padding:.5rem 0; display:grid; grid-template-columns: var(--sidebar-w) 1fr; align-items:center; } .top-banner h1{ font-size: 1.1rem; margin:0; padding-left: 1rem; } .banner-status{ color: var(--muted); font-size:.9rem; text-align:left; padding-left: 1.5rem; padding-right: 1.5rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } @@ -39,9 +90,22 @@ body { font-family: system-ui, Arial, sans-serif; margin: 0; color: var(--text); .health-dot[data-state="bad"]{ background:#ef4444; box-shadow:0 0 0 2px rgba(239,68,68,.3) inset; } /* Layout */ -.layout{ display:grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: calc(100vh - 52px); } -.sidebar{ background: var(--panel); border-right: 1px solid var(--border); padding: 1rem; position:sticky; top:0; align-self:start; height:100vh; overflow:auto; width: var(--sidebar-w); } -.content{ padding: 1.25rem 1.5rem; } +.layout{ display:grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); flex: 1 0 auto; } +.sidebar{ + background: var(--surface-sidebar); + color: var(--surface-sidebar-text); + border-right: 1px solid var(--border); + padding: 1rem; + position: fixed; + top: var(--banner-h); + left: 0; + bottom: 0; + overflow: auto; + width: var(--sidebar-w); + z-index: 9; /* below the banner (z=10) */ + box-shadow: 2px 0 10px rgba(0,0,0,.18); +} +.content{ padding: 1.25rem 1.5rem; grid-column: 2; min-width: 0; } .brand h1{ display:none; } .mana-dots{ display:flex; gap:.35rem; margin-bottom:.5rem; } @@ -53,13 +117,13 @@ body { font-family: system-ui, Arial, sans-serif; margin: 0; color: var(--text); .dot.black{ background: var(--black-light); } .nav{ display:flex; flex-direction:column; gap:.35rem; } -.nav a{ color: var(--text); text-decoration:none; padding:.4rem .5rem; border-radius:6px; border:1px solid transparent; } -.nav a:hover{ background: #202227; border-color: var(--border); } +.nav a{ color: var(--surface-sidebar-text); text-decoration:none; padding:.4rem .5rem; border-radius:6px; border:1px solid transparent; } +.nav a:hover{ background: color-mix(in srgb, var(--surface-sidebar) 85%, var(--surface-sidebar-text) 15%); border-color: var(--border); } /* Simple two-column layout for inspect panel */ .two-col { display: grid; grid-template-columns: 1fr 320px; gap: 1rem; align-items: start; } .two-col .grow { min-width: 0; } -.card-preview img { width: 100%; height: auto; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,.35); border:1px solid var(--border); background: #111; } +.card-preview img { width: 100%; height: auto; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,.35); border:1px solid var(--border); background: var(--panel); } @media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } } /* Left-rail variant puts the image first */ @@ -74,7 +138,7 @@ button:hover{ filter:brightness(1.05); } .btn:hover{ filter:brightness(1.05); text-decoration:none; } .btn.disabled, .btn[aria-disabled="true"]{ opacity:.6; cursor:default; pointer-events:none; } label{ display:inline-flex; flex-direction:column; gap:.25rem; margin-right:.75rem; } -select,input[type="text"],input[type="number"]{ background:#0f1115; color:var(--text); border:1px solid var(--border); border-radius:6px; padding:.35rem .4rem; } +select,input[type="text"],input[type="number"]{ background: var(--panel); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:.35rem .4rem; } fieldset{ border:1px solid var(--border); border-radius:8px; padding:.75rem; margin:.75rem 0; } small, .muted{ color: var(--muted); } @@ -108,8 +172,8 @@ small, .muted{ color: var(--muted); } /* Home actions */ .actions-grid{ display:grid; grid-template-columns: repeat( auto-fill, minmax(220px, 1fr) ); gap: .75rem; } -.action-button{ display:block; text-decoration:none; color: var(--text); border:1px solid var(--border); background:#0f1115; padding:1.25rem; border-radius:10px; text-align:center; font-weight:600; } -.action-button:hover{ border-color:#3a3c42; background:#12151b; } +.action-button{ display:block; text-decoration:none; color: var(--text); border:1px solid var(--border); background: var(--panel); padding:1.25rem; border-radius:10px; text-align:center; font-weight:600; } +.action-button:hover{ border-color: color-mix(in srgb, var(--border) 70%, var(--text) 30%); background: color-mix(in srgb, var(--panel) 80%, var(--text) 20%); } .action-button.primary{ background: linear-gradient(180deg, rgba(14,104,171,.25), rgba(14,104,171,.05)); border-color: #274766; } /* Card grid for added cards (responsive, compact tiles) */ @@ -123,7 +187,7 @@ small, .muted{ color: var(--muted); } .card-tile{ width:170px; position: relative; - background:#0f1115; + background: var(--panel); border:1px solid var(--border); border-radius:6px; padding:.25rem .25rem .4rem; @@ -165,13 +229,13 @@ small, .muted{ color: var(--muted); } gap:.75rem; } .candidate-tile{ - background:#0f1115; + background: var(--panel); border:1px solid var(--border); border-radius:8px; padding:.4rem; } .candidate-tile .img-btn{ display:block; width:100%; padding:0; background:transparent; border:none; cursor:pointer; } -.candidate-tile img{ width:100%; max-width:200px; height:auto; border-radius:8px; box-shadow:0 6px 18px rgba(0,0,0,.35); background:#111; display:block; margin:0 auto; } +.candidate-tile img{ width:100%; max-width:200px; height:auto; border-radius:8px; box-shadow:0 6px 18px rgba(0,0,0,.35); background: var(--panel); display:block; margin:0 auto; } .candidate-tile .meta{ text-align:center; margin-top:.35rem; } .candidate-tile .name{ font-weight:600; font-size:.95rem; } .candidate-tile .score{ color:var(--muted); font-size:.85rem; } @@ -186,7 +250,7 @@ small, .muted{ color: var(--muted); } /* Stage Navigator */ .stage-nav { margin:.5rem 0 1rem; } .stage-nav ol { list-style:none; padding:0; margin:0; display:flex; gap:.35rem; flex-wrap:wrap; } -.stage-nav .stage-link { display:flex; align-items:center; gap:.4rem; background:#0f1115; border:1px solid var(--border); color:var(--text); border-radius:999px; padding:.25rem .6rem; cursor:pointer; } +.stage-nav .stage-link { display:flex; align-items:center; gap:.4rem; background: var(--panel); border:1px solid var(--border); color:var(--text); border-radius:999px; padding:.25rem .6rem; cursor:pointer; } .stage-nav .stage-item.done .stage-link { opacity:.75; } .stage-nav .stage-item.current .stage-link { box-shadow: 0 0 0 2px rgba(96,165,250,.4) inset; border-color:#3b82f6; } .stage-nav .idx { display:inline-grid; place-items:center; width:20px; height:20px; border-radius:50%; background:#1f2937; font-size:12px; } @@ -198,12 +262,12 @@ small, .muted{ color: var(--muted); } } /* Progress bar */ -.progress { position: relative; height: 10px; background: #0f1115; border:1px solid var(--border); border-radius: 999px; overflow: hidden; } +.progress { position: relative; height: 10px; background: var(--panel); border:1px solid var(--border); border-radius: 999px; overflow: hidden; } .progress .bar { position:absolute; left:0; top:0; bottom:0; width: 0%; background: linear-gradient(90deg, rgba(96,165,250,.6), rgba(14,104,171,.9)); } .progress.flash { box-shadow: 0 0 0 2px rgba(245,158,11,.35) inset; } /* Chips */ -.chip { display:inline-flex; align-items:center; gap:.35rem; background:#0f1115; border:1px solid var(--border); color:var(--text); border-radius:999px; padding:.2rem .55rem; font-size:12px; } +.chip { display:inline-flex; align-items:center; gap:.35rem; background: var(--panel); border:1px solid var(--border); color:var(--text); border-radius:999px; padding:.2rem .55rem; font-size:12px; } .chip .dot { width:8px; height:8px; border-radius:50%; background:#6b7280; } /* Cards toolbar */ @@ -217,17 +281,17 @@ small, .muted{ color: var(--muted); } .group-header{ display:flex; align-items:center; gap:.5rem; } .group-header h5{ margin:.4rem 0; } .group-header .count{ color: var(--muted); font-size:12px; } -.group-header .toggle{ margin-left:auto; background:#1f2937; color:#e5e7eb; border:1px solid var(--border); border-radius:6px; padding:.2rem .5rem; font-size:12px; cursor:pointer; } +.group-header .toggle{ margin-left:auto; background: color-mix(in srgb, var(--panel) 80%, var(--text) 20%); color: var(--text); border:1px solid var(--border); border-radius:6px; padding:.2rem .5rem; font-size:12px; cursor:pointer; } .group-grid[data-collapsed]{ display:none; } .hide-reasons .card-tile .reason{ display:none; } .card-tile.force-show .reason{ display:block !important; } .card-tile.force-hide .reason{ display:none !important; } -.btn-why{ background:#1f2937; color:#e5e7eb; border:1px solid var(--border); border-radius:6px; padding:.15rem .4rem; font-size:12px; cursor:pointer; } +.btn-why{ background: color-mix(in srgb, var(--panel) 80%, var(--text) 20%); color: var(--text); border:1px solid var(--border); border-radius:6px; padding:.15rem .4rem; font-size:12px; cursor:pointer; } .chips-inline{ display:flex; gap:.35rem; flex-wrap:wrap; align-items:center; } .chips-inline .chip{ cursor:pointer; user-select:none; } /* Inline error banner */ -.inline-error-banner{ background:#1a0f10; border:1px solid #b91c1c; color:#fca5a5; padding:.5rem .6rem; border-radius:8px; margin-bottom:.5rem; } +.inline-error-banner{ background: color-mix(in srgb, var(--panel) 85%, #b91c1c 15%); border:1px solid #b91c1c; color:#b91c1c; padding:.5rem .6rem; border-radius:8px; margin-bottom:.5rem; } .inline-error-banner .muted{ color:#fda4af; } /* Alternatives panel */ diff --git a/code/web/static/sw.js b/code/web/static/sw.js new file mode 100644 index 0000000..017f037 --- /dev/null +++ b/code/web/static/sw.js @@ -0,0 +1,10 @@ +// Minimal service worker (stub). Controlled by ENABLE_PWA. +self.addEventListener('install', event => { + self.skipWaiting(); +}); +self.addEventListener('activate', event => { + event.waitUntil(clients.claim()); +}); +self.addEventListener('fetch', event => { + // Pass-through; caching strategy can be added later. +}); diff --git a/code/web/templates/base.html b/code/web/templates/base.html index 9b8868d..16a3ea4 100644 --- a/code/web/templates/base.html +++ b/code/web/templates/base.html @@ -1,11 +1,36 @@ - + MTG Deckbuilder - + + @@ -13,6 +38,9 @@ + {% if enable_pwa %} + + {% endif %}
@@ -21,8 +49,23 @@
- + {% if enable_themes %} + + + {% endif %}
@@ -60,16 +103,17 @@ + {% if enable_themes %} + + {% endif %} + {% if not enable_themes %} + + {% endif %} + {% if enable_pwa %} + + {% endif %}