From bde9cf8a98df254cff5cf1afbb9ff1111686b88a Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Wed, 22 Jul 2026 23:44:21 -0500 Subject: [PATCH] Cohesive styling pass: design tokens, shared primitives, responsive groundwork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI had a good terracotta-on-dark system that wasn't applied consistently — magic-number spacing, ad-hoc page headers, ~6 copied panel surfaces, ~8 drifting label variants, and scattered accent use. - Tokens: warm the neutral ramp, add spacing/radius/type/motion scales, --accent-quiet + panel highlight. Terracotta hue unchanged. - Primitives: .panel, .section-label, .page-header/.toolbar — collapse the copied surfaces and label variants onto shared classes. - Unify each filter-heavy page's display controls (group/sort/view, select-all, count) into a workspace toolbar inside the filter panel, so they read as one surface instead of floating above it. - Labels: sentence-case, higher-contrast form labels; uppercase section markers bumped faint -> muted. - Accent hierarchy: reserve full terracotta for primary actions/active state; demote decorative labels and the group-header underline. - Responsive groundwork: reflow classes for the deck two-pane and nav rail (stacks / icon-collapse at breakpoints), touch-target sizing. Desktop unchanged in kind. --- src/App.tsx | 3 +- src/styles.css | 386 ++++++++++++++++++++++++++--------- src/views/CardEntry.tsx | 8 +- src/views/CardSearch.tsx | 33 ++- src/views/DeckView.tsx | 12 +- src/views/LibraryBrowser.tsx | 61 +++--- 6 files changed, 361 insertions(+), 142 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c695886..552f004 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -325,9 +325,10 @@ export default function App() { key={n.id} className={`nav-item ${tab === n.id ? "active" : ""}`} onClick={() => setTab(n.id)} + title={n.label} > {n.icon} - {n.label} + {n.label} ))} diff --git a/src/styles.css b/src/styles.css index 9ccf684..c5261d5 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4,30 +4,38 @@ ============================================================ */ :root { - /* Layered surfaces (darkest → lightest) */ - --bg-rail: #16171a; - --bg-app: #1a1b1f; - --bg-topbar: #1e1f24; - --bg-content: #212228; - --bg-panel: #282a31; - --bg-input: #2f313a; - --bg-hover: #363841; - --bg-elev: #3a3c46; + /* Layered surfaces (darkest → lightest). A warm charcoal ramp (low-saturation + ~28° hue) rather than cool blue-grey — reads as "collection in warm light" + and harmonizes with the terracotta accent. Elevation model: + rail/app (chrome) → content (page) → panel (surface) → input (inset). */ + --bg-rail: #191714; + --bg-app: #1d1a16; + --bg-topbar: #221e19; + --bg-content: #27221d; + --bg-panel: #322c25; + --bg-input: #3b342d; + --bg-hover: #443c34; + --bg-elev: #4c433a; - --border: #34363e; - --border-soft: rgba(255, 255, 255, 0.055); + --border: #3d372f; + --border-soft: rgba(255, 248, 240, 0.06); + /* Tactile 1px top inner-highlight for raised surfaces (see .panel). */ + --panel-highlight: inset 0 1px 0 rgba(255, 246, 236, 0.045); - --text: #dbdee4; - --text-strong: #f5f6f8; - --muted: #9a9ea8; - --faint: #71757f; + --text: #e3ddd3; + --text-strong: #f8f4ec; + --muted: #a69d90; + --faint: #7d7568; - /* Terracotta accent */ + /* Terracotta accent (hue unchanged — the app's signature) */ --accent: #c8553d; --accent-hover: #d86e56; --accent-active: #a8412d; --accent-subtle: rgba(200, 85, 61, 0.15); --accent-glow: rgba(200, 85, 61, 0.35); + /* Quiet accent for decorative/label use, so full terracotta stays reserved + for primary actions + active state (accent hierarchy). */ + --accent-quiet: #c69a86; /* Complementary / status */ --info: #46a6bd; /* teal complement */ @@ -44,8 +52,39 @@ --mtg-g: #479b63; --mtg-c: #9aa0ab; - --radius: 8px; - --radius-lg: 12px; + /* Spacing scale — every gap/margin/padding resolves to one of these. */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 32px; + --space-8: 48px; + + /* Radius scale */ + --radius-sm: 6px; + --radius: 9px; + --radius-lg: 14px; + --radius-card: 12px; /* card-art corners (matches Scryfall image framing) */ + --radius-pill: 999px; + + /* Type scale */ + --fs-micro: 10px; /* dense data labels (under stat numbers, chart axes) */ + --fs-caption: 12px; + --fs-body: 14px; + --fs-label: 11px; /* canonical uppercase micro-label, see .section-label */ + --fs-title: 16px; + --fs-display: 21px; + --tracking-label: 0.06em; + + /* Motion */ + --dur-fast: 0.12s; + --dur: 0.2s; + + /* Interactive control height — bumped on coarse pointers (see bottom). */ + --control-h: 34px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4); --shadow-lg: 0 14px 44px rgba(0, 0, 0, 0.55); @@ -210,6 +249,68 @@ input[type="checkbox"] { cursor: pointer; } +/* ============================================================ + Primitives: shared surface / label / page-header building blocks. + These replace treatments that were previously hand-copied (with drift) + across the views — edit here to change all of them at once. + ============================================================ */ + +/* Canonical raised surface. Replaces the ~6 hand-copied panel treatments + (deck-meta, commander-hero, deck-stats, filters, legality-tile base, + preview-info). The top inner-highlight gives a subtle tactile edge. */ +.panel { + background: var(--bg-panel); + border: 1px solid var(--border-soft); + border-radius: var(--radius-lg); + padding: var(--space-4); + box-shadow: var(--panel-highlight); +} +/* Recessed variant for wells nested inside a panel. */ +.panel-inset { + background: var(--bg-app); + border: 1px solid var(--border-soft); + border-radius: var(--radius); + padding: var(--space-3); +} + +/* Canonical uppercase section marker (panel/section headers). Uppercase + + tracking reads as structural; `--muted` (not faint) keeps it legible. */ +.section-label { + font-size: var(--fs-label); + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--tracking-label); + color: var(--muted); +} + +/* Standard page header: title on the left, control cluster on the right, + wrapping onto two lines on a narrow viewport. Replaces the ad-hoc + `
` each view used. */ +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--space-3); + margin-bottom: var(--space-4); +} +.page-header-title { + display: flex; + align-items: center; + gap: var(--space-3); + margin: 0; + min-width: 0; +} +/* The right-hand control cluster (group-by + sort + view toggle + page + actions). Also usable standalone. */ +.page-header-actions, +.toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-4); +} + /* ============================================================ App shell: nav rail + main ============================================================ */ @@ -338,11 +439,11 @@ input[type="checkbox"] { margin-bottom: 10px; } .lib-stats-title { - font-size: 12px; + font-size: var(--fs-label); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--faint); + letter-spacing: var(--tracking-label); + color: var(--muted); } .lib-stats-loading { font-size: 12px; @@ -381,7 +482,7 @@ input[type="checkbox"] { text-shadow: 0 0 14px var(--accent-glow); } .stat-tile-label { - font-size: 10px; + font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; @@ -582,7 +683,7 @@ input[type="checkbox"] { .content { flex: 1; overflow: auto; - padding: 22px 26px; + padding: var(--space-5) var(--space-6); } /* Empty / welcome state */ @@ -614,27 +715,28 @@ input[type="checkbox"] { ============================================================ */ h2 { - margin: 2px 0 14px; - font-size: 20px; + margin: var(--space-1) 0 var(--space-4); + font-size: var(--fs-display); font-weight: 700; color: var(--text-strong); letter-spacing: -0.2px; } h3 { - margin: 20px 0 12px; - font-size: 16px; + margin: var(--space-5) 0 var(--space-3); + font-size: var(--fs-title); font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-strong); border-bottom: 2px solid var(--border-soft); - padding-bottom: 10px; + padding-bottom: var(--space-3); } h4 { - margin: 14px 0 7px; - font-size: 14px; + margin: var(--space-4) 0 var(--space-2); + font-size: var(--fs-body); font-weight: 600; - color: var(--accent-hover); + /* Quiet accent — full terracotta is reserved for primary actions. */ + color: var(--accent-quiet); } /* "Group by" sub-headers (e.g. per card type/subtype/CMC/color within a @@ -646,15 +748,16 @@ h4 { position: sticky; top: 0; z-index: 5; - margin: 14px 0 8px; - padding: 8px 0; + margin: var(--space-4) 0 var(--space-2); + padding: var(--space-2) 0; background: var(--bg-app); - font-size: 15px; + font-size: var(--fs-body); font-weight: 700; text-transform: none; letter-spacing: normal; color: var(--text-strong); - border-bottom: 2px solid var(--accent); + /* Quiet warm underline rather than full terracotta (accent hierarchy). */ + border-bottom: 2px solid var(--accent-quiet); } .muted { color: var(--muted); @@ -673,10 +776,10 @@ h4 { display: inline-flex; align-items: center; background: var(--accent-subtle); - color: var(--accent-hover); - border-radius: 999px; - padding: 1px 9px; - font-size: 12px; + color: var(--accent-quiet); + border-radius: var(--radius-pill); + padding: 1px var(--space-2); + font-size: var(--fs-caption); font-weight: 700; } @@ -1232,12 +1335,12 @@ table { } th { text-align: left; - padding: 8px 12px; - font-size: 12px; + padding: var(--space-2) var(--space-3); + font-size: var(--fs-label); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--faint); + letter-spacing: var(--tracking-label); + color: var(--muted); position: sticky; top: 0; background: var(--bg-content); @@ -1339,27 +1442,53 @@ tbody tr:hover { Filters panel ============================================================ */ +/* The workspace panel unifies the filter grid and the results toolbar into + one surface (Library + Card Search), so the display controls read as part + of the filtering apparatus instead of floating loosely above it. */ +.workspace { + margin-bottom: var(--space-4); +} +/* Just the responsive field grid; surface comes from the wrapping `.panel`. */ .filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); - gap: 14px; - background: var(--bg-panel); - border: 1px solid var(--border-soft); - border-radius: var(--radius-lg); - padding: 16px; - margin-bottom: 16px; + gap: var(--space-4); +} +/* The results toolbar strip at the bottom of the workspace panel: selection + + count on the left, group/sort/view on the right, divided from the filters. */ +.workspace-bar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 1px solid var(--border-soft); +} +.workspace-bar-check { + display: inline-flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; + font-size: var(--fs-caption); + color: var(--muted); +} +.workspace-bar-count { + font-size: var(--fs-caption); + color: var(--faint); + font-variant-numeric: tabular-nums; } .field { display: flex; flex-direction: column; gap: 6px; } +/* Form field labels: sentence-case and higher-contrast rather than tiny faint + all-caps — clearer to scan above each input than a structural section marker. */ .field > label { - font-size: 12px; + font-size: var(--fs-caption); font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--faint); + color: var(--muted); } .location-input { padding: 4px 8px; @@ -1631,12 +1760,12 @@ tbody tr:hover { flex: 0 0 auto; display: flex; flex-direction: column; - gap: 12px; + gap: var(--space-3); background: var(--bg-panel); border: 1px solid var(--border-soft); - border-radius: 18px; - padding: 18px; - box-shadow: var(--shadow-lg); + border-radius: var(--radius-lg); + padding: var(--space-4); + box-shadow: var(--shadow-lg), var(--panel-highlight); color: var(--text); } .preview-info-name { @@ -1671,11 +1800,11 @@ tbody tr:hover { border-top: 1px solid var(--border-soft); } .preview-info-label { - font-size: 11px; + font-size: var(--fs-label); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--faint); + letter-spacing: var(--tracking-label); + color: var(--muted); } .preview-info-deck-chips { display: flex; @@ -1735,10 +1864,10 @@ tbody tr:hover { .tile-img { width: 100%; display: block; - border-radius: 12px; + border-radius: var(--radius-card); box-shadow: var(--shadow-sm); cursor: zoom-in; - transition: transform 0.12s, box-shadow 0.12s; + transition: transform var(--dur-fast), box-shadow var(--dur-fast); } .tile:hover .tile-img { transform: translateY(-3px); @@ -1747,7 +1876,7 @@ tbody tr:hover { .tile-noimg { aspect-ratio: 488 / 680; border: 1px solid var(--border); - border-radius: 12px; + border-radius: var(--radius-card); background: var(--bg-panel); display: flex; align-items: center; @@ -1890,15 +2019,25 @@ tbody tr:hover { Deck metadata (format + commander) ============================================================ */ +/* Two-pane deck screen: deck list + selected-deck detail. Stacks below the + tablet breakpoint (see the responsive section) so the list sits above the + detail on narrow/portrait viewports. */ +.deck-layout { + display: flex; + gap: var(--space-5); + align-items: flex-start; +} +.deck-sidebar { + width: 240px; + flex-shrink: 0; +} + +/* Surface from `.panel`; layout only here. */ .deck-meta { display: flex; flex-wrap: wrap; align-items: flex-start; - gap: 20px; - background: var(--bg-panel); - border: 1px solid var(--border-soft); - border-radius: var(--radius-lg); - padding: 14px 16px; + gap: var(--space-5); } /* Separates the deck-level info panels (commander, format, analysis, legality) from the card-management controls + lists below. */ @@ -1957,15 +2096,12 @@ tbody tr:hover { flex: 1; } -/* Prominent commander card display, above the fold on a Commander/Brawl deck. */ +/* Prominent commander card display, above the fold on a Commander/Brawl deck. + Surface from `.panel`; layout only here. */ .commander-hero { display: flex; align-items: center; - gap: 16px; - background: var(--bg-panel); - border: 1px solid var(--border-soft); - border-radius: var(--radius-lg); - padding: 14px 16px; + gap: var(--space-4); } .commander-hero-img { width: 84px; @@ -1998,14 +2134,16 @@ tbody tr:hover { gap: 5px; } .commander-hero-label { - font-size: 10px; + font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--accent-hover); + letter-spacing: var(--tracking-label); + /* Quiet accent keeps a hint of brand on the hero without competing with + primary-action terracotta elsewhere. */ + color: var(--accent-quiet); } .commander-hero-name { - font-size: 19px; + font-size: var(--fs-display); font-weight: 700; color: var(--text-strong); } @@ -2014,20 +2152,9 @@ tbody tr:hover { color: var(--muted); } -/* Per-deck analysis panel. */ -.deck-stats { - background: var(--bg-panel); - border: 1px solid var(--border-soft); - border-radius: var(--radius-lg); - padding: 14px 16px; -} +/* Per-deck analysis panel. Surface from `.panel`. */ .deck-stats-head { - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--faint); - margin-bottom: 10px; + margin-bottom: var(--space-3); } .deck-stats-body { display: flex; @@ -2048,11 +2175,84 @@ tbody tr:hover { flex-direction: column; } .deck-stats-curve-label { - font-size: 10px; + font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint); - margin-bottom: 2px; + margin-bottom: var(--space-1); +} + +/* ============================================================ + Responsive groundwork + Desktop (the primary target) is unchanged above; these breakpoints keep + the layout usable as the window narrows and lay the foundation for a + future mobile port. Breakpoints: 1000 (tablet) and 720 (compact). + ============================================================ */ + +/* Tablet: the deck two-pane stacks (list above detail). */ +@media (max-width: 1000px) { + .deck-layout { + flex-direction: column; + } + .deck-sidebar { + width: 100%; + } +} + +/* Compact: tighten page gutters and collapse the nav rail to an icon strip + (same items — labels, wordmark, and the library summary hide; hover/title + still names each tab). No separate mobile navigation is introduced. */ +@media (max-width: 720px) { + .content { + padding: var(--space-4); + } + .nav-rail { + width: 60px; + padding: var(--space-3) var(--space-2) var(--space-2); + } + .brand { + justify-content: center; + padding: var(--space-1) 0 var(--space-4); + } + .brand-text { + display: none; + } + .nav-item { + justify-content: center; + padding: var(--space-2); + } + .nav-label { + display: none; + } + .nav-item.active::before { + left: -8px; + } + .nav-foot { + display: none; + } + .topbar { + padding: 0 var(--space-3); + } + /* The library path is the first thing to go when horizontal space is tight — + the library actions stay reachable. */ + .path { + display: none; + } +} + +/* Touch input (future mobile / touchscreen laptops): grow tap targets to a + comfortable size without inflating the compact mouse-driven desktop look. */ +@media (pointer: coarse) { + button, + input, + select, + .nav-item { + min-height: var(--control-h); + } + input[type="checkbox"] { + width: 18px; + height: 18px; + } } diff --git a/src/views/CardEntry.tsx b/src/views/CardEntry.tsx index 8701c2e..8d268c8 100644 --- a/src/views/CardEntry.tsx +++ b/src/views/CardEntry.tsx @@ -78,9 +78,11 @@ export function CardEntry() { return (
-

Add Cards

+
+

Add Cards

+
-
+
0 && ( <> -

Just added

+

Just added

    {recent.map((r, i) => (
  • diff --git a/src/views/CardSearch.tsx b/src/views/CardSearch.tsx index a1b6410..7d2c3e6 100644 --- a/src/views/CardSearch.tsx +++ b/src/views/CardSearch.tsx @@ -267,11 +267,11 @@ export function CardSearch({ return (
    -
    -

    +
    +

    Card Search {hasActiveFilter && {rows.length}}

    -
    +
    - - setSortDir(sortDir === "asc" ? "desc" : "asc")} - /> -
    +
    @@ -357,6 +350,24 @@ export function CardSearch({ placeholder="e.g. landfall" />
    +
    + +
    + {hasActiveFilter && ( + + {rows.length} result{rows.length === 1 ? "" : "s"} + + )} + + + setSortDir(sortDir === "asc" ? "desc" : "asc")} + /> + +
    {!hasActiveFilter ? ( diff --git a/src/views/DeckView.tsx b/src/views/DeckView.tsx index 6f239dd..4221354 100644 --- a/src/views/DeckView.tsx +++ b/src/views/DeckView.tsx @@ -159,7 +159,7 @@ function CommanderHero({ card, onClear }: { card: DeckEntry; onClear: () => void const { handlers } = useHoverPreview(); const { open } = useClickPreview(); return ( -
    +
    {card.image_uri ? ( -
    +
    +

    Decks

    @@ -925,7 +925,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) { {/* Deck metadata: format + (for Commander/Brawl) the commander card. */}
    -
    +
    -

    - Library {rows.length} -

    -
    -
    +
    +

    + Library {rows.length} +

    +
    - - setSortDir(sortDir === "asc" ? "desc" : "asc")} - /> -
    @@ -538,7 +516,8 @@ export function LibraryBrowser({ /> )} -
    +
    +
    setText(e.target.value)} placeholder="e.g. bolt" /> @@ -620,6 +599,32 @@ export function LibraryBrowser({ placeholder="e.g. landfall" />
    +
    + +
    + + + {rows.length} card{rows.length === 1 ? "" : "s"} + + + + setSortDir(sortDir === "asc" ? "desc" : "asc")} + /> + +
    {rows.length === 0 ? (