Cohesive styling pass: design tokens, shared primitives, responsive groundwork
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.
This commit is contained in:
@@ -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}
|
||||
>
|
||||
<span className="nav-icon">{n.icon}</span>
|
||||
{n.label}
|
||||
<span className="nav-label">{n.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
386
src/styles.css
386
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
|
||||
`<div class="row" style="justify-content:space-between">` 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,11 @@ export function CardEntry() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Add Cards</h2>
|
||||
<div className="page-header">
|
||||
<h2 className="page-header-title">Add Cards</h2>
|
||||
</div>
|
||||
|
||||
<div className="field" style={{ maxWidth: 260, marginBottom: 12 }}>
|
||||
<div className="field" style={{ maxWidth: 260, marginBottom: "var(--space-3)" }}>
|
||||
<label>Location for added cards (optional)</label>
|
||||
<input
|
||||
placeholder="e.g. Box 1 — prefilled onto each card"
|
||||
@@ -102,7 +104,7 @@ export function CardEntry() {
|
||||
|
||||
{recent.length > 0 && (
|
||||
<>
|
||||
<h2 style={{ marginTop: 24 }}>Just added</h2>
|
||||
<h2 style={{ marginTop: "var(--space-6)" }}>Just added</h2>
|
||||
<ul className="result-list">
|
||||
{recent.map((r, i) => (
|
||||
<li key={i} style={{ cursor: "default" }}>
|
||||
|
||||
@@ -267,11 +267,11 @@ export function CardSearch({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<h2 style={{ margin: 0 }}>
|
||||
<div className="page-header">
|
||||
<h2 className="page-header-title">
|
||||
Card Search {hasActiveFilter && <span className="badge">{rows.length}</span>}
|
||||
</h2>
|
||||
<div className="row" style={{ gap: 16 }}>
|
||||
<div className="page-header-actions">
|
||||
<select
|
||||
value={deckId}
|
||||
onChange={(e) => setDeckId(e.target.value === "" ? "" : Number(e.target.value))}
|
||||
@@ -283,17 +283,10 @@ export function CardSearch({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
||||
<SortBySelect
|
||||
value={sortBy}
|
||||
dir={sortDir}
|
||||
onChangeValue={setSortBy}
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel workspace">
|
||||
<div className="filters">
|
||||
<div className="field">
|
||||
<label>Name contains</label>
|
||||
@@ -357,6 +350,24 @@ export function CardSearch({
|
||||
placeholder="e.g. landfall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-bar">
|
||||
{hasActiveFilter && (
|
||||
<span className="workspace-bar-count">
|
||||
{rows.length} result{rows.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
)}
|
||||
<span className="spacer" />
|
||||
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
||||
<SortBySelect
|
||||
value={sortBy}
|
||||
dir={sortDir}
|
||||
onChangeValue={setSortBy}
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasActiveFilter ? (
|
||||
|
||||
@@ -159,7 +159,7 @@ function CommanderHero({ card, onClear }: { card: DeckEntry; onClear: () => void
|
||||
const { handlers } = useHoverPreview();
|
||||
const { open } = useClickPreview();
|
||||
return (
|
||||
<div className="commander-hero">
|
||||
<div className="panel commander-hero">
|
||||
{card.image_uri ? (
|
||||
<img
|
||||
className="commander-hero-img"
|
||||
@@ -818,8 +818,8 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 20 }}>
|
||||
<div style={{ width: 240, flexShrink: 0 }}>
|
||||
<div className="deck-layout">
|
||||
<div className="deck-sidebar">
|
||||
<div className="row" style={{ justifyContent: "space-between" }}>
|
||||
<h2>Decks</h2>
|
||||
<IconButton icon={Icons.upload} label="Import deck…" onClick={pickImportFile} />
|
||||
@@ -925,7 +925,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
|
||||
|
||||
{/* Deck metadata: format + (for Commander/Brawl) the commander card. */}
|
||||
<div className="deck-top-meta">
|
||||
<div className="deck-meta">
|
||||
<div className="panel deck-meta">
|
||||
<label className="row" style={{ gap: 6 }}>
|
||||
<span className="muted">Format</span>
|
||||
<select value={deck.format} onChange={(e) => changeFormat(e.target.value)}>
|
||||
@@ -958,8 +958,8 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
|
||||
|
||||
{contents.length > 0 && (
|
||||
<div className="deck-top-stats">
|
||||
<div className="deck-stats">
|
||||
<div className="deck-stats-head">Deck analysis</div>
|
||||
<div className="panel deck-stats">
|
||||
<div className="deck-stats-head section-label">Deck analysis</div>
|
||||
<div className="deck-stats-body">
|
||||
<div className="deck-stats-numbers">
|
||||
<div className="stat-tiles-row">
|
||||
|
||||
@@ -399,34 +399,12 @@ export function LibraryBrowser({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<div className="row" style={{ gap: 14 }}>
|
||||
<label className="row" style={{ gap: 6, cursor: "pointer" }} title="Select all filtered cards">
|
||||
<input
|
||||
ref={selectAllRef}
|
||||
type="checkbox"
|
||||
checked={rows.length > 0 && selected.size === rows.length}
|
||||
onChange={toggleSelectAll}
|
||||
disabled={rows.length === 0}
|
||||
/>
|
||||
<span className="muted" style={{ fontSize: 12 }}>
|
||||
Select all
|
||||
</span>
|
||||
</label>
|
||||
<h2 style={{ margin: 0 }}>
|
||||
Library <span className="badge">{rows.length}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="row" style={{ gap: 16 }}>
|
||||
<div className="page-header">
|
||||
<h2 className="page-header-title">
|
||||
Library <span className="badge">{rows.length}</span>
|
||||
</h2>
|
||||
<div className="page-header-actions">
|
||||
<button onClick={pickImportFile}>Import cards…</button>
|
||||
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
||||
<SortBySelect
|
||||
value={sortBy}
|
||||
dir={sortDir}
|
||||
onChangeValue={setSortBy}
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -538,7 +516,8 @@ export function LibraryBrowser({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="filters">
|
||||
<div className="panel workspace">
|
||||
<div className="filters">
|
||||
<div className="field">
|
||||
<label>Name contains</label>
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. bolt" />
|
||||
@@ -620,6 +599,32 @@ export function LibraryBrowser({
|
||||
placeholder="e.g. landfall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-bar">
|
||||
<label className="workspace-bar-check" title="Select all filtered cards">
|
||||
<input
|
||||
ref={selectAllRef}
|
||||
type="checkbox"
|
||||
checked={rows.length > 0 && selected.size === rows.length}
|
||||
onChange={toggleSelectAll}
|
||||
disabled={rows.length === 0}
|
||||
/>
|
||||
Select all
|
||||
</label>
|
||||
<span className="workspace-bar-count">
|
||||
{rows.length} card{rows.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
<span className="spacer" />
|
||||
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
||||
<SortBySelect
|
||||
value={sortBy}
|
||||
dir={sortDir}
|
||||
onChangeValue={setSortBy}
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user