Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
import {
|
|
|
|
|
|
createContext,
|
|
|
|
|
|
ReactNode,
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
useContext,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useRef,
|
|
|
|
|
|
useState,
|
|
|
|
|
|
} from "react";
|
|
|
|
|
|
import { api, CardRef, COLORS, maskToLetters } from "./api";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Inline editor for a card's free-text storage location (e.g. "Box 1").
|
|
|
|
|
|
* Saves on blur; used for owned cards that aren't in any deck.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function LocationField({
|
|
|
|
|
|
oracleId,
|
|
|
|
|
|
location,
|
|
|
|
|
|
onSaved,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
oracleId: string;
|
|
|
|
|
|
location: string;
|
|
|
|
|
|
onSaved?: () => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [val, setVal] = useState(location);
|
|
|
|
|
|
useEffect(() => setVal(location), [location]);
|
|
|
|
|
|
|
|
|
|
|
|
const save = () => {
|
|
|
|
|
|
if (val !== location) api.setLocation(oracleId, val).then(() => onSaved?.());
|
|
|
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
|
|
|
<input
|
|
|
|
|
|
className="location-input"
|
|
|
|
|
|
placeholder="Location…"
|
|
|
|
|
|
value={val}
|
|
|
|
|
|
onChange={(e) => setVal(e.target.value)}
|
|
|
|
|
|
onBlur={save}
|
|
|
|
|
|
onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* A small pill-shaped [− N +] quantity stepper, used consistently for owned-copy
|
|
|
|
|
|
* counts on the Add Cards page and both Library views.
|
|
|
|
|
|
*
|
|
|
|
|
|
* - `overlay`: dark translucent variant for use atop card art (tile corners),
|
|
|
|
|
|
* instead of the default panel-toned variant used in rows/lists.
|
|
|
|
|
|
* - `warn`: tints the pill amber (e.g. "assigned to more decks than owned").
|
|
|
|
|
|
* - `pending`: dims the pill (e.g. mid grace-period before a 0-qty row is removed).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function QtyStepper({
|
|
|
|
|
|
value,
|
|
|
|
|
|
onDecrement,
|
|
|
|
|
|
onIncrement,
|
|
|
|
|
|
overlay,
|
|
|
|
|
|
warn,
|
|
|
|
|
|
pending,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
value: number;
|
|
|
|
|
|
onDecrement: () => void;
|
|
|
|
|
|
onIncrement: () => void;
|
|
|
|
|
|
overlay?: boolean;
|
|
|
|
|
|
warn?: boolean;
|
|
|
|
|
|
pending?: boolean;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={[
|
|
|
|
|
|
"qty-stepper",
|
|
|
|
|
|
overlay ? "qty-stepper-overlay" : "",
|
|
|
|
|
|
warn ? "warn" : "",
|
|
|
|
|
|
pending ? "pending" : "",
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(" ")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="qty-stepper-btn"
|
|
|
|
|
|
onClick={onDecrement}
|
|
|
|
|
|
disabled={value <= 0}
|
|
|
|
|
|
title="Remove a copy"
|
|
|
|
|
|
>
|
|
|
|
|
|
−
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span className="qty-stepper-value">{value}</span>
|
|
|
|
|
|
<button type="button" className="qty-stepper-btn" onClick={onIncrement} title="Add a copy">
|
|
|
|
|
|
+
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
/** Render a color-identity/colors bitmask as WUBRG pips (colorless = a "C" pip). */
|
|
|
|
|
|
export function ColorPips({ mask }: { mask: number }) {
|
|
|
|
|
|
const letters = maskToLetters(mask);
|
|
|
|
|
|
if (letters.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="pips">
|
|
|
|
|
|
<span className="pip C">C</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="pips">
|
|
|
|
|
|
{letters.split("").map((c) => (
|
|
|
|
|
|
<span key={c} className={`pip ${c}`}>
|
|
|
|
|
|
{c}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Small card-image thumbnail. Hovering it shows a cursor-tracking preview;
|
|
|
|
|
|
* clicking it opens a larger, centered preview (with a flip button for
|
|
|
|
|
|
* double-faced cards).
|
|
|
|
|
|
*/
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
export function CardThumb({ card }: { card: CardRef }) {
|
|
|
|
|
|
const { handlers } = useHoverPreview();
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
const { open } = useClickPreview();
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
if (!card.image_uri) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="thumb-empty" title="No image">
|
|
|
|
|
|
🂠
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<img
|
|
|
|
|
|
className="card-thumb"
|
|
|
|
|
|
src={card.image_uri}
|
|
|
|
|
|
alt={card.name}
|
|
|
|
|
|
loading="lazy"
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
open(card);
|
|
|
|
|
|
}}
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
{...handlers(card)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A search input plus a results dropdown with keyboard navigation:
|
|
|
|
|
|
* ↑/↓ move the highlight, Enter picks, mouse hover/click also work.
|
|
|
|
|
|
* `value`/`onChange` are controlled by the caller (which runs the search).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function CardSearchBox({
|
|
|
|
|
|
placeholder,
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
results,
|
|
|
|
|
|
onPick,
|
|
|
|
|
|
autoFocus,
|
|
|
|
|
|
big,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
placeholder: string;
|
|
|
|
|
|
value: string;
|
|
|
|
|
|
onChange: (v: string) => void;
|
|
|
|
|
|
results: CardRef[];
|
|
|
|
|
|
onPick: (card: CardRef) => void;
|
|
|
|
|
|
autoFocus?: boolean;
|
|
|
|
|
|
/** Larger, more prominent input (used for the primary Add Cards field). */
|
|
|
|
|
|
big?: boolean;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [sel, setSel] = useState(0);
|
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
useEffect(() => setSel(0), [results]);
|
|
|
|
|
|
|
|
|
|
|
|
// Keep focus in the box after a pick so cards can be added in a rapid streak.
|
|
|
|
|
|
function pick(card: CardRef) {
|
|
|
|
|
|
onPick(card);
|
|
|
|
|
|
inputRef.current?.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onKeyDown(e: React.KeyboardEvent) {
|
|
|
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setSel((s) => Math.min(s + 1, results.length - 1));
|
|
|
|
|
|
} else if (e.key === "ArrowUp") {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setSel((s) => Math.max(s - 1, 0));
|
|
|
|
|
|
} else if (e.key === "Enter" && results[sel]) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
pick(results[sel]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
|
autoFocus={autoFocus}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: "100%",
|
|
|
|
|
|
marginBottom: 8,
|
|
|
|
|
|
...(big ? { fontSize: 16, padding: "10px 12px" } : {}),
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
onChange={(e) => onChange(e.target.value)}
|
|
|
|
|
|
onKeyDown={onKeyDown}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{results.length > 0 && (
|
|
|
|
|
|
<ul className="result-list" style={{ marginBottom: 14 }}>
|
|
|
|
|
|
{results.map((c, i) => (
|
|
|
|
|
|
<li
|
|
|
|
|
|
key={c.oracle_id}
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
style={i === sel ? { background: "var(--accent-subtle)" } : undefined}
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
onMouseEnter={() => setSel(i)}
|
|
|
|
|
|
onClick={() => pick(c)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ColorPips mask={c.color_identity} />
|
|
|
|
|
|
<span className="card-name">{c.name}</span>
|
|
|
|
|
|
<span className="card-type">{c.type_line}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
// --- Hover + click preview -------------------------------------------------
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
|
|
interface HoverApi {
|
|
|
|
|
|
enter: (card: CardRef, e: React.MouseEvent) => void;
|
|
|
|
|
|
move: (e: React.MouseEvent) => void;
|
|
|
|
|
|
leave: () => void;
|
|
|
|
|
|
}
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
interface ClickApi {
|
|
|
|
|
|
open: (card: CardRef) => void;
|
|
|
|
|
|
}
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
const HoverCtx = createContext<HoverApi | null>(null);
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
const ClickCtx = createContext<ClickApi | null>(null);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
|
|
/**
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
* Provides card-image previews app-wide: a small floating preview that
|
|
|
|
|
|
* tracks the cursor while hovering (see [`useHoverPreview`]), and a larger
|
|
|
|
|
|
* centered modal on click (see [`useClickPreview`]) — with a flip button for
|
|
|
|
|
|
* double-faced cards. Wrap the app once.
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
*/
|
|
|
|
|
|
export function HoverPreviewProvider({ children }: { children: ReactNode }) {
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
const [hover, setHover] = useState<{ card: CardRef; x: number; y: number } | null>(null);
|
|
|
|
|
|
const [clicked, setClicked] = useState<{ card: CardRef; showBack: boolean } | null>(null);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
|
|
const enter = useCallback((card: CardRef, e: React.MouseEvent) => {
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
if (card.image_uri) setHover({ card, x: e.clientX, y: e.clientY });
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
}, []);
|
|
|
|
|
|
const move = useCallback((e: React.MouseEvent) => {
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
setHover((s) => (s ? { ...s, x: e.clientX, y: e.clientY } : s));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
const leave = useCallback(() => setHover(null), []);
|
|
|
|
|
|
|
|
|
|
|
|
const open = useCallback((card: CardRef) => {
|
|
|
|
|
|
if (!card.image_uri) return;
|
|
|
|
|
|
setHover(null);
|
|
|
|
|
|
setClicked({ card, showBack: false });
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
}, []);
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
const close = useCallback(() => setClicked(null), []);
|
|
|
|
|
|
const flip = useCallback(() => setClicked((c) => (c ? { ...c, showBack: !c.showBack } : c)), []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!clicked) return;
|
|
|
|
|
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && close();
|
|
|
|
|
|
window.addEventListener("keydown", onKey);
|
|
|
|
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
|
|
|
|
}, [clicked, close]);
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<HoverCtx.Provider value={{ enter, move, leave }}>
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
<ClickCtx.Provider value={{ open }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
{!clicked && hover && <FloatingCard card={hover.card} x={hover.x} y={hover.y} />}
|
|
|
|
|
|
{clicked && <ClickPreviewModal state={clicked} onClose={close} onFlip={flip} />}
|
|
|
|
|
|
</ClickCtx.Provider>
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
</HoverCtx.Provider>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns a `handlers(card)` factory to spread onto a row/cell for hover preview. */
|
|
|
|
|
|
export function useHoverPreview() {
|
|
|
|
|
|
const api = useContext(HoverCtx);
|
|
|
|
|
|
return {
|
|
|
|
|
|
handlers: (card: CardRef) =>
|
|
|
|
|
|
api
|
|
|
|
|
|
? {
|
|
|
|
|
|
onMouseEnter: (e: React.MouseEvent) => api.enter(card, e),
|
|
|
|
|
|
onMouseMove: (e: React.MouseEvent) => api.move(e),
|
|
|
|
|
|
onMouseLeave: () => api.leave(),
|
|
|
|
|
|
}
|
|
|
|
|
|
: {},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
/** Returns `open(card)` to trigger the larger click-to-preview modal. */
|
|
|
|
|
|
export function useClickPreview() {
|
|
|
|
|
|
const api = useContext(ClickCtx);
|
|
|
|
|
|
return { open: (card: CardRef) => api?.open(card) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
const PREVIEW_W = 320;
|
|
|
|
|
|
const PREVIEW_H = 446; // ~ card aspect for the normal image
|
|
|
|
|
|
const GAP = 24;
|
|
|
|
|
|
const PAD = 12;
|
|
|
|
|
|
|
|
|
|
|
|
function FloatingCard({ card, x, y }: { card: CardRef; x: number; y: number }) {
|
|
|
|
|
|
// Prefer the right of the cursor; flip left if it would overflow.
|
|
|
|
|
|
let left = x + GAP;
|
|
|
|
|
|
if (left + PREVIEW_W + PAD > window.innerWidth) left = x - PREVIEW_W - GAP;
|
|
|
|
|
|
const top = Math.max(PAD, Math.min(y - PREVIEW_H / 2, window.innerHeight - PREVIEW_H - PAD));
|
|
|
|
|
|
return (
|
|
|
|
|
|
<img
|
|
|
|
|
|
className="hover-card"
|
|
|
|
|
|
src={card.image_uri}
|
|
|
|
|
|
alt={card.name}
|
|
|
|
|
|
style={{ left, top, width: PREVIEW_W }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
function ClickPreviewModal({
|
|
|
|
|
|
state,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onFlip,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
state: { card: CardRef; showBack: boolean };
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
onFlip: () => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const { card, showBack } = state;
|
|
|
|
|
|
const src = showBack && card.back_image_uri ? card.back_image_uri : card.image_uri;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="preview-modal-backdrop" onClick={onClose}>
|
|
|
|
|
|
<div className="preview-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<img className="preview-modal-img" src={src} alt={card.name} />
|
|
|
|
|
|
<button className="preview-modal-close" onClick={onClose} title="Close (Esc)">
|
|
|
|
|
|
✕
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{card.back_image_uri && (
|
|
|
|
|
|
<button className="preview-modal-flip" onClick={onFlip} title="Show the other side">
|
|
|
|
|
|
⟲ Flip
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
// --- Grouping -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export type GroupMode = "none" | "type" | "subtype" | "cmc" | "colors";
|
|
|
|
|
|
|
|
|
|
|
|
export const GROUP_OPTIONS: { value: GroupMode; label: string }[] = [
|
|
|
|
|
|
{ value: "none", label: "No grouping" },
|
|
|
|
|
|
{ value: "type", label: "Card type" },
|
|
|
|
|
|
{ value: "subtype", label: "Subtype" },
|
|
|
|
|
|
{ value: "cmc", label: "Mana value" },
|
|
|
|
|
|
{ value: "colors", label: "Color identity" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Priority for choosing a card's primary type from its (multi-type) type line.
|
|
|
|
|
|
const TYPE_PRIORITY = [
|
|
|
|
|
|
"Creature",
|
|
|
|
|
|
"Planeswalker",
|
|
|
|
|
|
"Battle",
|
|
|
|
|
|
"Land",
|
|
|
|
|
|
"Artifact",
|
|
|
|
|
|
"Enchantment",
|
|
|
|
|
|
"Instant",
|
|
|
|
|
|
"Sorcery",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function primaryType(typeLine: string): string {
|
|
|
|
|
|
const left = typeLine.split("—")[0];
|
|
|
|
|
|
for (const t of TYPE_PRIORITY) if (left.includes(t)) return t;
|
|
|
|
|
|
const words = left.trim().split(/\s+/);
|
|
|
|
|
|
return words[words.length - 1] || "Other";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function firstSubtype(typeLine: string): string {
|
|
|
|
|
|
const parts = typeLine.split("—");
|
|
|
|
|
|
if (parts.length < 2) return "No subtype";
|
|
|
|
|
|
return parts[1].trim().split(/\s+/)[0] || "No subtype";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function colorLabel(mask: number): string {
|
|
|
|
|
|
const letters = maskToLetters(mask);
|
|
|
|
|
|
return letters === "" ? "Colorless" : letters;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Group cards by the chosen attribute, returning ordered groups. */
|
|
|
|
|
|
export function groupCards<T extends CardRef>(
|
|
|
|
|
|
cards: T[],
|
|
|
|
|
|
mode: GroupMode,
|
|
|
|
|
|
): { label: string; cards: T[] }[] {
|
|
|
|
|
|
if (mode === "none") return [{ label: "", cards }];
|
|
|
|
|
|
|
|
|
|
|
|
const keyOf = (c: T) =>
|
|
|
|
|
|
mode === "type"
|
|
|
|
|
|
? primaryType(c.type_line)
|
|
|
|
|
|
: mode === "subtype"
|
|
|
|
|
|
? firstSubtype(c.type_line)
|
|
|
|
|
|
: mode === "cmc"
|
|
|
|
|
|
? `Mana value ${c.mana_value}`
|
|
|
|
|
|
: colorLabel(c.color_identity);
|
|
|
|
|
|
|
|
|
|
|
|
const map = new Map<string, T[]>();
|
|
|
|
|
|
for (const c of cards) {
|
|
|
|
|
|
const k = keyOf(c);
|
|
|
|
|
|
const arr = map.get(k);
|
|
|
|
|
|
if (arr) arr.push(c);
|
|
|
|
|
|
else map.set(k, [c]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const groups = [...map.entries()].map(([label, cs]) => ({ label, cards: cs }));
|
|
|
|
|
|
if (mode === "type") {
|
|
|
|
|
|
const rank = (l: string) => {
|
|
|
|
|
|
const i = TYPE_PRIORITY.indexOf(l);
|
|
|
|
|
|
return i === -1 ? TYPE_PRIORITY.length : i;
|
|
|
|
|
|
};
|
|
|
|
|
|
groups.sort((a, b) => rank(a.label) - rank(b.label) || a.label.localeCompare(b.label));
|
|
|
|
|
|
} else if (mode === "cmc") {
|
|
|
|
|
|
groups.sort((a, b) => a.cards[0].mana_value - b.cards[0].mana_value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
groups.sort((a, b) => a.label.localeCompare(b.label));
|
|
|
|
|
|
}
|
|
|
|
|
|
return groups;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Dropdown for choosing a [`GroupMode`]. */
|
|
|
|
|
|
export function GroupBySelect({
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
value: GroupMode;
|
|
|
|
|
|
onChange: (m: GroupMode) => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<label className="group-by">
|
|
|
|
|
|
<span className="muted">Group by</span>
|
|
|
|
|
|
<select value={value} onChange={(e) => onChange(e.target.value as GroupMode)}>
|
|
|
|
|
|
{GROUP_OPTIONS.map((o) => (
|
|
|
|
|
|
<option key={o.value} value={o.value}>
|
|
|
|
|
|
{o.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Persistence ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/** useState that mirrors its value to localStorage (JSON), so it survives
|
|
|
|
|
|
* remounts and app restarts. */
|
|
|
|
|
|
export function usePersistedState<T>(key: string, initial: T): [T, (v: T) => void] {
|
|
|
|
|
|
const [val, setVal] = useState<T>(() => {
|
|
|
|
|
|
const raw = localStorage.getItem(key);
|
|
|
|
|
|
if (raw === null) return initial;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(raw) as T;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return initial;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const set = useCallback(
|
|
|
|
|
|
(v: T) => {
|
|
|
|
|
|
localStorage.setItem(key, JSON.stringify(v));
|
|
|
|
|
|
setVal(v);
|
|
|
|
|
|
},
|
|
|
|
|
|
[key],
|
|
|
|
|
|
);
|
|
|
|
|
|
return [val, set];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- View mode (list vs. image tiles) ------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export type ViewMode = "list" | "tiles";
|
|
|
|
|
|
|
|
|
|
|
|
/** View-mode state persisted to localStorage so it survives tab switches. */
|
|
|
|
|
|
export function useViewMode(key: string): [ViewMode, (m: ViewMode) => void] {
|
|
|
|
|
|
const [mode, setMode] = useState<ViewMode>(
|
|
|
|
|
|
() => (localStorage.getItem(key) as ViewMode) || "list",
|
|
|
|
|
|
);
|
|
|
|
|
|
const set = useCallback(
|
|
|
|
|
|
(m: ViewMode) => {
|
|
|
|
|
|
localStorage.setItem(key, m);
|
|
|
|
|
|
setMode(m);
|
|
|
|
|
|
},
|
|
|
|
|
|
[key],
|
|
|
|
|
|
);
|
|
|
|
|
|
return [mode, set];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Segmented control switching between the list and image-tile layouts. */
|
|
|
|
|
|
export function ViewToggle({
|
|
|
|
|
|
mode,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
mode: ViewMode;
|
|
|
|
|
|
onChange: (m: ViewMode) => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="view-toggle">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={mode === "list" ? "active" : ""}
|
|
|
|
|
|
onClick={() => onChange("list")}
|
|
|
|
|
|
title="List view"
|
|
|
|
|
|
>
|
|
|
|
|
|
☰ List
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={mode === "tiles" ? "active" : ""}
|
|
|
|
|
|
onClick={() => onChange("tiles")}
|
|
|
|
|
|
title="Image tiles"
|
|
|
|
|
|
>
|
|
|
|
|
|
▦ Tiles
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* A single card rendered as a full image tile, with optional corner + footer.
|
|
|
|
|
|
* Clicking the image opens a larger click-to-preview modal.
|
|
|
|
|
|
*/
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
export function CardTile({
|
|
|
|
|
|
card,
|
|
|
|
|
|
corner,
|
|
|
|
|
|
footer,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
card: CardRef;
|
|
|
|
|
|
corner?: ReactNode;
|
|
|
|
|
|
footer?: ReactNode;
|
|
|
|
|
|
}) {
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
const { open } = useClickPreview();
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="tile">
|
|
|
|
|
|
<div className="tile-img-wrap">
|
|
|
|
|
|
{card.image_uri ? (
|
Card preview modal, library quantity steppers, and UI polish
- Fix keyboard-nav selection highlight (search results, deck list): the
Discord-styling rewrite dropped the --panel-2 CSS variable it relied
on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
larger centered modal (list and tile views), with a flip button for
double-faced cards' back face. Hover preview is unchanged and clears
when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
variant for tile corners) and use it consistently for owned-copy
counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
dims the row for a 5s grace period (still fully interactive) before
it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
truncation), add icons to Open/New Library and Download/Update card
data, and drop the trailing "…" from the library buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 20:55:18 -05:00
|
|
|
|
<img
|
|
|
|
|
|
className="tile-img"
|
|
|
|
|
|
src={card.image_uri}
|
|
|
|
|
|
alt={card.name}
|
|
|
|
|
|
loading="lazy"
|
|
|
|
|
|
onClick={() => open(card)}
|
|
|
|
|
|
/>
|
Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
tile/list view toggle, hover card previews, deck format + commander support,
per-card location field, grouping by type/subtype/CMC/color, localStorage
persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="tile-noimg">{card.name}</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{corner && <div className="tile-corner">{corner}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{footer && <div className="tile-footer">{footer}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Clickable WUBRG toggles that edit a bitmask value. */
|
|
|
|
|
|
export function ColorToggles({
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
value: number;
|
|
|
|
|
|
onChange: (mask: number) => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="color-toggles">
|
|
|
|
|
|
{COLORS.map((c) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={c.code}
|
|
|
|
|
|
title={c.label}
|
|
|
|
|
|
className={`pip ${c.code} ${value & c.bit ? "on" : ""}`}
|
|
|
|
|
|
onClick={() => onChange(value ^ c.bit)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{c.code}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|