Printing picker: quick-filter box for cards with many printings

Filters by set code, collector number, year, rarity, language and finish,
matching each word against the start of a field. Shown when a card has 7+
printings.

Closes #31

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 01:04:31 +00:00
parent ea689b0f2b
commit a92b650dab
2 changed files with 52 additions and 1 deletions

View File

@@ -1261,9 +1261,33 @@ function PrintingsSection({
);
}
/** Below this many printings the picker skips its filter box. */
const PRINTING_FILTER_MIN = 7;
/** Whether a printing matches a filter query: every word must start one of
* the things the picker shows for it — set code, collector number, year,
* rarity, language, finish. Prefixes rather than substrings, so "foil"
* doesn't also match every nonfoil printing. */
function printingMatches(p: PrintingRef, query: string): boolean {
const fields = [
p.set_code,
p.collector_number,
`#${p.collector_number}`,
p.released_at,
p.rarity,
p.lang,
...p.finishes.map((f) => FINISH_LABELS[f] ?? f),
].map(fold);
return fold(query)
.split(/\s+/)
.filter(Boolean)
.every((word) => fields.some((f) => f.startsWith(word)));
}
/**
* A grid of a card's printings to pick from. With `chooseFinish`, each
* printing also offers its finishes (clicking the art takes the first).
* Long lists (basic lands run to hundreds) get a filter box.
*
* Opens above the click preview, which closes on any Escape it hears on
* `window` — so this listens in the capture phase and stops the event there,
@@ -1294,6 +1318,8 @@ function PrintingPicker({
return () => window.removeEventListener("keydown", onKey, true);
}, [onCancel]);
useBackButton(onCancel);
const [query, setQuery] = useState("");
const shown = printings.filter((p) => printingMatches(p, query));
return createPortal(
<div
@@ -1308,8 +1334,23 @@ function PrintingPicker({
<h3 className="dialog-title">{chooseFinish ? `Add a printing of ${name}` : `Show ${name} as`}</h3>
<IconButton icon={Icons.close} label="Close" onClick={onCancel} />
</div>
{printings.length >= PRINTING_FILTER_MIN && (
<div className="printing-filter">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter by set, number, year, rarity, finish…"
autoFocus
/>
<span className="muted">
{shown.length === printings.length ? printings.length : `${shown.length} of ${printings.length}`}
</span>
</div>
)}
{shown.length === 0 && <p className="hint">No printings match.</p>}
<div className="printing-grid">
{printings.map((p) => {
{shown.map((p) => {
const finishes = p.finishes.length > 0 ? p.finishes : ["nonfoil"];
const meta = [
p.released_at.slice(0, 4),

View File

@@ -3949,6 +3949,16 @@ button.pull-action {
.printing-picker-backdrop {
z-index: 500;
}
.printing-filter {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.printing-filter input {
flex: 1;
min-width: 0;
}
.printing-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(130px, 40vw), 1fr));