feat: web documentation portal with contextual help links and consistent page headers (#67)

This commit is contained in:
mwisnowski 2026-04-01 11:46:08 -07:00 committed by GitHub
parent 46637cf27f
commit 13f6fa5dbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2232 additions and 140 deletions

View file

@ -91,6 +91,7 @@
<a href="/decks">Finished Decks</a>
<a href="/themes/">Themes</a>
{% if random_ui %}<a href="/random">Random</a>{% endif %}
<a href="/help">Help</a>
{% if show_diagnostics %}<a href="/diagnostics">Diagnostics</a>{% endif %}
{% if show_logs %}<a href="/logs">Logs</a>{% endif %}
</nav>
@ -557,5 +558,62 @@
document.addEventListener('htmx:afterSwap', function(){ setTimeout(initPriceDisplay, 80); });
})();
</script>
<!-- Shared help-tip panel: position:fixed to avoid overflow clipping in any container -->
<div id="g-help-tip" class="help-tip-panel" role="tooltip"></div>
<script>
(function() {
var P = null, hide = null, activeBtn = null;
function panel() { return P || (P = document.getElementById('g-help-tip')); }
function show(btn) {
clearTimeout(hide);
var t = btn.getAttribute('data-tip') || '';
var h = btn.getAttribute('data-tip-href') || '';
var p = panel();
p.innerHTML = t + (h ? '<a href="' + h + '" target="_blank" rel="noopener noreferrer">Full guide &rarr;</a>' : '');
p.style.display = 'block';
activeBtn = btn;
var r = btn.getBoundingClientRect();
var w = 200, m = 8;
var left = r.left + r.width / 2 - w / 2;
left = Math.max(m, Math.min(left, window.innerWidth - w - m));
p.style.left = left + 'px';
var ph = p.offsetHeight || 88;
p.style.top = r.top >= ph + 10 ? (r.top - ph - 6) + 'px' : (r.bottom + 6) + 'px';
}
function doHide(ms) {
clearTimeout(hide);
hide = setTimeout(function() { var p = panel(); p.style.display = 'none'; activeBtn = null; }, ms || 0);
}
// Desktop hover
document.addEventListener('mouseover', function(e) {
var b = e.target.closest && e.target.closest('.help-tip-btn');
if (b) { show(b); return; }
if (e.target.closest && e.target.closest('#g-help-tip')) clearTimeout(hide);
});
document.addEventListener('mouseout', function(e) {
var r = e.relatedTarget;
if (e.target.closest && e.target.closest('.help-tip-btn')) {
if (r && r.closest && r.closest('#g-help-tip')) return;
doHide(150);
}
if (e.target.closest && e.target.closest('#g-help-tip')) {
if (r && r.closest && r.closest('.help-tip-btn')) return;
doHide(150);
}
});
// Mobile tap
document.addEventListener('click', function(e) {
var b = e.target.closest && e.target.closest('.help-tip-btn');
if (b) {
if (panel().style.display === 'block' && activeBtn === b) { doHide(0); }
else { show(b); }
e.stopPropagation();
return;
}
if (e.target.closest && e.target.closest('#g-help-tip')) return;
doHide(0);
});
})();
</script>
</body>
</html>

View file

@ -60,8 +60,10 @@
</style>
<section class="card-browser-container">
<h3>Card Browser</h3>
<p class="muted">Browse all {{ total_cards }} cards with filters and search.</p>
<div class="page-header">
<h2>All Cards</h2>
<p class="muted">Browse all {{ total_cards }} cards with filters and search.</p>
</div>
{# Error message #}
{% if error %}

View file

@ -48,7 +48,7 @@
{% include "build/_new_deck_additional_themes.html" %}
{% endif %}
<div class="mt-2" id="newdeck-bracket-slot">
<label>Bracket
<label><span style="display:inline-flex; align-items:center; gap:4px;">Bracket <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Restricts deck picks to the power-level rules for that bracket tier." data-tip-href="/help/bracket_compliance#bracket-tiers" aria-label="Bracket compliance help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></span>
<select name="bracket">
{% for b in brackets %}
{% if not gc_commander or b.level >= 3 %}
@ -89,12 +89,12 @@
</div>
<label for="pref-mc-chk" class="form-checkbox-label" title="When enabled, include a Multi-Copy package for matching archetypes (e.g., tokens/tribal).">
<input type="checkbox" name="enable_multicopy" id="pref-mc-chk" value="1" {% if form and form.enable_multicopy %}checked{% endif %} />
<span>Enable Multi-Copy package</span>
<span>Enable Multi-Copy package <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Includes multiple copies of a single archetype card (tokens, slivers, etc.)." data-tip-href="/help/multi_copy" aria-label="Multi-Copy package help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></span>
</label>
<div class="flex flex-col gap-2 mt-3">
<div class="flex flex-col gap-2">
<label for="use-owned-chk" class="form-checkbox-label" title="Limit the pool to cards you already own. Cards outside your owned library will be skipped.">
<input type="checkbox" name="use_owned_only" id="use-owned-chk" value="1" {% if form and form.use_owned_only %}checked{% endif %} />
<span>Use only owned cards</span>
<span>Use only owned cards <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Limits card picks to your uploaded owned-card library only." data-tip-href="/help/owned_cards#build-modes" aria-label="Owned cards help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></span>
</label>
<label for="prefer-owned-chk" class="form-checkbox-label" title="Still allow unowned cards, but rank owned cards higher when choosing picks.">
<input type="checkbox" name="prefer_owned" id="prefer-owned-chk" value="1" {% if form and form.prefer_owned %}checked{% endif %} />
@ -106,7 +106,7 @@
</label>
<label for="smart-lands-chk" class="form-checkbox-label" title="When enabled, the builder automatically adjusts the land count and mana-base profile based on your commander's speed and color complexity.">
<input type="checkbox" name="enable_smart_lands" id="smart-lands-chk" value="1" {% if form and form.enable_smart_lands %}checked{% endif %} />
<span>Smart Land Bases</span>
<span>Smart Land Bases <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Auto-adjusts land count and mana profile based on commander speed and color complexity." data-tip-href="/help/land_bases#speed-categories-land-counts" aria-label="Smart Land Bases help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></span>
</label>
</div>
</div>
@ -115,7 +115,7 @@
{% include "build/_new_deck_ideals.html" %}
{% if allow_must_haves %}
<fieldset>
<legend>Include/Exclude Cards</legend>
<legend>Include/Exclude Cards <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Force specific cards into or out of your deck before the build runs." data-tip-href="/help/include_exclude#adding-cards" aria-label="Include/Exclude help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></legend>
<div class="include-exclude-grid">
<!-- Include Cards Column (Left, Green) -->
<div>
@ -214,7 +214,7 @@
{% include "build/_new_deck_skip_controls.html" %}
{% if enable_budget_mode %}
<fieldset>
<legend>Budget</legend>
<legend>Budget <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Set deck and per-card price ceilings. Over-budget cards are flagged during the build." data-tip-href="/help/budget_mode#setting-a-budget" aria-label="Budget mode help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></legend>
<div class="flex flex-col gap-3">
<label class="block">
<span>Total budget ($)</span>
@ -270,6 +270,7 @@
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
<div class="modal-footer-left">
<button type="submit" name="quick_build" value="1" class="btn-continue" id="quick-build-btn" title="Build entire deck automatically without approval steps">Quick Build</button>
<span class="help-tip" style="margin-left:6px;"><button type="button" class="help-tip-btn" data-tip="Builds the complete deck in one step without step-by-step approval prompts." data-tip-href="/help/quick_build_skip_controls#quick-build" aria-label="Quick Build help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span>
<button type="submit" class="btn-continue" id="create-btn">Build Deck</button>
</div>
</div>

View file

@ -169,7 +169,7 @@
{# Always update the bracket dropdown on commander change; hide 12 only when gc_commander is true #}
<div id="newdeck-bracket-slot" hx-swap-oob="true">
<label>Bracket
<label><span style="display:inline-flex; align-items:center; gap:4px;">Bracket <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Restricts deck picks to the power-level rules for that bracket tier." data-tip-href="/help/bracket_compliance#bracket-tiers" aria-label="Bracket compliance help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></span>
<select name="bracket">
{% for b in brackets %}
{% if not gc_commander or b.level >= 3 %}

View file

@ -22,7 +22,7 @@
{% set partner_suggestions_has_hidden = partner_suggestions_has_hidden if partner_suggestions_has_hidden is defined else False %}
{% if feature_available %}
<fieldset>
<legend>Partner Mechanics</legend>
<legend>Partner Mechanics <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Select a second commander via Partner, Friends Forever, or Choose a Background." data-tip-href="/help/partner_mechanics#selecting-a-partner-in-the-web-ui" aria-label="Partner Mechanics help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></legend>
{% if not partner_capable %}
<p class="muted" style="font-size:12px;">This commander doesn't support partner mechanics or backgrounds.</p>
{% else %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% block banner_subtitle %}Build a Deck{% endblock %}
{% block content %}
<h2>Build a Deck</h2>
<div class="page-header">
<h2>Build a Deck</h2>
</div>
<div style="margin:.25rem 0 1rem 0; display:flex; align-items:center; gap:.75rem; flex-wrap:wrap;">
<button type="button" class="btn" hx-get="/build/new" hx-target="body" hx-swap="beforeend">Build a New Deck…</button>
<span class="muted" style="margin-left:.25rem;">Quick-start wizard (name, commander, themes, ideals)</span>

View file

@ -3,13 +3,13 @@
{% block content %}
<section class="commander-page">
<header class="commander-hero">
<div class="page-header">
<h2>Commanders</h2>
<p class="muted">Browse the catalog and jump straight into a build with your chosen leader.</p>
</header>
<p style="font-size: .875rem;">
Know of commander-specific themes or synergies we're missing? <a href="https://github.com/mwisnowski/mtg_python_deckbuilder/issues/new?template=commander-specific-theme-request.md" target="_blank" rel="noopener" style="color: var(--accent); text-decoration: underline;">Submit a request here</a>.
</p>
<p class="muted">Browse the catalog and jump straight into a build with your chosen leader.
<p class="muted" style="font-size: .875rem;">
Know of commander-specific themes or synergies we're missing? <a href="https://github.com/mwisnowski/mtg_python_deckbuilder/issues/new?template=commander-specific-theme-request.md" target="_blank" rel="noopener" style="color: var(--accent); text-decoration: underline;">Submit a request here</a>.
</p>
</div>
<form
id="commander-filter-form"

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block content %}
<h2>Build from JSON</h2>
<div class="page-header">
<h2>Build from JSON</h2>
<p class="muted">Run a non-interactive deck build using a saved JSON configuration. Upload a JSON file, view its details, or run it headlessly to generate deck exports and a build summary.</p>
</div>
<div style="display:grid; grid-template-columns: 1fr minmax(360px, 520px); gap:16px; align-items:start;">
<p class="muted" style="max-width: 70ch; margin:0;">
Run a non-interactive deck build using a saved JSON configuration. Upload a JSON file, view its details, or run it headlessly to generate deck exports and a build summary.
</p>
<div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong style="font-size:14px;">Example: {{ example_name }}</strong>

View file

@ -1,8 +1,10 @@
{% extends "base.html" %}
{% block banner_subtitle %}Finished Decks{% endblock %}
{% block content %}
<h2 id="decks-heading">Finished Decks</h2>
<p class="muted">These are exported decklists from previous runs. Open a deck to view the final summary, download CSV/TXT, and inspect card types and curve.</p>
<div class="page-header">
<h2 id="decks-heading">Finished Decks</h2>
<p class="muted">These are exported decklists from previous runs. Open a deck to view the final summary, download CSV/TXT, and inspect card types and curve.</p>
</div>
{% if error %}
<div class="error">{{ error }}</div>

View file

@ -1,8 +1,10 @@
{% extends "base.html" %}
{% block content %}
<section>
<h2>Diagnostics</h2>
<p class="muted">Use these tools to verify error handling surfaces.</p>
<div class="page-header">
<h2>Diagnostics</h2>
<p class="muted">Use these tools to verify error handling surfaces.</p>
</div>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">System summary</summary>
<div id="sysSummary" class="muted" style="margin-top:.5rem">Loading…</div>

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% block content %}
<section>
<h2>Logs</h2>
<div class="page-header">
<h2>Logs</h2>
</div>
<form method="get" action="/logs" class="form-row" style="gap:.5rem; align-items: center;">
<label>Tail <input type="number" name="tail" value="{{ tail }}" min="1" max="500" style="width:80px"></label>
<label>Filter <input type="text" name="q" value="{{ q }}" placeholder="keyword"></label>

View file

@ -0,0 +1,588 @@
{% extends "base.html" %}
{% from 'partials/_buttons.html' import button %}
{% block title %}{{ page_title }} - MTG Deckbuilder{% endblock %}
{% block content %}
<!-- Mobile guide nav toggle (must be outside sidebar due to CSS transform/fixed positioning) -->
<button class="docs-sidebar-toggle" id="docsMobileToggle" aria-label="Toggle guide navigation" aria-expanded="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="docs-layout">
<!-- Sidebar navigation -->
<aside class="docs-sidebar" id="docsSidebar">
<div class="docs-sidebar-header">
<h3>Documentation</h3>
{{ button('← All Guides', variant='ghost', href='/help', size='sm', classes='docs-back-link') }}
</div>
<!-- Table of contents for current guide -->
{% if toc_html %}
<div class="docs-toc">
<h4 class="docs-toc-title">On This Page</h4>
<div class="docs-toc-content">
{{ toc_html | safe }}
</div>
</div>
{% endif %}
<!-- All guides navigation (collapsible, collapsed by default if TOC present) -->
<details class="docs-all-guides" {% if not toc_html %}open{% endif %}>
<summary class="docs-all-guides-toggle">All Guides</summary>
<nav class="docs-nav" aria-label="Documentation navigation">
{% for guide in all_guides %}
<a
href="/help/{{ guide.name }}"
class="docs-nav-item {% if guide.name == guide_name %}active{% endif %}"
{% if guide.name == guide_name %}aria-current="page"{% endif %}
>
{{ guide.title }}
</a>
{% endfor %}
</nav>
</details>
</aside>
<!-- Main content -->
<main class="docs-content">
<article class="docs-article">
<!-- Guide header -->
<header class="docs-header">
<h1>{{ guide_title }}</h1>
{% if guide_description %}
<p class="docs-description">{{ guide_description }}</p>
{% endif %}
</header>
<!-- Rendered markdown content -->
<div class="docs-body markdown-content">
{{ html_content | safe }}
</div>
<!-- Footer navigation -->
<footer class="docs-footer">
{{ button('← Back to All Guides', variant='primary', href='/help', size='md') }}
</footer>
</article>
</main>
</div>
<style>
/* Layout */
.docs-layout {
display: grid;
grid-template-columns: minmax(280px, 320px) 1fr;
gap: 2rem;
max-width: 1400px;
margin: 0;
margin-right: auto;
padding: 2rem 1rem;
min-height: calc(100vh - 120px);
align-items: start;
}
/* Sidebar */
.docs-sidebar {
position: sticky;
top: 1rem;
height: auto;
max-height: none;
overflow-y: visible;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
}
.docs-sidebar-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.docs-sidebar-header h3 {
margin: 0 0 0.75rem 0;
font-size: 1.1rem;
color: var(--text);
}
.docs-back-link {
width: 100%;
justify-content: flex-start;
}
/* Table of contents */
.docs-toc {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.docs-toc-title {
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.docs-toc-content {
font-size: 0.9rem;
}
.docs-toc-content ul {
list-style: none;
margin: 0;
padding: 0;
}
.docs-toc-content li {
margin: 0;
padding: 0;
}
.docs-toc-content a {
display: block;
padding: 0.4rem 0.5rem;
color: var(--text-muted);
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.docs-toc-content a:hover {
background: var(--surface-hover);
color: var(--text);
}
.docs-toc-content a:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* Nested TOC items (H3, H4, etc.) */
.docs-toc-content ul ul {
padding-left: 0.75rem;
margin-top: 0.25rem;
}
/* All guides collapsible section */
.docs-all-guides {
margin-top: 1rem;
}
.docs-all-guides-toggle {
cursor: pointer;
padding: 0.5rem 0.5rem 0.75rem 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
user-select: none;
list-style: none;
border-bottom: 1px solid var(--border);
transition: color 0.2s;
}
.docs-all-guides-toggle:hover {
color: var(--text);
}
.docs-all-guides-toggle::-webkit-details-marker {
display: none;
}
.docs-all-guides-toggle::before {
content: '▼';
display: inline-block;
margin-right: 0.5rem;
font-size: 0.7rem;
transition: transform 0.2s;
}
.docs-all-guides:not([open]) .docs-all-guides-toggle::before {
transform: rotate(-90deg);
}
.docs-nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.docs-nav-item {
padding: 0.5rem 0.75rem;
color: var(--text-muted);
text-decoration: none;
border-radius: 4px;
font-size: 0.95rem;
transition: background-color 0.2s, color 0.2s, transform 0.1s;
}
.docs-nav-item:hover {
background: var(--surface-hover);
color: var(--text);
transform: translateX(2px);
}
.docs-nav-item:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.docs-nav-item.active {
background: var(--primary);
color: white;
font-weight: 600;
}
.docs-nav-item.active:focus {
outline-color: var(--text);
}
/* Main content */
.docs-content {
min-width: 0; /* Prevent grid overflow */
}
.docs-article {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 2rem;
}
.docs-header {
padding-bottom: 1.5rem;
border-bottom: 2px solid var(--border);
margin-bottom: 2rem;
}
.docs-header h1 {
margin: 0 0 0.5rem 0;
font-size: 2rem;
color: var(--text);
}
.docs-description {
color: var(--text-muted);
margin: 0;
font-size: 1.05rem;
line-height: 1.6;
}
.docs-body {
line-height: 1.7;
color: var(--text);
}
.docs-footer {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
}
/* Markdown content styling */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1.5em;
margin-bottom: 0.75em;
font-weight: 600;
line-height: 1.3;
color: var(--text);
scroll-margin-top: 68px; /* banner height (52px) + breathing room */
}
.markdown-content h1 { font-size: 1.8rem; border-bottom: 2px solid var(--border); padding-bottom: 0.3em; }
.markdown-content h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.25em; }
.markdown-content h3 { font-size: 1.25rem; }
.markdown-content h4 { font-size: 1.1rem; }
.markdown-content h5 { font-size: 1rem; }
.markdown-content h6 { font-size: 0.95rem; color: var(--text-muted); }
.markdown-content p {
margin: 1em 0;
}
.markdown-content a {
color: var(--primary);
text-decoration: underline;
transition: color 0.2s;
}
.markdown-content a:hover {
color: var(--primary-hover);
}
.markdown-content ul,
.markdown-content ol {
margin: 1em 0;
padding-left: 2em;
}
.markdown-content li {
margin: 0.5em 0;
}
.markdown-content code {
background: var(--surface-alt);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
color: var(--text);
border: 1px solid var(--border);
}
.markdown-content pre {
background: var(--surface-alt);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1em;
overflow-x: auto;
margin: 1em 0;
}
.markdown-content pre code {
background: none;
padding: 0;
border: none;
font-size: 0.9em;
display: block;
}
.markdown-content blockquote {
border-left: 4px solid var(--primary);
padding-left: 1em;
margin: 1em 0;
color: var(--text-muted);
font-style: italic;
}
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
border: 1px solid var(--border);
}
.markdown-content th,
.markdown-content td {
border: 1px solid var(--border);
padding: 0.75em;
text-align: left;
}
.markdown-content th {
background: var(--surface-alt);
font-weight: 600;
}
.markdown-content tr:nth-child(even) {
background: var(--surface-hover);
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1em 0;
}
.markdown-content hr {
border: none;
border-top: 2px solid var(--border);
margin: 2em 0;
}
/* Mobile responsive */
/* Sidebar toggle tab (integrated into sidebar) */
.docs-sidebar-toggle {
display: none; /* Hidden on desktop */
}
@media (max-width: 968px) {
/* Toggle is a fixed sibling of sidebar - visible regardless of sidebar transform */
.docs-sidebar-toggle {
display: flex;
position: fixed;
top: 60px; /* Below banner */
left: 0;
z-index: 100;
background: var(--surface-sidebar);
color: var(--surface-sidebar-text);
border: 1px solid var(--border);
border-left: none;
border-radius: 0 6px 6px 0;
padding: 0.75rem 0.5rem;
cursor: pointer;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
transition: left 0.3s ease-in-out, border-radius 0.3s;
align-items: center;
justify-content: center;
}
.docs-sidebar-toggle:hover {
background: color-mix(in srgb, var(--surface-sidebar) 85%, var(--surface-sidebar-text) 15%);
}
/* When sidebar is open, shift toggle to be at edge of open sidebar */
.docs-sidebar-toggle.sidebar-open {
left: 280px;
border-radius: 0 6px 6px 0;
border-left: none;
}
.docs-layout {
grid-template-columns: 1fr;
padding: 1rem;
}
.docs-sidebar {
position: fixed;
top: 52px; /* Below top banner */
left: 0;
width: 280px;
height: calc(100vh - 52px);
z-index: 50;
max-height: none;
margin-bottom: 0;
border-radius: 0;
border-left: none;
border-top: none;
border-bottom: none;
background: var(--surface-sidebar);
color: var(--surface-sidebar-text);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
overflow-y: auto;
}
.docs-sidebar.mobile-open {
transform: translateX(0);
}
/* Backdrop when sidebar is open */
.docs-sidebar::before {
content: '';
position: fixed;
top: 52px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: -1;
}
.docs-sidebar.mobile-open::before {
opacity: 1;
pointer-events: auto;
}
/* Sidebar content (not the toggle) */
.docs-sidebar-header,
.docs-toc,
.docs-all-guides {
position: relative;
z-index: 1;
}
.docs-article {
padding: 1.5rem;
}
.docs-header h1 {
font-size: 1.5rem;
}
}
@media (max-width: 640px) {
.docs-article {
padding: 1rem;
}
.markdown-content h1 { font-size: 1.5rem; }
.markdown-content h2 { font-size: 1.25rem; }
.markdown-content h3 { font-size: 1.1rem; }
}
</style>
<script>
// Smooth anchor scrolling
document.addEventListener('DOMContentLoaded', function() {
const links = document.querySelectorAll('.markdown-content a[href^="#"], .docs-toc-content a[href^="#"]');
links.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Close mobile sidebar after navigation
if (window.innerWidth <= 968) {
const sidebar = document.getElementById('docsSidebar');
const toggleButton = document.getElementById('docsMobileToggle');
if (sidebar && toggleButton) {
sidebar.classList.remove('mobile-open');
toggleButton.classList.remove('sidebar-open');
toggleButton.setAttribute('aria-expanded', 'false');
}
}
}
});
});
// Mobile sidebar toggle
const toggleButton = document.getElementById('docsMobileToggle');
const sidebar = document.getElementById('docsSidebar');
if (toggleButton && sidebar) {
toggleButton.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = sidebar.classList.toggle('mobile-open');
toggleButton.classList.toggle('sidebar-open', isOpen);
toggleButton.setAttribute('aria-expanded', isOpen);
});
// Close sidebar when clicking backdrop
sidebar.addEventListener('click', function(e) {
if (e.target === sidebar || e.target.classList.contains('docs-sidebar')) {
sidebar.classList.remove('mobile-open');
toggleButton.classList.remove('sidebar-open');
toggleButton.setAttribute('aria-expanded', 'false');
}
});
// Close sidebar on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
toggleButton.classList.remove('sidebar-open');
toggleButton.setAttribute('aria-expanded', 'false');
}
});
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,96 @@
{% extends "base.html" %}
{% from 'partials/_buttons.html' import button %}
{% from 'partials/_panels.html' import simple_panel %}
{% block title %}{{ page_title }} - MTG Deckbuilder{% endblock %}
{% block content %}
<div class="page-header">
<h2>Documentation</h2>
<p class="muted">User guides and feature documentation</p>
</div>
{% if guides %}
<div class="docs-index">
{% for guide in guides %}
<div class="doc-card">
<h3 class="doc-card-title">
<a href="/help/{{ guide.name }}">{{ guide.title }}</a>
</h3>
{% if guide.description %}
<p class="doc-card-description">{{ guide.description }}</p>
{% endif %}
<div class="doc-card-actions">
{{ button('Read Guide →', variant='ghost', href='/help/' + guide.name, size='sm') }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div style="padding: 2rem; text-align: center; color: var(--text-muted);">
<p>No documentation guides available.</p>
</div>
{% endif %}
<style>
.docs-index {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-top: 0;
}
.doc-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.doc-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.doc-card:focus-within {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.doc-card-title {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
font-weight: 600;
}
.doc-card-title a {
color: var(--text);
text-decoration: none;
transition: color 0.2s;
}
.doc-card-title a:hover {
color: var(--primary);
}
.doc-card-description {
color: var(--text-muted);
margin: 0 0 1rem 0;
line-height: 1.5;
font-size: 0.95rem;
}
.doc-card-actions {
display: flex;
justify-content: flex-end;
}
@media (max-width: 768px) {
.docs-index {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}

View file

@ -12,6 +12,7 @@
{% if show_commanders %}{{ button('Browse Commanders', variant='secondary', href='/commanders', classes='action-button home-button') }}{% endif %}
{{ button('Finished Decks', variant='secondary', href='/decks', classes='action-button home-button') }}
{{ button('Browse Themes', variant='secondary', href='/themes/', classes='action-button home-button') }}
{{ button('Help & Guides', variant='secondary', href='/help', classes='action-button home-button') }}
{% if random_ui %}{{ button('Random Build', variant='secondary', href='/random', classes='action-button home-button') }}{% endif %}
{% if show_diagnostics %}{{ button('Diagnostics', variant='secondary', href='/diagnostics', classes='action-button home-button') }}{% endif %}
{% if show_logs %}{{ button('View Logs', variant='secondary', href='/logs', classes='action-button home-button') }}{% endif %}

View file

@ -1,8 +1,10 @@
{% extends "base.html" %}
{% block content %}
<section>
<h3>Owned Cards Library</h3>
<p class="muted">Upload .txt or .csv lists. Well extract names and keep a de-duplicated library for the web UI.</p>
<div class="page-header">
<h2>Owned Library</h2>
<p class="muted">Upload .txt or .csv lists. We'll extract names and keep a de-duplicated library for the web UI.</p>
</div>
{% if error %}
<div class="error" style="margin:.5rem 0;">{{ error }}</div>

View file

@ -10,7 +10,7 @@
background:#0f1115; border:1px solid var(--border); border-radius:10px;
box-shadow:0 10px 30px rgba(0,0,0,.5); padding:1.25rem;">
<div class="modal-header" style="display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin-bottom:.75rem;">
<h3 id="mc-include-title" style="margin:0; font-size:1rem;">Include multi-copy package?</h3>
<h3 id="mc-include-title" style="margin:0; font-size:1rem;">Include multi-copy package? <span class="help-tip"><button type="button" class="help-tip-btn" data-tip="Configures a multi-copy package for archetypes like tokens, slivers, or relentless creatures." data-tip-href="/help/multi_copy" aria-label="Multi-Copy help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" width="11" height="11"><circle cx="8" cy="8" r="6.5"/><line x1="8" y1="7" x2="8" y2="10.5" stroke-linecap="round"/><circle cx="8" cy="4.5" r="0.75" fill="currentColor" stroke="none"/></svg></button></span></h3>
<button type="button" class="btn" aria-label="Close" onclick="_mcIncludeClose()">×</button>
</div>
<p class="muted" style="font-size:13px; margin:.25rem 0 .75rem;">

View file

@ -2,7 +2,9 @@
{% block content %}
{% set enable_ui = random_ui %}
<section id="random-modes" aria-labelledby="random-heading">
<h2 id="random-heading">Random Modes</h2>
<div class="page-header">
<h2 id="random-heading">Random</h2>
</div>
{% if not enable_ui %}
<div class="notice" role="status">Random UI is disabled. Set <code>RANDOM_UI=1</code> to enable.</div>
{% else %}
@ -29,6 +31,7 @@
<span id="theme-tooltip-text" class="sr-only">Explain theme fallback order</span>
<div id="theme-tooltip-panel" class="tooltip-panel" role="dialog" aria-modal="false">
<p>We attempt your Primary + Secondary + Tertiary first. If that has no hits, we relax to Primary + Secondary, then Primary + Tertiary, Primary only, synergy overlap, and finally the full pool.</p>
<p style="margin-top:6px;"><a href="/help/random_build#multi-theme-fallback-cascade" target="_blank" rel="noopener noreferrer" style="color:var(--primary,#6366f1); font-size:11px;">Full guide &rarr;</a></p>
</div>
</span>
</span>

View file

@ -47,8 +47,10 @@
</style>
<section>
<h2>Setup / Tagging</h2>
<p class="muted" style="max-width:70ch;">Prepare or refresh the card database and apply tags. You can run this anytime.</p>
<div class="page-header">
<h2>Setup / Tagging</h2>
<p class="muted">Prepare or refresh the card database and apply tags. You can run this anytime.</p>
</div>
<details open style="margin-top:.5rem;">
<summary>Current Status</summary>

View file

@ -1,6 +1,8 @@
{% extends 'base.html' %}
{% block content %}
<h2>Theme Catalog (Simple)</h2>
<div class="page-header">
<h2>Themes</h2>
</div>
<p style="margin-bottom: 1rem; font-size: .875rem;">
See a theme that's missing or might be set up wrong? <a href="https://github.com/mwisnowski/mtg_python_deckbuilder/issues/new?template=general-theme-request.md" target="_blank" rel="noopener" style="color: var(--accent); text-decoration: underline;">Submit a request here</a>.
</p>