2026-07-20 00:21:22 -05:00
|
|
|
import { open, save } from "@tauri-apps/plugin-dialog";
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from "react";
|
2026-10-26 22:35:41 +00:00
|
|
|
import {
|
|
|
|
|
api,
|
|
|
|
|
CardRef,
|
|
|
|
|
Deck,
|
|
|
|
|
DECK_STATUSES,
|
|
|
|
|
DeckEntry,
|
|
|
|
|
DeckHolding,
|
|
|
|
|
DeckImportResult,
|
|
|
|
|
DeckStatus,
|
|
|
|
|
isPhysical,
|
|
|
|
|
libraryKey,
|
|
|
|
|
PreviewContext,
|
|
|
|
|
} from "../api";
|
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 {
|
|
|
|
|
CardSearchBox,
|
|
|
|
|
CardThumb,
|
|
|
|
|
CardTile,
|
2026-07-08 23:54:47 -05:00
|
|
|
ColorDistributionBar,
|
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
|
|
|
ColorPips,
|
2026-10-26 20:35:21 -05:00
|
|
|
ColumnDef,
|
|
|
|
|
ColumnsMenu,
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
ConfirmDialog,
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
DialogFrame,
|
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
|
|
|
GroupBySelect,
|
|
|
|
|
GroupMode,
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
IconButton,
|
|
|
|
|
Icons,
|
2026-07-20 00:21:22 -05:00
|
|
|
InfoDialog,
|
2026-07-08 23:54:47 -05:00
|
|
|
ManaCurve,
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
PromptDialog,
|
2026-07-08 08:20:20 -05:00
|
|
|
QtyStepper,
|
2026-07-20 00:21:22 -05:00
|
|
|
SampleHandModal,
|
2026-07-08 23:54:47 -05:00
|
|
|
StatTile,
|
2026-10-16 21:14:43 -05:00
|
|
|
TileColumnsControl,
|
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
|
|
|
ViewToggle,
|
|
|
|
|
groupCards,
|
2026-07-08 23:54:47 -05:00
|
|
|
useClickPreview,
|
|
|
|
|
useHoverPreview,
|
2026-10-26 20:35:21 -05:00
|
|
|
useColumns,
|
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
|
|
|
usePersistedState,
|
2026-10-16 21:14:43 -05:00
|
|
|
useTileColumns,
|
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
|
|
|
useViewMode,
|
2026-10-23 23:05:19 -05:00
|
|
|
fold,
|
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
|
|
|
} from "../components";
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
import {
|
|
|
|
|
COMMANDER_FORMATS,
|
|
|
|
|
computeDeckLegality,
|
|
|
|
|
computeDeckStats,
|
|
|
|
|
formatCost,
|
|
|
|
|
FORMATS,
|
2026-10-26 21:44:16 +00:00
|
|
|
illegalSelection,
|
|
|
|
|
LEGALITY_CATEGORIES,
|
|
|
|
|
LegalityCategory,
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
pairsWith,
|
|
|
|
|
partnerPlaceholder,
|
|
|
|
|
shortBy,
|
|
|
|
|
} from "../deckAnalysis";
|
2026-07-21 21:04:36 -05:00
|
|
|
import { SuggestionsModal } from "../SuggestionsModal";
|
2026-10-14 21:14:50 -05:00
|
|
|
import { useBackButton } from "../backButton";
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
import { DeckList } from "./DeckList";
|
2026-10-23 22:56:59 -05:00
|
|
|
|
|
|
|
|
function libraryAddLabel(e: DeckEntry): string {
|
|
|
|
|
const n = shortBy(e);
|
|
|
|
|
return n === 1 ? "Add to library" : `Add ${n} copies to library`;
|
|
|
|
|
}
|
|
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
/** Tooltip/label + icon for the per-card mainboard/maybeboard toggle. */
|
2026-07-14 21:32:25 -05:00
|
|
|
function boardToggleProps(e: DeckEntry) {
|
|
|
|
|
return e.is_maybeboard
|
|
|
|
|
? { icon: Icons.arrowLeftCircle, label: "Move to deck" }
|
|
|
|
|
: { icon: Icons.helpCircle, label: "Move to maybeboard" };
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 20:35:21 -05:00
|
|
|
type DeckColumn = "image" | "colors" | "type" | "qty" | "owned" | "where" | "actions";
|
|
|
|
|
|
|
|
|
|
// The List view's optional columns; the checkbox and name always show.
|
|
|
|
|
const DECK_COLUMNS: ColumnDef<DeckColumn>[] = [
|
|
|
|
|
{ id: "image", label: "Image" },
|
|
|
|
|
{ id: "colors", label: "Colors" },
|
|
|
|
|
{ id: "type", label: "Type" },
|
|
|
|
|
{ id: "qty", label: "In deck" },
|
|
|
|
|
{ id: "owned", label: "Owned" },
|
|
|
|
|
{ id: "where", label: "Where" },
|
|
|
|
|
{ id: "actions", label: "Actions" },
|
|
|
|
|
];
|
|
|
|
|
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
type DeckDialog =
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
| { kind: "create" }
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
| { kind: "delete"; deck: Deck }
|
|
|
|
|
| { kind: "rename"; deck: Deck }
|
|
|
|
|
| { kind: "duplicate"; deck: Deck }
|
2026-07-20 00:21:22 -05:00
|
|
|
| { kind: "import-name"; suggestedName: string; path: string }
|
|
|
|
|
| { kind: "import-result"; result: DeckImportResult }
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
| null;
|
|
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
/** Name a new deck and, optionally, pick its format up front. */
|
|
|
|
|
function NewDeckDialog({
|
|
|
|
|
defaultFormat,
|
|
|
|
|
error,
|
|
|
|
|
onConfirm,
|
|
|
|
|
onCancel,
|
|
|
|
|
}: {
|
|
|
|
|
defaultFormat: string;
|
|
|
|
|
error: string | null;
|
|
|
|
|
onConfirm: (name: string, format: string) => void;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [name, setName] = useState("");
|
|
|
|
|
const [format, setFormat] = useState(FORMATS.includes(defaultFormat) ? defaultFormat : "");
|
|
|
|
|
const confirm = () => {
|
|
|
|
|
if (name.trim()) onConfirm(name.trim(), format);
|
2026-07-08 23:54:47 -05:00
|
|
|
};
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
return (
|
|
|
|
|
<DialogFrame title="New deck" onCancel={onCancel}>
|
|
|
|
|
<label className="dialog-field">
|
|
|
|
|
<span className="muted">Deck name</span>
|
|
|
|
|
<input
|
|
|
|
|
autoFocus
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => e.key === "Enter" && confirm()}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="dialog-field">
|
|
|
|
|
<span className="muted">Format</span>
|
|
|
|
|
<select value={format} onChange={(e) => setFormat(e.target.value)}>
|
|
|
|
|
{FORMATS.map((f) => (
|
|
|
|
|
<option key={f} value={f}>
|
|
|
|
|
{f || "—"}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
{error && <p className="dialog-error">{error}</p>}
|
|
|
|
|
<div className="dialog-actions">
|
|
|
|
|
<button onClick={onCancel}>Cancel</button>
|
|
|
|
|
<button className="primary" onClick={confirm} disabled={!name.trim()}>
|
|
|
|
|
Create
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogFrame>
|
|
|
|
|
);
|
2026-07-08 23:54:47 -05:00
|
|
|
}
|
|
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
function friendlyDeckError(err: unknown): string {
|
|
|
|
|
const msg = String(err);
|
|
|
|
|
return msg.includes("UNIQUE constraint failed") ? "A deck with that name already exists." : msg;
|
2026-10-23 23:18:27 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:54:47 -05:00
|
|
|
/** A prominent, art-forward display for a deck's commander (Commander/Brawl). */
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
function CommanderHero({
|
|
|
|
|
card,
|
|
|
|
|
onClear,
|
|
|
|
|
preview,
|
|
|
|
|
}: {
|
|
|
|
|
card: DeckEntry;
|
|
|
|
|
onClear: () => void;
|
|
|
|
|
preview?: PreviewContext;
|
|
|
|
|
}) {
|
2026-07-08 23:54:47 -05:00
|
|
|
const { handlers } = useHoverPreview();
|
|
|
|
|
const { open } = useClickPreview();
|
|
|
|
|
return (
|
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.
2026-07-22 23:44:21 -05:00
|
|
|
<div className="panel commander-hero">
|
2026-07-08 23:54:47 -05:00
|
|
|
{card.image_uri ? (
|
|
|
|
|
<img
|
|
|
|
|
className="commander-hero-img"
|
|
|
|
|
src={card.image_uri}
|
|
|
|
|
alt={card.name}
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
onClick={() => open(card, preview)}
|
2026-07-08 23:54:47 -05:00
|
|
|
{...handlers(card)}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="commander-hero-noimg">{card.name}</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="commander-hero-info">
|
|
|
|
|
<span className="commander-hero-label">Commander</span>
|
|
|
|
|
<span className="commander-hero-name">{card.name}</span>
|
|
|
|
|
<span className="commander-hero-type">{card.type_line}</span>
|
|
|
|
|
<ColorPips mask={card.color_identity} />
|
|
|
|
|
</div>
|
2026-10-23 22:29:56 -05:00
|
|
|
<IconButton icon={Icons.close} label={`Clear ${card.name} as commander`} onClick={onClear} />
|
2026-07-08 23:54:47 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 08:20:20 -05:00
|
|
|
// How long a card decremented to 0 copies lingers (dimmed, still adjustable)
|
|
|
|
|
// before actually dropping out of the deck — same grace-period pattern as
|
|
|
|
|
// the Library tab, so a mistaken removal can be undone with +.
|
|
|
|
|
const REMOVAL_GRACE_MS = 5000;
|
|
|
|
|
|
2026-10-26 21:44:16 +00:00
|
|
|
const extraCopiesLabel = (n: number) => `${n} extra ${n === 1 ? "copy" : "copies"}`;
|
|
|
|
|
|
2026-10-26 22:35:41 +00:00
|
|
|
const copies = (n: number) => `${n} ${n === 1 ? "copy" : "copies"}`;
|
|
|
|
|
|
|
|
|
|
const statusLabel = (s: DeckStatus) => DECK_STATUSES.find((d) => d.value === s)?.label ?? s;
|
|
|
|
|
|
|
|
|
|
/** One line under the status select saying what the status means. */
|
|
|
|
|
const STATUS_HINTS: Record<DeckStatus, string> = {
|
|
|
|
|
idea: "Just a list — it claims none of your cards.",
|
|
|
|
|
building: "Being assembled: pull its cards as you go.",
|
|
|
|
|
built: "Assembled: its cards are in the deck box.",
|
|
|
|
|
retired: "Taken apart, kept for reference.",
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
const STARTING_HAND_SIZE = 7;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deck editor. A deck's mainboard cards are split into two groups — "In your
|
|
|
|
|
* library" (owned) and "Not owned" — each with its own search box and each
|
|
|
|
|
* groupable by card attribute; "Combine sections" merges these two into one
|
|
|
|
|
* list. The maybeboard is always shown as its own separate section. Deck-level
|
|
|
|
|
* format + commander metadata sit at the top.
|
2026-07-21 21:04:36 -05:00
|
|
|
*
|
|
|
|
|
* `refreshSignal` is bumped by `App` when something outside this view's own
|
|
|
|
|
* control changes deck contents — e.g. undoing a Library bulk add-to-deck
|
|
|
|
|
* from the command palette while this view is already mounted — so the
|
|
|
|
|
* card list, mana curve, and legality tile recompute immediately instead of
|
|
|
|
|
* only on the next mount.
|
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
|
|
|
*/
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
function DeckBuilder({
|
|
|
|
|
deck,
|
2026-10-23 23:10:40 -05:00
|
|
|
refreshSignal,
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
onBack,
|
|
|
|
|
onDecksChanged,
|
|
|
|
|
onRename,
|
|
|
|
|
onDuplicate,
|
|
|
|
|
onDelete,
|
|
|
|
|
onExport,
|
2026-10-23 23:10:40 -05:00
|
|
|
}: {
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
deck: Deck;
|
2026-10-23 23:10:40 -05:00
|
|
|
refreshSignal?: number;
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
onBack: () => void;
|
|
|
|
|
/** Refetch the deck list after a change to the deck's own fields. */
|
|
|
|
|
onDecksChanged: () => Promise<void>;
|
|
|
|
|
onRename: () => void;
|
|
|
|
|
onDuplicate: () => void;
|
|
|
|
|
onDelete: () => void;
|
|
|
|
|
onExport: () => void;
|
2026-10-23 23:10:40 -05:00
|
|
|
}) {
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
const deckId = deck.id;
|
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 [contents, setContents] = useState<DeckEntry[]>([]);
|
|
|
|
|
const [view, setView] = useViewMode("deck-view");
|
2026-10-16 21:14:43 -05:00
|
|
|
const tiles = useTileColumns();
|
2026-10-26 20:35:21 -05:00
|
|
|
const columns = useColumns("deck-columns", DECK_COLUMNS);
|
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 [groupBy, setGroupBy] = usePersistedState<GroupMode>("deck-group", "none");
|
2026-07-20 00:21:22 -05:00
|
|
|
const [combineSections, setCombineSections] = usePersistedState("deck-combine-sections", false);
|
2026-07-21 21:04:36 -05:00
|
|
|
const [filterQuery, setFilterQuery] = useState("");
|
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 [libQuery, setLibQuery] = useState("");
|
|
|
|
|
const [libResults, setLibResults] = useState<CardRef[]>([]);
|
|
|
|
|
const [anyQuery, setAnyQuery] = useState("");
|
|
|
|
|
const [anyResults, setAnyResults] = useState<CardRef[]>([]);
|
|
|
|
|
const [cmdQuery, setCmdQuery] = useState("");
|
|
|
|
|
const [cmdResults, setCmdResults] = useState<CardRef[]>([]);
|
2026-10-23 22:29:56 -05:00
|
|
|
const [partnerQuery, setPartnerQuery] = useState("");
|
|
|
|
|
const [partnerResults, setPartnerResults] = useState<CardRef[]>([]);
|
2026-07-14 21:32:25 -05:00
|
|
|
const [maybeQuery, setMaybeQuery] = useState("");
|
|
|
|
|
const [maybeResults, setMaybeResults] = useState<CardRef[]>([]);
|
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
|
|
|
|
2026-07-08 08:20:20 -05:00
|
|
|
const [pendingRemoval, setPendingRemoval] = useState<Set<string>>(new Set());
|
|
|
|
|
const removalTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
|
|
|
const [selectedCards, setSelectedCards] = useState<Set<string>>(new Set());
|
2026-10-26 21:44:16 +00:00
|
|
|
// Selected cards where only some copies are marked for removal (the extras
|
|
|
|
|
// over a format's copy limit), keyed by oracle id → copies to remove.
|
|
|
|
|
const [partialRemoval, setPartialRemoval] = useState<Map<string, number>>(new Map());
|
2026-07-08 08:20:20 -05:00
|
|
|
const [bulkBusy, setBulkBusy] = useState(false);
|
|
|
|
|
const selectAllRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
const [handOpen, setHandOpen] = useState(false);
|
|
|
|
|
const [hand, setHand] = useState<DeckEntry[]>([]);
|
|
|
|
|
const [libraryStack, setLibraryStack] = useState<DeckEntry[]>([]);
|
2026-07-21 21:04:36 -05:00
|
|
|
const [suggestOpen, setSuggestOpen] = useState(false);
|
2026-10-26 22:35:41 +00:00
|
|
|
const [statusDialog, setStatusDialog] = useState<{ to: DeckStatus; kind: "assemble" | "release" } | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2026-07-20 00:21:22 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
// Keyed by deck id in `DeckView`, so switching decks remounts with fresh
|
|
|
|
|
// state rather than resetting it here.
|
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 loadContents = useCallback(async () => {
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
setContents(await api.deckContents(deckId));
|
|
|
|
|
}, [deckId]);
|
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
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadContents();
|
|
|
|
|
}, [loadContents]);
|
|
|
|
|
|
2026-07-21 21:04:36 -05:00
|
|
|
// Refetch on an external refresh signal without resetting the UI state
|
|
|
|
|
// above (unlike a deck switch, this shouldn't clear the user's filter,
|
|
|
|
|
// selection, or open sample hand). Skips the first render so it doesn't
|
|
|
|
|
// duplicate the mount-time fetch above.
|
|
|
|
|
const skipFirstRefresh = useRef(true);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (skipFirstRefresh.current) {
|
|
|
|
|
skipFirstRefresh.current = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loadContents();
|
|
|
|
|
}, [refreshSignal]);
|
|
|
|
|
|
2026-07-08 08:20:20 -05:00
|
|
|
// Cancel any in-flight removal timers on unmount so they don't fire against
|
|
|
|
|
// an unmounted component.
|
|
|
|
|
useEffect(() => () => removalTimers.current.forEach(clearTimeout), []);
|
|
|
|
|
|
|
|
|
|
// Keep selection in sync with what's actually visible — drop ids that fall
|
|
|
|
|
// out of the deck (deck switched, or a card's qty hit 0 and its grace
|
|
|
|
|
// period expired).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setSelectedCards((s) => {
|
|
|
|
|
const visible = new Set(contents.map((e) => e.oracle_id));
|
|
|
|
|
const next = new Set([...s].filter((id) => visible.has(id)));
|
|
|
|
|
return next.size === s.size ? s : next;
|
|
|
|
|
});
|
2026-10-26 21:44:16 +00:00
|
|
|
setPartialRemoval((m) => {
|
|
|
|
|
const visible = new Set(contents.map((e) => e.oracle_id));
|
|
|
|
|
const next = new Map([...m].filter(([id]) => visible.has(id)));
|
|
|
|
|
return next.size === m.size ? m : next;
|
|
|
|
|
});
|
2026-07-08 08:20:20 -05:00
|
|
|
}, [contents]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectAllRef.current) {
|
|
|
|
|
selectAllRef.current.indeterminate = selectedCards.size > 0 && selectedCards.size < contents.length;
|
|
|
|
|
}
|
|
|
|
|
}, [selectedCards, contents]);
|
|
|
|
|
|
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
|
|
|
useEffect(() => {
|
|
|
|
|
const q = libQuery.trim();
|
|
|
|
|
if (!q) return setLibResults([]);
|
|
|
|
|
const t = setTimeout(() => api.searchLibrary({ text: q }).then(setLibResults), 100);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [libQuery]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const q = anyQuery.trim();
|
|
|
|
|
if (!q) return setAnyResults([]);
|
|
|
|
|
const t = setTimeout(() => api.searchCards(q, 12).then(setAnyResults), 80);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [anyQuery]);
|
|
|
|
|
|
2026-07-14 21:32:25 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
const q = maybeQuery.trim();
|
|
|
|
|
if (!q) return setMaybeResults([]);
|
|
|
|
|
const t = setTimeout(() => api.searchCards(q, 12).then(setMaybeResults), 80);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [maybeQuery]);
|
|
|
|
|
|
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
|
|
|
useEffect(() => {
|
|
|
|
|
const q = cmdQuery.trim();
|
|
|
|
|
if (!q) return setCmdResults([]);
|
|
|
|
|
const t = setTimeout(() => api.searchCards(q, 12).then(setCmdResults), 80);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [cmdQuery]);
|
|
|
|
|
|
2026-10-23 22:29:56 -05:00
|
|
|
// The primary commander, whose partner ability the second search box serves.
|
|
|
|
|
const partnerOf =
|
|
|
|
|
deck?.commander_oracle_id ?? contents.find((e) => e.is_commander && !e.is_maybeboard)?.oracle_id ?? null;
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const q = partnerQuery.trim();
|
|
|
|
|
if (!q || !partnerOf) return setPartnerResults([]);
|
|
|
|
|
const t = setTimeout(() => api.searchPartners(partnerOf, q, 12).then(setPartnerResults), 80);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [partnerQuery, partnerOf]);
|
|
|
|
|
|
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
|
|
|
async function addToDeck(card: CardRef) {
|
|
|
|
|
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
|
|
|
|
const qty = existing ? existing.quantity + 1 : 1;
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckCard(deckId, card.oracle_id, qty, false, 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
|
|
|
setLibQuery("");
|
|
|
|
|
setLibResults([]);
|
|
|
|
|
setAnyQuery("");
|
|
|
|
|
setAnyResults([]);
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 21:32:25 -05:00
|
|
|
async function addToMaybeboard(card: CardRef) {
|
|
|
|
|
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
|
|
|
|
const qty = existing ? existing.quantity + 1 : 1;
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckCard(deckId, card.oracle_id, qty, false, true);
|
2026-07-14 21:32:25 -05:00
|
|
|
setMaybeQuery("");
|
|
|
|
|
setMaybeResults([]);
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Move a card between the mainboard and the maybeboard. Clears commander
|
2026-10-23 22:29:56 -05:00
|
|
|
* status (dropping it from the deck's commanders) when maybeboarding. */
|
2026-07-14 21:32:25 -05:00
|
|
|
async function setMaybeboard(entry: DeckEntry, isMaybeboard: boolean) {
|
|
|
|
|
const isCommander = isMaybeboard ? false : entry.is_commander;
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckCard(deckId, entry.oracle_id, entry.quantity, isCommander, isMaybeboard);
|
2026-10-23 22:29:56 -05:00
|
|
|
if (isMaybeboard && entry.is_commander) {
|
|
|
|
|
await api.setDeckCommanders(
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
deckId,
|
2026-10-23 22:29:56 -05:00
|
|
|
commanders.filter((e) => e.oracle_id !== entry.oracle_id).map((e) => e.oracle_id),
|
|
|
|
|
);
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await onDecksChanged();
|
2026-07-14 21:32:25 -05:00
|
|
|
}
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 00:21:22 -05:00
|
|
|
/** Every mainboard copy (commander/maybeboard excluded), one entry per
|
|
|
|
|
* physical copy so a card at quantity N is N times as likely to be drawn,
|
|
|
|
|
* shuffled in place (Fisher-Yates). */
|
|
|
|
|
function shuffledLibrary(): DeckEntry[] {
|
|
|
|
|
const pool: DeckEntry[] = [];
|
|
|
|
|
for (const e of contents) {
|
|
|
|
|
if (e.is_commander || e.is_maybeboard) continue;
|
|
|
|
|
for (let i = 0; i < e.quantity; i++) pool.push(e);
|
|
|
|
|
}
|
|
|
|
|
for (let i = pool.length - 1; i > 0; i--) {
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
|
|
|
}
|
|
|
|
|
return pool;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawNewHand() {
|
|
|
|
|
const shuffled = shuffledLibrary();
|
|
|
|
|
setHand(shuffled.slice(0, STARTING_HAND_SIZE));
|
|
|
|
|
setLibraryStack(shuffled.slice(STARTING_HAND_SIZE));
|
|
|
|
|
setHandOpen(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawOneMore() {
|
|
|
|
|
if (libraryStack.length === 0) return;
|
|
|
|
|
setHand((h) => [...h, libraryStack[0]]);
|
|
|
|
|
setLibraryStack((stack) => stack.slice(1));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
async function setQty(oracleId: string, qty: number) {
|
2026-07-08 08:20:20 -05:00
|
|
|
const clamped = Math.max(0, qty);
|
2026-07-14 21:32:25 -05:00
|
|
|
const existing = contents.find((e) => e.oracle_id === oracleId);
|
|
|
|
|
await api.setDeckCard(
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
deckId,
|
2026-07-14 21:32:25 -05:00
|
|
|
oracleId,
|
|
|
|
|
clamped,
|
|
|
|
|
existing?.is_commander ?? false,
|
|
|
|
|
existing?.is_maybeboard ?? false,
|
|
|
|
|
);
|
2026-07-08 08:20:20 -05:00
|
|
|
setContents((cs) => cs.map((e) => (e.oracle_id === oracleId ? { ...e, quantity: clamped } : e)));
|
|
|
|
|
|
|
|
|
|
// Re-adding during the grace period below cancels the pending removal.
|
|
|
|
|
const timer = removalTimers.current.get(oracleId);
|
|
|
|
|
if (timer) {
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
removalTimers.current.delete(oracleId);
|
|
|
|
|
setPendingRemoval((p) => {
|
|
|
|
|
const next = new Set(p);
|
|
|
|
|
next.delete(oracleId);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clamped <= 0) {
|
|
|
|
|
// deck_contents wouldn't include this card on a fresh fetch anymore —
|
|
|
|
|
// rather than dropping the row immediately, dim it and give the user a
|
|
|
|
|
// window to undo a mistaken decrement (via +) before it disappears.
|
|
|
|
|
setPendingRemoval((p) => new Set(p).add(oracleId));
|
|
|
|
|
removalTimers.current.set(
|
|
|
|
|
oracleId,
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setContents((cs) => cs.filter((e) => e.oracle_id !== oracleId));
|
|
|
|
|
setPendingRemoval((p) => {
|
|
|
|
|
const next = new Set(p);
|
|
|
|
|
next.delete(oracleId);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
removalTimers.current.delete(oracleId);
|
|
|
|
|
}, REMOVAL_GRACE_MS),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleCard(oracleId: string) {
|
2026-10-26 21:44:16 +00:00
|
|
|
// A manual (de)select always means the whole entry.
|
|
|
|
|
dropPartial(oracleId);
|
2026-07-08 08:20:20 -05:00
|
|
|
setSelectedCards((s) => {
|
|
|
|
|
const next = new Set(s);
|
|
|
|
|
if (next.has(oracleId)) next.delete(oracleId);
|
|
|
|
|
else next.add(oracleId);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 21:44:16 +00:00
|
|
|
function dropPartial(oracleId: string) {
|
|
|
|
|
setPartialRemoval((m) => {
|
|
|
|
|
if (!m.has(oracleId)) return m;
|
|
|
|
|
const next = new Map(m);
|
|
|
|
|
next.delete(oracleId);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearSelection() {
|
|
|
|
|
setSelectedCards(new Set());
|
|
|
|
|
setPartialRemoval(new Map());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Replace the selection with the cards behind the deck's card-level
|
|
|
|
|
* legality issues (optionally only some categories). Over-limit copies
|
|
|
|
|
* select just the extras, so removing them leaves the legal count. */
|
|
|
|
|
function selectIllegal(categories?: LegalityCategory[]) {
|
|
|
|
|
const sel = illegalSelection(deckLegality.issues, categories);
|
|
|
|
|
setSelectedCards(new Set(sel.keys()));
|
|
|
|
|
setPartialRemoval(
|
|
|
|
|
new Map([...sel].filter((kv): kv is [string, number] => kv[1] !== null)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 08:20:20 -05:00
|
|
|
function toggleSelectAllCards() {
|
2026-10-26 21:44:16 +00:00
|
|
|
setPartialRemoval(new Map());
|
2026-07-08 08:20:20 -05:00
|
|
|
setSelectedCards((s) =>
|
|
|
|
|
s.size === contents.length ? new Set() : new Set(contents.map((e) => e.oracle_id)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 22:29:56 -05:00
|
|
|
/** Remove every selected card outright. Unlike stepping one card down to 0,
|
|
|
|
|
* this is a deliberate action, so there's no grace period to linger in. */
|
2026-07-08 08:20:20 -05:00
|
|
|
async function bulkRemoveFromDeck() {
|
2026-10-26 21:44:16 +00:00
|
|
|
// Entries with only some copies selected are trimmed rather than removed.
|
|
|
|
|
const remaining = new Map<string, number>();
|
|
|
|
|
for (const id of selectedCards) {
|
|
|
|
|
const e = contents.find((c) => c.oracle_id === id);
|
|
|
|
|
const n = partialRemoval.get(id);
|
|
|
|
|
remaining.set(id, e && n !== undefined ? Math.max(e.quantity - n, 0) : 0);
|
|
|
|
|
}
|
2026-07-08 08:20:20 -05:00
|
|
|
setBulkBusy(true);
|
|
|
|
|
try {
|
2026-10-23 22:29:56 -05:00
|
|
|
await Promise.all(
|
2026-10-26 21:44:16 +00:00
|
|
|
[...remaining].map(([id, qty]) => {
|
2026-10-23 22:29:56 -05:00
|
|
|
const e = contents.find((c) => c.oracle_id === id);
|
2026-10-26 21:44:16 +00:00
|
|
|
return api.setDeckCard(deckId, id, qty, e?.is_commander ?? false, e?.is_maybeboard ?? false);
|
2026-10-23 22:29:56 -05:00
|
|
|
}),
|
|
|
|
|
);
|
2026-10-26 21:44:16 +00:00
|
|
|
const removed = new Set([...remaining].filter(([, qty]) => qty === 0).map(([id]) => id));
|
|
|
|
|
for (const id of removed) {
|
2026-10-23 22:29:56 -05:00
|
|
|
const timer = removalTimers.current.get(id);
|
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
|
removalTimers.current.delete(id);
|
|
|
|
|
}
|
2026-10-26 21:44:16 +00:00
|
|
|
setPendingRemoval((p) => new Set([...p].filter((id) => !removed.has(id))));
|
|
|
|
|
setContents((cs) =>
|
|
|
|
|
cs
|
|
|
|
|
.filter((e) => !removed.has(e.oracle_id))
|
|
|
|
|
.map((e) => (remaining.has(e.oracle_id) ? { ...e, quantity: remaining.get(e.oracle_id)! } : e)),
|
|
|
|
|
);
|
|
|
|
|
clearSelection();
|
2026-07-08 08:20:20 -05:00
|
|
|
} finally {
|
|
|
|
|
setBulkBusy(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
|
|
|
}
|
|
|
|
|
|
2026-10-23 22:56:59 -05:00
|
|
|
/** Record cards as owned: as many copies as the deck uses beyond those
|
|
|
|
|
* already owned, so an acquired card moves to "In your library". */
|
|
|
|
|
async function addToLibrary(entries: DeckEntry[]) {
|
|
|
|
|
await Promise.all(entries.filter((e) => shortBy(e) > 0).map((e) => api.addOwned(e.oracle_id, shortBy(e))));
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function bulkAddToLibrary() {
|
|
|
|
|
setBulkBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
await addToLibrary(contents.filter((e) => selectedCards.has(e.oracle_id)));
|
2026-10-26 21:44:16 +00:00
|
|
|
clearSelection();
|
2026-10-23 22:56:59 -05:00
|
|
|
} finally {
|
|
|
|
|
setBulkBusy(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
|
|
|
async function changeFormat(format: string) {
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckFormat(deckId, format);
|
|
|
|
|
await onDecksChanged();
|
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
|
|
|
}
|
|
|
|
|
|
2026-10-26 22:35:41 +00:00
|
|
|
async function applyStatus(status: DeckStatus, assembled = false) {
|
|
|
|
|
setStatusDialog(null);
|
|
|
|
|
await api.setDeckStatus(deckId, status, assembled);
|
|
|
|
|
await onDecksChanged();
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Change status, first asking how to treat the cards when the deck starts
|
|
|
|
|
* or stops physically holding them. */
|
|
|
|
|
function changeStatus(status: DeckStatus) {
|
|
|
|
|
if (status === deck.status) return;
|
|
|
|
|
if (isPhysical(status) && !isPhysical(deck.status) && contents.some((e) => !e.is_maybeboard)) {
|
|
|
|
|
setStatusDialog({ to: status, kind: "assemble" });
|
|
|
|
|
} else if (!isPhysical(status) && pulledCopies > 0) {
|
|
|
|
|
setStatusDialog({ to: status, kind: "release" });
|
|
|
|
|
} else {
|
|
|
|
|
applyStatus(status);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Mark as many copies pulled as the deck needs (the backend clamps to
|
|
|
|
|
* what's actually in the box). */
|
|
|
|
|
async function pullCard(e: DeckEntry) {
|
|
|
|
|
await api.setDeckCardPulled(deckId, e.oracle_id, e.quantity);
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function takeCard(e: DeckEntry, from: DeckHolding) {
|
|
|
|
|
await api.takeDeckCardCopy(from.deck_id, deckId, e.oracle_id);
|
|
|
|
|
await loadContents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function bulkSetPulled(pulled: boolean) {
|
|
|
|
|
setBulkBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
contents
|
|
|
|
|
.filter((e) => selectedCards.has(e.oracle_id) && !e.is_maybeboard)
|
|
|
|
|
.map((e) => api.setDeckCardPulled(deckId, e.oracle_id, pulled ? e.quantity : 0)),
|
|
|
|
|
);
|
|
|
|
|
clearSelection();
|
|
|
|
|
await loadContents();
|
|
|
|
|
} finally {
|
|
|
|
|
setBulkBusy(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
|
|
|
async function setCommander(card: CardRef) {
|
2026-07-14 21:32:25 -05:00
|
|
|
// Ensure the card is in the deck (mainboard, not maybeboard) and flagged
|
|
|
|
|
// commander — covers both "not in the deck yet" and "was maybeboarded".
|
|
|
|
|
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckCard(deckId, card.oracle_id, existing?.quantity ?? 1, true, false);
|
2026-10-23 22:29:56 -05:00
|
|
|
// Keep the partner only if it can still pair with the new commander; if
|
|
|
|
|
// not, it stays in the deck as an ordinary card.
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
const picked = (await api.deckContents(deckId)).find((e) => e.oracle_id === card.oracle_id);
|
2026-10-23 22:29:56 -05:00
|
|
|
const keepPartner =
|
|
|
|
|
partnerEntry && partnerEntry.oracle_id !== card.oracle_id && picked && pairsWith(picked, partnerEntry);
|
|
|
|
|
await api.setDeckCommanders(
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
deckId,
|
2026-10-23 22:29:56 -05:00
|
|
|
keepPartner ? [card.oracle_id, partnerEntry.oracle_id] : [card.oracle_id],
|
|
|
|
|
);
|
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
|
|
|
setCmdQuery("");
|
|
|
|
|
setCmdResults([]);
|
|
|
|
|
await loadContents();
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await onDecksChanged();
|
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
|
|
|
}
|
|
|
|
|
|
2026-10-23 22:29:56 -05:00
|
|
|
async function addPartner(card: CardRef) {
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
if (!commanderEntry) return;
|
2026-10-23 22:29:56 -05:00
|
|
|
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await api.setDeckCard(deckId, card.oracle_id, existing?.quantity ?? 1, true, false);
|
|
|
|
|
await api.setDeckCommanders(deckId, [commanderEntry.oracle_id, card.oracle_id]);
|
2026-10-23 22:29:56 -05:00
|
|
|
setPartnerQuery("");
|
|
|
|
|
setPartnerResults([]);
|
|
|
|
|
await loadContents();
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await onDecksChanged();
|
2026-10-23 22:29:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Stop treating a card as a commander; it stays in the deck. Clearing the
|
|
|
|
|
* primary commander promotes its partner. */
|
|
|
|
|
async function clearCommander(entry: DeckEntry) {
|
|
|
|
|
await api.setDeckCommanders(
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
deckId,
|
2026-10-23 22:29:56 -05:00
|
|
|
commanders.filter((e) => e.oracle_id !== entry.oracle_id).map((e) => e.oracle_id),
|
|
|
|
|
);
|
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
|
|
|
await loadContents();
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
await onDecksChanged();
|
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
|
|
|
}
|
|
|
|
|
|
2026-07-21 21:04:36 -05:00
|
|
|
// Quick-search over cards already in the deck/maybeboard — a display
|
|
|
|
|
// filter only, distinct from the CardSearchBox boxes below (which search
|
|
|
|
|
// the library/reference db to find NEW cards to add).
|
|
|
|
|
function matchesFilter(e: DeckEntry): boolean {
|
2026-10-23 23:05:19 -05:00
|
|
|
const q = fold(filterQuery.trim());
|
2026-07-21 21:04:36 -05:00
|
|
|
if (!q) return true;
|
2026-10-23 23:05:19 -05:00
|
|
|
return fold(e.name).includes(q) || fold(e.type_line).includes(q);
|
2026-07-21 21:04:36 -05:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 22:35:41 +00:00
|
|
|
// Building/built decks split by what's physically in them; ideas and
|
|
|
|
|
// retired decks by whether the cards are free to use.
|
|
|
|
|
const physical = isPhysical(deck.status);
|
|
|
|
|
const mainboard = contents.filter((e) => !e.is_maybeboard && matchesFilter(e));
|
|
|
|
|
const toPull = mainboard.filter((e) => e.quantity_pulled < e.quantity);
|
|
|
|
|
const inDeck = mainboard.filter((e) => e.quantity_pulled >= e.quantity);
|
|
|
|
|
const available = mainboard.filter((e) => e.quantity_owned > 0 && e.quantity_spare >= e.quantity);
|
|
|
|
|
const elsewhere = mainboard.filter((e) => e.quantity_owned > 0 && e.quantity_spare < e.quantity);
|
|
|
|
|
const unowned = mainboard.filter((e) => e.quantity_owned === 0);
|
|
|
|
|
const pulledCopies = contents.reduce((n, e) => n + (e.is_maybeboard ? 0 : e.quantity_pulled), 0);
|
|
|
|
|
const leftToPull = contents.filter((e) => !e.is_maybeboard && e.quantity_pulled < e.quantity).length;
|
2026-07-21 21:04:36 -05:00
|
|
|
const maybeboard = contents.filter((e) => e.is_maybeboard && matchesFilter(e));
|
2026-10-23 22:29:56 -05:00
|
|
|
// The primary commander first, then its partner, if any.
|
|
|
|
|
const isPrimary = (e: DeckEntry) => Number(e.oracle_id === deck?.commander_oracle_id);
|
|
|
|
|
const commanders = contents
|
|
|
|
|
.filter((e) => e.is_commander && !e.is_maybeboard)
|
|
|
|
|
.sort((a, b) => isPrimary(b) - isPrimary(a));
|
|
|
|
|
const commanderEntry: DeckEntry | undefined = commanders[0];
|
|
|
|
|
const partnerEntry: DeckEntry | undefined = commanders[1];
|
2026-07-08 23:54:47 -05:00
|
|
|
const deckStats = useMemo(() => computeDeckStats(contents), [contents]);
|
2026-07-20 00:21:22 -05:00
|
|
|
const deckLegality = useMemo(() => computeDeckLegality(deck, contents), [deck, contents]);
|
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
|
|
|
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
// Lets the click preview track copies and choose each slot's printing.
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
const preview: PreviewContext = {
|
|
|
|
|
deckId,
|
|
|
|
|
coverOracleId: deck.cover_oracle_id,
|
|
|
|
|
onChanged: () => {
|
|
|
|
|
loadContents();
|
|
|
|
|
onDecksChanged();
|
|
|
|
|
},
|
|
|
|
|
};
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
|
2026-10-26 22:35:41 +00:00
|
|
|
/** Where a card's copies are, when that's worth saying: for a deck being
|
|
|
|
|
* built, where to pull what's missing from; for an idea, which real decks
|
|
|
|
|
* already use the copies. Null when there's nothing to act on. */
|
|
|
|
|
function whereabouts(e: DeckEntry, compact = false) {
|
|
|
|
|
if (e.is_maybeboard) return null;
|
|
|
|
|
if (physical) {
|
|
|
|
|
if (e.quantity_pulled >= e.quantity) return null;
|
|
|
|
|
const progress = e.quantity_pulled > 0 && (
|
|
|
|
|
<span className="muted">
|
|
|
|
|
{e.quantity_pulled}/{e.quantity} pulled
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
const holders = e.claimed_by.filter((h) => h.pulled > 0);
|
|
|
|
|
let source;
|
|
|
|
|
if (e.quantity_in_box > 0) {
|
|
|
|
|
source = (
|
|
|
|
|
<>
|
|
|
|
|
<span className="pull-source">In {e.location || "your library"}</span>
|
|
|
|
|
<button className="pull-action" onClick={() => pullCard(e)}>
|
|
|
|
|
Pulled
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
} else if (holders.length > 0) {
|
|
|
|
|
source = holders.map((h) => (
|
|
|
|
|
<span key={h.deck_id} className="row" style={{ gap: 4 }}>
|
|
|
|
|
<span className="pull-source">In {h.name}</span>
|
|
|
|
|
<button className="pull-action" onClick={() => takeCard(e, h)} title={`Move a copy from ${h.name}`}>
|
|
|
|
|
Take
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
));
|
|
|
|
|
} else if (e.quantity_owned === 0) {
|
|
|
|
|
source = <span className="pull-source muted">Not owned</span>;
|
|
|
|
|
} else {
|
|
|
|
|
source = (
|
|
|
|
|
<span className="pull-source muted">Need {copies(e.quantity - e.quantity_pulled)} more</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<span className={`whereabouts ${compact ? "compact" : ""}`}>
|
|
|
|
|
{progress}
|
|
|
|
|
{source}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (e.quantity_owned === 0 || e.quantity_spare >= e.quantity) return null;
|
|
|
|
|
const names = e.claimed_by.map((h) => h.name).join(", ");
|
|
|
|
|
return (
|
|
|
|
|
<span className={`whereabouts ${compact ? "compact" : ""}`}>
|
|
|
|
|
<span className="pull-source" title={`Copies claimed by: ${names}`}>
|
|
|
|
|
In {names}
|
|
|
|
|
</span>
|
|
|
|
|
{e.quantity_spare > 0 && (
|
|
|
|
|
<span className="muted">
|
|
|
|
|
{e.quantity_spare} of {e.quantity} free
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</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
|
|
|
function deckTiles(entries: DeckEntry[]) {
|
|
|
|
|
return (
|
2026-10-16 21:14:43 -05:00
|
|
|
<div {...tiles.gridProps}>
|
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
|
|
|
{entries.map((e) => (
|
|
|
|
|
<CardTile
|
|
|
|
|
key={e.oracle_id}
|
|
|
|
|
card={e}
|
Printing tracking for owned copies and deck art, and an outdated-card-data notice
Owned copies can now be pinned to a printing + finish (nonfoil / foil /
etched), activating the long-dormant library_card_copies table as groups
of identical copies. library_cards.quantity_owned stays authoritative:
groups are a subset of it, and the rest are "unspecified". Tracking more
copies than are unspecified grows ownership to fit; lowering ownership
trims the most recently added groups first. Migration 0006 adds the
group quantity, a unique (card, printing, finish) index, and
deck_cards.preferred_scryfall_id.
A deck slot can show a chosen printing's art, both faces from that
printing, falling back to the default when the id no longer resolves.
It survives quantity/board changes and is kept by duplicate_deck.
UI: the click preview's info tile gets a Printings section when opened
from the Library or a deck (copy groups with steppers, "Add a
printing…", and "Shown in this deck" with Change…/Default), backed by a
printing picker grid. The picker takes Escape in the capture phase so
it closes without closing the preview under it.
Sync: CardEntry.printings (keyed "<scryfall_id>/<finish>") and
DeckCardEntry.printing. Cards now merge field by field with groups per
key, so a quantity edit and a printing added on another device both
survive; merged groups are clamped deterministically. A document is
written as version 2 only once it holds printing data, so older builds
and servers refuse it instead of dropping the data and syncing the loss.
Decklists: "(SET) NUM", optionally "*F*"/"*E*", round-trips through deck
and library export/import. Library export writes a line per group plus
one for unspecified copies.
reference.db printings gain lang and released_at for picker labels and
ordering. When the installed card data lacks any column this build's
reference schema has (found by diffing against the schema applied
in-memory, so no list to maintain), a notice under the top bar offers
Update card data (and Import card data… on phones); "Not now" is
remembered until something more goes missing.
2026-10-16 21:06:17 -05:00
|
|
|
preview={preview}
|
2026-07-08 08:20:20 -05:00
|
|
|
selected={selectedCards.has(e.oracle_id)}
|
|
|
|
|
cornerLeft={
|
|
|
|
|
<span className="tile-select">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedCards.has(e.oracle_id)}
|
|
|
|
|
onChange={() => toggleCard(e.oracle_id)}
|
|
|
|
|
/>
|
|
|
|
|
</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
|
|
|
corner={
|
2026-07-08 08:20:20 -05:00
|
|
|
<QtyStepper
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
overlay
|
2026-07-08 08:20:20 -05:00
|
|
|
value={e.quantity}
|
|
|
|
|
onDecrement={() => setQty(e.oracle_id, e.quantity - 1)}
|
|
|
|
|
onIncrement={() => setQty(e.oracle_id, e.quantity + 1)}
|
|
|
|
|
warn={e.quantity_owned > 0 && e.quantity > e.quantity_owned}
|
|
|
|
|
pending={pendingRemoval.has(e.oracle_id)}
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
/>
|
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
|
|
|
}
|
2026-07-20 00:21:22 -05:00
|
|
|
cornerBottomLeft={
|
2026-10-23 22:56:59 -05:00
|
|
|
<span className="row" style={{ gap: 4 }}>
|
|
|
|
|
<IconButton overlay {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
|
|
|
|
|
{e.quantity_owned === 0 && (
|
|
|
|
|
<IconButton
|
|
|
|
|
overlay
|
|
|
|
|
icon={Icons.libraryAdd}
|
|
|
|
|
label={libraryAddLabel(e)}
|
|
|
|
|
onClick={() => addToLibrary([e])}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
2026-07-14 21:32:25 -05:00
|
|
|
}
|
2026-10-26 21:44:16 +00:00
|
|
|
footer={
|
2026-10-26 22:35:41 +00:00
|
|
|
e.is_commander || partialRemoval.has(e.oracle_id) || whereabouts(e) ? (
|
|
|
|
|
<span className="row" style={{ gap: 4, flexWrap: "wrap" }}>
|
2026-10-26 21:44:16 +00:00
|
|
|
{e.is_commander && <span className="tile-badge">★ commander</span>}
|
|
|
|
|
{partialRemoval.has(e.oracle_id) && (
|
|
|
|
|
<span className="tile-badge">{extraCopiesLabel(partialRemoval.get(e.oracle_id)!)}</span>
|
|
|
|
|
)}
|
2026-10-26 22:35:41 +00:00
|
|
|
{whereabouts(e, true)}
|
2026-10-26 21:44:16 +00:00
|
|
|
</span>
|
|
|
|
|
) : 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
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deckTable(entries: DeckEntry[]) {
|
|
|
|
|
return (
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
2026-07-08 08:20:20 -05:00
|
|
|
<th></th>
|
2026-10-26 20:35:21 -05:00
|
|
|
{columns.shown("image") && <th></th>}
|
|
|
|
|
{columns.shown("colors") && <th>Colors</th>}
|
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
|
|
|
<th>Name</th>
|
2026-10-26 20:35:21 -05:00
|
|
|
{columns.shown("type") && <th>Type</th>}
|
|
|
|
|
{columns.shown("qty") && <th>In deck</th>}
|
|
|
|
|
{columns.shown("owned") && <th>Owned</th>}
|
|
|
|
|
{columns.shown("where") && <th>Where</th>}
|
|
|
|
|
{columns.shown("actions") && <th></th>}
|
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
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{entries.map((e) => (
|
|
|
|
|
<tr key={e.oracle_id}>
|
2026-08-02 16:10:25 -05:00
|
|
|
<td className="cell-check">
|
2026-07-08 08:20:20 -05:00
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedCards.has(e.oracle_id)}
|
|
|
|
|
onChange={() => toggleCard(e.oracle_id)}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
2026-10-26 20:35:21 -05:00
|
|
|
{columns.shown("image") && (
|
|
|
|
|
<td className="cell-thumb">
|
|
|
|
|
<CardThumb card={e} preview={preview} />
|
|
|
|
|
</td>
|
|
|
|
|
)}
|
|
|
|
|
{columns.shown("colors") && (
|
|
|
|
|
<td>
|
|
|
|
|
<ColorPips mask={e.color_identity} />
|
|
|
|
|
</td>
|
|
|
|
|
)}
|
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
|
|
|
<td className="card-name">
|
|
|
|
|
{e.is_commander && <span title="Commander">★ </span>}
|
|
|
|
|
{e.name}
|
2026-10-26 21:44:16 +00:00
|
|
|
{partialRemoval.has(e.oracle_id) && (
|
|
|
|
|
<span className="badge" style={{ marginLeft: 6 }}>
|
|
|
|
|
{extraCopiesLabel(partialRemoval.get(e.oracle_id)!)}
|
|
|
|
|
</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
|
|
|
</td>
|
2026-10-26 20:35:21 -05:00
|
|
|
{columns.shown("type") && <td className="card-type">{e.type_line}</td>}
|
|
|
|
|
{columns.shown("qty") && (
|
|
|
|
|
<td data-label="In deck">
|
|
|
|
|
<QtyStepper
|
|
|
|
|
value={e.quantity}
|
|
|
|
|
onDecrement={() => setQty(e.oracle_id, e.quantity - 1)}
|
|
|
|
|
onIncrement={() => setQty(e.oracle_id, e.quantity + 1)}
|
|
|
|
|
warn={e.quantity_owned > 0 && e.quantity > e.quantity_owned}
|
|
|
|
|
pending={pendingRemoval.has(e.oracle_id)}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
)}
|
|
|
|
|
{columns.shown("owned") && <td data-label="Owned">{e.quantity_owned}</td>}
|
|
|
|
|
{columns.shown("where") && <td data-label="Where">{whereabouts(e)}</td>}
|
|
|
|
|
{columns.shown("actions") && (
|
|
|
|
|
<td>
|
|
|
|
|
<span className="row" style={{ gap: 4 }}>
|
|
|
|
|
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
|
|
|
|
|
{e.quantity_owned === 0 && (
|
|
|
|
|
<IconButton icon={Icons.libraryAdd} label={libraryAddLabel(e)} onClick={() => addToLibrary([e])} />
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
)}
|
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
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cardsGroup(entries: DeckEntry[]) {
|
|
|
|
|
if (entries.length === 0) return <p className="hint">None yet.</p>;
|
|
|
|
|
return groupCards(entries, groupBy).map((g) => (
|
|
|
|
|
<div key={g.label || "all"} style={{ marginBottom: g.label ? 14 : 0 }}>
|
|
|
|
|
{g.label && (
|
2026-07-20 00:21:22 -05:00
|
|
|
<h4 className="group-header">
|
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
|
|
|
{g.label} <span className="badge">{g.cards.length}</span>
|
|
|
|
|
</h4>
|
|
|
|
|
)}
|
|
|
|
|
{view === "tiles" ? deckTiles(g.cards) : deckTable(g.cards)}
|
|
|
|
|
</div>
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="deck-builder">
|
|
|
|
|
<button className="deck-builder-back" onClick={onBack}>
|
|
|
|
|
{Icons.arrowLeft}
|
|
|
|
|
All decks
|
|
|
|
|
</button>
|
|
|
|
|
<div className="row" style={{ justifyContent: "space-between" }}>
|
|
|
|
|
<div className="row deck-builder-title">
|
|
|
|
|
<h2>{deck.name}</h2>
|
|
|
|
|
<IconButton icon={Icons.pencil} label="Rename deck" onClick={onRename} />
|
|
|
|
|
<IconButton icon={Icons.copy} label="Duplicate deck" onClick={onDuplicate} />
|
|
|
|
|
<IconButton icon={Icons.trash} label="Delete deck" danger onClick={onDelete} />
|
2026-07-20 00:21:22 -05:00
|
|
|
</div>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="row" style={{ gap: 8 }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setSuggestOpen(true)}
|
|
|
|
|
disabled={contents.filter((e) => !e.is_maybeboard).length === 0}
|
|
|
|
|
>
|
|
|
|
|
{Icons.cards}
|
|
|
|
|
Suggest cards
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={onExport}>
|
|
|
|
|
{Icons.download}
|
|
|
|
|
Export deck
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={drawNewHand}
|
|
|
|
|
disabled={!contents.some((e) => !e.is_commander && !e.is_maybeboard)}
|
|
|
|
|
>
|
|
|
|
|
{Icons.cards}
|
|
|
|
|
Sample hand
|
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
|
|
|
</button>
|
|
|
|
|
</div>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="deck-top-grid">
|
|
|
|
|
{COMMANDER_FORMATS.includes(deck.format) && commanderEntry && (
|
|
|
|
|
<div className="deck-top-commander">
|
|
|
|
|
{commanders.map((c) => (
|
|
|
|
|
<CommanderHero key={c.oracle_id} card={c} onClear={() => clearCommander(c)} preview={preview} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Deck metadata: format + (for Commander/Brawl) the commander card. */}
|
|
|
|
|
<div className="deck-top-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)}>
|
|
|
|
|
{FORMATS.map((f) => (
|
|
|
|
|
<option key={f} value={f}>
|
|
|
|
|
{f || "—"}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
2026-10-26 22:35:41 +00:00
|
|
|
<label className="row" style={{ gap: 6 }}>
|
|
|
|
|
<span className="muted">Status</span>
|
|
|
|
|
<select value={deck.status} onChange={(e) => changeStatus(e.target.value as DeckStatus)}>
|
|
|
|
|
{DECK_STATUSES.map((s) => (
|
|
|
|
|
<option key={s.value} value={s.value}>
|
|
|
|
|
{s.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<span className="muted deck-status-hint">
|
|
|
|
|
{deck.status === "building" && leftToPull > 0
|
|
|
|
|
? `Being assembled: ${leftToPull} card${leftToPull === 1 ? "" : "s"} left to pull.`
|
|
|
|
|
: deck.status === "built" && leftToPull > 0
|
|
|
|
|
? `Assembled, with ${leftToPull} card${leftToPull === 1 ? "" : "s"} still to pull.`
|
|
|
|
|
: STATUS_HINTS[deck.status]}
|
|
|
|
|
</span>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
|
|
|
|
|
{COMMANDER_FORMATS.includes(deck.format) && (
|
|
|
|
|
<div className="commander-box">
|
|
|
|
|
{!commanderEntry && <span className="muted">No commander set yet.</span>}
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder={
|
|
|
|
|
commanderEntry
|
|
|
|
|
? "Search to change the commander…"
|
|
|
|
|
: "Search to set the commander…"
|
|
|
|
|
}
|
|
|
|
|
value={cmdQuery}
|
|
|
|
|
onChange={setCmdQuery}
|
|
|
|
|
results={cmdResults}
|
|
|
|
|
onPick={setCommander}
|
|
|
|
|
/>
|
|
|
|
|
{commanderEntry?.partner && !partnerEntry && (
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder={partnerPlaceholder(commanderEntry.partner)}
|
|
|
|
|
value={partnerQuery}
|
|
|
|
|
onChange={setPartnerQuery}
|
|
|
|
|
results={partnerResults}
|
|
|
|
|
onPick={addPartner}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-07-08 23:54:47 -05:00
|
|
|
</div>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{contents.length > 0 && (
|
|
|
|
|
<div className="deck-top-stats">
|
|
|
|
|
<div className="panel deck-stats">
|
|
|
|
|
<div className="deck-stats-head section-label">Deck analysis</div>
|
|
|
|
|
<div className="stat-tiles-row">
|
|
|
|
|
<StatTile value={deckStats.totalCards} label="cards" />
|
|
|
|
|
<StatTile value={deckStats.uniqueCards} label="unique" />
|
|
|
|
|
<StatTile value={deckStats.landCount} label="lands" />
|
|
|
|
|
<StatTile value={deckStats.avgManaValue.toFixed(2)} label="avg MV" />
|
|
|
|
|
<StatTile value={formatCost(deckStats.totalCost)} label="cost" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="deck-stats-body">
|
|
|
|
|
<div className="deck-stats-numbers">
|
|
|
|
|
<div className="deck-stats-curve-label">Color identity</div>
|
|
|
|
|
<ColorDistributionBar
|
|
|
|
|
colorCounts={deckStats.colorCounts}
|
|
|
|
|
colorlessCount={deckStats.colorlessCount}
|
2026-07-08 23:54:47 -05:00
|
|
|
/>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="deck-stats-curve-label">Mana production</div>
|
|
|
|
|
<ColorDistributionBar
|
|
|
|
|
colorCounts={deckStats.manaProduction}
|
|
|
|
|
colorlessCount={deckStats.colorlessProduction}
|
2026-07-08 23:54:47 -05:00
|
|
|
/>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="deck-stats-curve-label">Mana consumption</div>
|
|
|
|
|
<ColorDistributionBar
|
|
|
|
|
colorCounts={deckStats.manaConsumption}
|
|
|
|
|
colorlessCount={deckStats.colorlessConsumption}
|
2026-07-08 23:54:47 -05:00
|
|
|
/>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</div>
|
|
|
|
|
<div className="deck-stats-curve">
|
|
|
|
|
<div className="deck-stats-curve-label">Mana curve (nonland)</div>
|
|
|
|
|
<ManaCurve buckets={deckStats.curve} />
|
|
|
|
|
</div>
|
2026-07-08 23:54:47 -05:00
|
|
|
</div>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</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
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
{deck.format !== "Other" && (
|
|
|
|
|
<div className="deck-top-legality">
|
|
|
|
|
<div className={`legality-tile ${deckLegality.status}`}>
|
|
|
|
|
<div className="legality-tile-head">
|
|
|
|
|
<span className="legality-tile-icon">
|
|
|
|
|
{deckLegality.status === "legal" ? "✓" : deckLegality.status === "illegal" ? "✗" : "—"}
|
2026-08-02 16:10:25 -05:00
|
|
|
</span>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="legality-tile-info">
|
|
|
|
|
<span className="legality-tile-status">
|
|
|
|
|
{deckLegality.status === "legal"
|
|
|
|
|
? "Legal"
|
|
|
|
|
: deckLegality.status === "illegal"
|
|
|
|
|
? "Illegal"
|
|
|
|
|
: "Not checked"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="legality-tile-detail">
|
|
|
|
|
{deckLegality.status === "legal"
|
|
|
|
|
? `Meets all ${deck.format} deck-building rules.`
|
|
|
|
|
: deckLegality.status === "illegal"
|
2026-10-26 21:44:16 +00:00
|
|
|
? `${deckLegality.issues.length} issue${deckLegality.issues.length === 1 ? "" : "s"} found:`
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
: deck.format
|
|
|
|
|
? `Legality isn't checked for "${deck.format}".`
|
|
|
|
|
: "Set a format to check legality."}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-10-26 21:44:16 +00:00
|
|
|
{illegalSelection(deckLegality.issues).size > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
<span className="spacer" />
|
|
|
|
|
<button className="legality-tile-select" onClick={() => selectIllegal()}>
|
|
|
|
|
Select illegal cards
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-07-08 08:20:20 -05:00
|
|
|
</div>
|
2026-10-26 21:44:16 +00:00
|
|
|
{deckLegality.status === "illegal" &&
|
|
|
|
|
LEGALITY_CATEGORIES.map((cat) => {
|
|
|
|
|
const issues = deckLegality.issues.filter((i) => i.category === cat.key);
|
|
|
|
|
if (issues.length === 0) return null;
|
|
|
|
|
const cardCount = illegalSelection(issues).size;
|
|
|
|
|
return (
|
|
|
|
|
<div key={cat.key} className="legality-tile-group">
|
|
|
|
|
<div className="legality-tile-group-head">
|
|
|
|
|
<span>{cat.label}</span>
|
|
|
|
|
{cat.selectable && cardCount > 0 && (
|
|
|
|
|
<button className="legality-tile-select" onClick={() => selectIllegal([cat.key])}>
|
|
|
|
|
Select ({cardCount})
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<ul className="legality-tile-reasons">
|
|
|
|
|
{issues.map((i, n) => (
|
|
|
|
|
<li key={n}>{i.message}</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-07-20 00:21:22 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<hr className="deck-section-divider" />
|
2026-07-08 23:54:47 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="row" style={{ gap: 16 }}>
|
|
|
|
|
<label className="row" style={{ gap: 6, cursor: "pointer" }} title="Select all cards">
|
|
|
|
|
<input
|
|
|
|
|
ref={selectAllRef}
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={contents.length > 0 && selectedCards.size === contents.length}
|
|
|
|
|
onChange={toggleSelectAllCards}
|
|
|
|
|
disabled={contents.length === 0}
|
|
|
|
|
/>
|
|
|
|
|
<span className="muted" style={{ fontSize: 12 }}>
|
|
|
|
|
Select all
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="row" style={{ gap: 6, cursor: "pointer" }} title="Combine owned/unowned into one section">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={combineSections}
|
|
|
|
|
onChange={(e) => setCombineSections(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="muted" style={{ fontSize: 12 }}>
|
|
|
|
|
Combine sections
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
value={filterQuery}
|
|
|
|
|
onChange={(e) => setFilterQuery(e.target.value)}
|
|
|
|
|
placeholder="Filter cards in deck…"
|
|
|
|
|
style={{ width: 200 }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="spacer" />
|
|
|
|
|
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
|
|
|
|
<ViewToggle mode={view} onChange={setView} />
|
2026-10-26 20:35:21 -05:00
|
|
|
{view === "list" && <ColumnsMenu columns={columns} />}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
|
|
|
|
|
</div>
|
2026-07-21 21:04:36 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
{selectedCards.size > 0 && (
|
|
|
|
|
<div className="bulk-bar">
|
2026-10-26 21:44:16 +00:00
|
|
|
<span className="bulk-bar-count">
|
|
|
|
|
{selectedCards.size} selected
|
|
|
|
|
{partialRemoval.size > 0 && ` (${partialRemoval.size} only extra copies)`}
|
|
|
|
|
</span>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<div className="bulk-bar-actions">
|
|
|
|
|
<button className="danger" onClick={bulkRemoveFromDeck} disabled={bulkBusy}>
|
|
|
|
|
Remove from deck
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={bulkAddToLibrary}
|
|
|
|
|
disabled={bulkBusy || !contents.some((e) => selectedCards.has(e.oracle_id) && shortBy(e) > 0)}
|
|
|
|
|
title="Add the copies these cards are missing to your library"
|
|
|
|
|
>
|
|
|
|
|
Add to library
|
|
|
|
|
</button>
|
2026-10-26 22:35:41 +00:00
|
|
|
{physical && (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => bulkSetPulled(true)}
|
|
|
|
|
disabled={
|
|
|
|
|
bulkBusy ||
|
|
|
|
|
!contents.some(
|
|
|
|
|
(e) =>
|
|
|
|
|
selectedCards.has(e.oracle_id) &&
|
|
|
|
|
!e.is_maybeboard &&
|
|
|
|
|
e.quantity_pulled < e.quantity &&
|
|
|
|
|
e.quantity_in_box > 0,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
title="Record that these cards' copies have been taken from their boxes"
|
|
|
|
|
>
|
|
|
|
|
Mark pulled
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => bulkSetPulled(false)}
|
|
|
|
|
disabled={
|
|
|
|
|
bulkBusy || !contents.some((e) => selectedCards.has(e.oracle_id) && e.quantity_pulled > 0)
|
|
|
|
|
}
|
|
|
|
|
title="Record that these cards' copies are back in their boxes"
|
|
|
|
|
>
|
|
|
|
|
Mark not pulled
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</div>
|
|
|
|
|
<span className="spacer" />
|
2026-10-26 21:44:16 +00:00
|
|
|
<button onClick={clearSelection} disabled={bulkBusy}>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
Clear
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-07-20 00:21:22 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
{combineSections ? (
|
|
|
|
|
<section style={{ marginTop: 10 }}>
|
|
|
|
|
<h3>
|
2026-10-26 22:35:41 +00:00
|
|
|
All cards <span className="badge">{mainboard.length}</span>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</h3>
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder="Search all cards to add to your deck…"
|
|
|
|
|
value={anyQuery}
|
|
|
|
|
onChange={setAnyQuery}
|
|
|
|
|
results={anyResults}
|
|
|
|
|
onPick={addToDeck}
|
|
|
|
|
/>
|
2026-10-26 22:35:41 +00:00
|
|
|
{cardsGroup(physical ? [...toPull, ...inDeck] : [...available, ...elsewhere, ...unowned])}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</section>
|
2026-10-26 22:35:41 +00:00
|
|
|
) : physical ? (
|
|
|
|
|
<>
|
|
|
|
|
<section style={{ marginTop: 10 }}>
|
|
|
|
|
<h3>
|
|
|
|
|
To pull <span className="badge">{toPull.length}</span>
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="hint" style={{ marginTop: -4 }}>
|
|
|
|
|
In the deck's list, but not physically in it yet. New cards land here.
|
|
|
|
|
</p>
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder="Search all cards to add to your deck…"
|
|
|
|
|
value={anyQuery}
|
|
|
|
|
onChange={setAnyQuery}
|
|
|
|
|
results={anyResults}
|
|
|
|
|
onPick={addToDeck}
|
|
|
|
|
/>
|
|
|
|
|
{toPull.length === 0 && deck.status === "building" && mainboard.length > 0 ? (
|
|
|
|
|
<p className="hint row" style={{ gap: 8 }}>
|
|
|
|
|
Everything's pulled.
|
|
|
|
|
<button className="primary" onClick={() => applyStatus("built")}>
|
|
|
|
|
Mark as Built
|
|
|
|
|
</button>
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
cardsGroup(toPull)
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section style={{ marginTop: 26 }}>
|
|
|
|
|
<h3>
|
|
|
|
|
In deck <span className="badge">{inDeck.length}</span>
|
|
|
|
|
</h3>
|
|
|
|
|
{cardsGroup(inDeck)}
|
|
|
|
|
</section>
|
|
|
|
|
</>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<section style={{ marginTop: 10 }}>
|
|
|
|
|
<h3>
|
2026-10-26 22:35:41 +00:00
|
|
|
Available <span className="badge">{available.length}</span>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</h3>
|
2026-10-26 22:35:41 +00:00
|
|
|
<p className="hint" style={{ marginTop: -4 }}>
|
|
|
|
|
Owned, with copies no built deck is using.
|
|
|
|
|
</p>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder="Search your library to add owned cards…"
|
|
|
|
|
value={libQuery}
|
|
|
|
|
onChange={setLibQuery}
|
|
|
|
|
results={libResults}
|
|
|
|
|
onPick={addToDeck}
|
|
|
|
|
/>
|
2026-10-26 22:35:41 +00:00
|
|
|
{cardsGroup(available)}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</section>
|
|
|
|
|
|
2026-10-26 22:35:41 +00:00
|
|
|
{elsewhere.length > 0 && (
|
|
|
|
|
<section style={{ marginTop: 26 }}>
|
|
|
|
|
<h3>
|
|
|
|
|
In other decks <span className="badge">{elsewhere.length}</span>
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="hint" style={{ marginTop: -4 }}>
|
|
|
|
|
Owned, but your copies are used by decks you're building or have built.
|
|
|
|
|
</p>
|
|
|
|
|
{cardsGroup(elsewhere)}
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<section style={{ marginTop: 26 }}>
|
|
|
|
|
<h3>
|
|
|
|
|
Not owned <span className="badge">{unowned.length}</span>
|
|
|
|
|
</h3>
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder="Search all cards to add ones you don't own…"
|
|
|
|
|
value={anyQuery}
|
|
|
|
|
onChange={setAnyQuery}
|
|
|
|
|
results={anyResults}
|
|
|
|
|
onPick={addToDeck}
|
|
|
|
|
/>
|
|
|
|
|
{cardsGroup(unowned)}
|
|
|
|
|
</section>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-07-20 00:21:22 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
<section style={{ marginTop: 26 }}>
|
|
|
|
|
<h3>
|
|
|
|
|
Maybeboard <span className="badge">{maybeboard.length}</span>
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="hint" style={{ marginTop: -4 }}>
|
|
|
|
|
Cards under consideration — not counted in deck analysis.
|
|
|
|
|
</p>
|
|
|
|
|
<CardSearchBox
|
|
|
|
|
placeholder="Search all cards to add one to the maybeboard…"
|
|
|
|
|
value={maybeQuery}
|
|
|
|
|
onChange={setMaybeQuery}
|
|
|
|
|
results={maybeResults}
|
|
|
|
|
onPick={addToMaybeboard}
|
|
|
|
|
/>
|
|
|
|
|
{cardsGroup(maybeboard)}
|
|
|
|
|
</section>
|
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
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
{handOpen && (
|
|
|
|
|
<SampleHandModal
|
|
|
|
|
hand={hand}
|
|
|
|
|
libraryCount={libraryStack.length}
|
|
|
|
|
onDrawOne={drawOneMore}
|
|
|
|
|
onNewHand={drawNewHand}
|
|
|
|
|
onClose={() => setHandOpen(false)}
|
|
|
|
|
/>
|
2026-10-26 22:35:41 +00:00
|
|
|
)}
|
|
|
|
|
{statusDialog?.kind === "assemble" && (
|
|
|
|
|
<DialogFrame title={`Mark as ${statusLabel(statusDialog.to)}`} onCancel={() => setStatusDialog(null)}>
|
|
|
|
|
<p className="dialog-message">
|
|
|
|
|
Are this deck's cards already together? If so, the copies in your library are recorded as in the
|
|
|
|
|
deck. Otherwise, they're listed under "To pull" until you take them out of their boxes.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="dialog-actions">
|
|
|
|
|
<button onClick={() => setStatusDialog(null)}>Cancel</button>
|
|
|
|
|
<button onClick={() => applyStatus(statusDialog.to, false)}>Start pulling</button>
|
|
|
|
|
<button className="primary" onClick={() => applyStatus(statusDialog.to, true)} autoFocus>
|
|
|
|
|
Already assembled
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogFrame>
|
|
|
|
|
)}
|
|
|
|
|
{statusDialog?.kind === "release" && (
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
title={`Mark as ${statusLabel(statusDialog.to)}`}
|
|
|
|
|
message={`${copies(pulledCopies)} in this deck will be recorded as back in ${
|
|
|
|
|
pulledCopies === 1 ? "its box" : "their boxes"
|
|
|
|
|
}.`}
|
|
|
|
|
confirmLabel={`Mark as ${statusLabel(statusDialog.to)}`}
|
|
|
|
|
onConfirm={() => applyStatus(statusDialog.to)}
|
|
|
|
|
onCancel={() => setStatusDialog(null)}
|
|
|
|
|
/>
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
)}
|
|
|
|
|
{suggestOpen && (
|
|
|
|
|
<SuggestionsModal
|
|
|
|
|
deckId={deck.id}
|
|
|
|
|
onClose={() => setSuggestOpen(false)}
|
|
|
|
|
onCardAdded={loadContents}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</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
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
// Which screen the Decks tab was last on. Module-level rather than component
|
|
|
|
|
// state so hopping to another tab and back returns to the deck being built,
|
|
|
|
|
// while a fresh launch starts on the deck list.
|
|
|
|
|
let lastScreen: "list" | "builder" = "list";
|
|
|
|
|
// Where the deck list was scrolled to when a deck was opened, so coming back
|
|
|
|
|
// lands where you left off rather than at the top.
|
|
|
|
|
let listScrollTop = 0;
|
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
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
/** The app's scrolling page area (the window itself doesn't scroll). */
|
|
|
|
|
const scroller = () => document.querySelector<HTMLElement>(".content");
|
2026-07-14 21:32:25 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
/**
|
|
|
|
|
* The Decks tab: the deck list, or one deck's builder, as separate screens.
|
|
|
|
|
* Owns the deck list itself and every whole-deck action (create, import,
|
|
|
|
|
* rename, duplicate, export, delete), which both screens offer.
|
|
|
|
|
*/
|
|
|
|
|
export function DeckView({
|
|
|
|
|
refreshSignal,
|
|
|
|
|
openDeckId,
|
|
|
|
|
onOpenedDeck,
|
|
|
|
|
}: {
|
|
|
|
|
refreshSignal?: number;
|
|
|
|
|
/** A deck to open, e.g. from a deck chip elsewhere; `onOpenedDeck` clears it. */
|
|
|
|
|
openDeckId?: number | null;
|
|
|
|
|
onOpenedDeck?: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [decks, setDecks] = useState<Deck[] | null>(null);
|
|
|
|
|
const [selected, setSelected] = usePersistedState<number | null>(libraryKey("deck-selected"), null);
|
|
|
|
|
// When each deck was last opened, for the list's "Recently opened" sort.
|
|
|
|
|
const [openedAt, setOpenedAt] = usePersistedState<Record<number, number>>(libraryKey("deck-opened"), {});
|
|
|
|
|
// The format the last new deck was created with; null until one has been.
|
|
|
|
|
const [lastNewFormat, setLastNewFormat] = usePersistedState<string | null>("new-deck-format", null);
|
|
|
|
|
const [screen, setScreenState] = useState(lastScreen);
|
|
|
|
|
const [dialog, setDialog] = useState<DeckDialog>(null);
|
|
|
|
|
const [dialogError, setDialogError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const setScreen = useCallback((next: "list" | "builder") => {
|
|
|
|
|
if (lastScreen === "list" && next === "builder") listScrollTop = scroller()?.scrollTop ?? 0;
|
|
|
|
|
lastScreen = next;
|
|
|
|
|
setScreenState(next);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// A fresh screen starts at the top; the list at wherever it was left.
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
scroller()?.scrollTo(0, screen === "list" ? listScrollTop : 0);
|
|
|
|
|
}, [screen, selected]);
|
|
|
|
|
|
|
|
|
|
const loadDecks = useCallback(async () => {
|
|
|
|
|
setDecks(await api.listDecks());
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadDecks();
|
|
|
|
|
}, [loadDecks]);
|
|
|
|
|
|
|
|
|
|
function openDeck(id: number) {
|
|
|
|
|
setSelected(id);
|
|
|
|
|
setOpenedAt({ ...openedAt, [id]: Date.now() });
|
|
|
|
|
setScreen("builder");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open a deck asked for from outside — straight to its building screen.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (openDeckId == null) return;
|
|
|
|
|
openDeck(openDeckId);
|
|
|
|
|
onOpenedDeck?.();
|
|
|
|
|
}, [openDeckId]);
|
|
|
|
|
|
|
|
|
|
// Android back from the builder returns to the deck list, like its ← button.
|
|
|
|
|
useBackButton(() => setScreen("list"), screen === "builder");
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
const deck = decks?.find((d) => d.id === selected) ?? null;
|
|
|
|
|
|
|
|
|
|
// A new deck starts in the format the last one was created with — or,
|
|
|
|
|
// before any has been, the format of the deck opened most recently.
|
|
|
|
|
const newDeckFormat =
|
|
|
|
|
lastNewFormat ??
|
|
|
|
|
(decks ?? []).reduce<Deck | null>(
|
|
|
|
|
(latest, d) => ((openedAt[d.id] ?? 0) > (latest ? openedAt[latest.id] ?? 0 : 0) ? d : latest),
|
|
|
|
|
null,
|
|
|
|
|
)?.format ??
|
|
|
|
|
"";
|
|
|
|
|
|
|
|
|
|
function showDialog(next: DeckDialog) {
|
|
|
|
|
setDialogError(null);
|
|
|
|
|
setDialog(next);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmCreate(name: string, format: string) {
|
|
|
|
|
try {
|
|
|
|
|
const d = await api.createDeck(name, null);
|
|
|
|
|
if (format) await api.setDeckFormat(d.id, format);
|
|
|
|
|
setLastNewFormat(format);
|
|
|
|
|
setDialog(null);
|
|
|
|
|
await loadDecks();
|
|
|
|
|
openDeck(d.id);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setDialogError(friendlyDeckError(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function exportDeck(d: Deck) {
|
|
|
|
|
const path = await save({
|
|
|
|
|
title: "Export deck",
|
|
|
|
|
defaultPath: `${d.name}.txt`,
|
|
|
|
|
filters: [{ name: "Decklist", extensions: ["txt"] }],
|
|
|
|
|
});
|
|
|
|
|
if (typeof path === "string") await api.exportDeck(d.id, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pickImportFile() {
|
|
|
|
|
const path = await open({
|
|
|
|
|
title: "Import deck",
|
|
|
|
|
filters: [{ name: "Decklist", extensions: ["txt"] }],
|
|
|
|
|
});
|
|
|
|
|
if (typeof path !== "string") return;
|
|
|
|
|
const base = path.split(/[/\\]/).pop() ?? "Imported deck";
|
|
|
|
|
const suggestedName = base.replace(/\.txt$/i, "");
|
|
|
|
|
showDialog({ kind: "import-name", suggestedName, path });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmImportName(name: string) {
|
|
|
|
|
if (dialog?.kind !== "import-name") return;
|
|
|
|
|
try {
|
|
|
|
|
const result = await api.importDeck(dialog.path, name);
|
|
|
|
|
setDialog({ kind: "import-result", result });
|
|
|
|
|
setDialogError(null);
|
|
|
|
|
await loadDecks();
|
|
|
|
|
openDeck(result.deck.id);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setDialogError(friendlyDeckError(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmDelete() {
|
|
|
|
|
if (dialog?.kind !== "delete") return;
|
|
|
|
|
await api.deleteDeck(dialog.deck.id);
|
|
|
|
|
if (selected === dialog.deck.id) {
|
|
|
|
|
setSelected(null);
|
|
|
|
|
setScreen("list");
|
|
|
|
|
}
|
|
|
|
|
setDialog(null);
|
|
|
|
|
await loadDecks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmRename(name: string) {
|
|
|
|
|
if (dialog?.kind !== "rename") return;
|
|
|
|
|
try {
|
|
|
|
|
await api.renameDeck(dialog.deck.id, name);
|
|
|
|
|
setDialog(null);
|
|
|
|
|
setDialogError(null);
|
|
|
|
|
await loadDecks();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setDialogError(friendlyDeckError(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmDuplicate(newName: string) {
|
|
|
|
|
if (dialog?.kind !== "duplicate") return;
|
|
|
|
|
try {
|
|
|
|
|
const d = await api.duplicateDeck(dialog.deck.id, newName);
|
|
|
|
|
setDialog(null);
|
|
|
|
|
setDialogError(null);
|
|
|
|
|
await loadDecks();
|
|
|
|
|
// From the builder, carry on in the copy; from the list, it just appears.
|
|
|
|
|
if (screen === "builder") openDeck(d.id);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setDialogError(friendlyDeckError(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing until the first fetch, so a builder left open doesn't flash the
|
|
|
|
|
// list on the way back in.
|
|
|
|
|
if (decks === null) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{screen === "builder" && deck ? (
|
|
|
|
|
<DeckBuilder
|
|
|
|
|
key={deck.id}
|
|
|
|
|
deck={deck}
|
|
|
|
|
refreshSignal={refreshSignal}
|
|
|
|
|
onBack={() => setScreen("list")}
|
|
|
|
|
onDecksChanged={loadDecks}
|
|
|
|
|
onRename={() => showDialog({ kind: "rename", deck })}
|
|
|
|
|
onDuplicate={() => showDialog({ kind: "duplicate", deck })}
|
|
|
|
|
onDelete={() => showDialog({ kind: "delete", deck })}
|
|
|
|
|
onExport={() => exportDeck(deck)}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<DeckList
|
|
|
|
|
decks={decks}
|
|
|
|
|
refreshSignal={refreshSignal}
|
|
|
|
|
openedAt={openedAt}
|
|
|
|
|
onOpen={openDeck}
|
|
|
|
|
onNew={() => showDialog({ kind: "create" })}
|
|
|
|
|
onImport={pickImportFile}
|
|
|
|
|
onRename={(d) => showDialog({ kind: "rename", deck: d })}
|
|
|
|
|
onDuplicate={(d) => showDialog({ kind: "duplicate", deck: d })}
|
|
|
|
|
onDelete={(d) => showDialog({ kind: "delete", deck: d })}
|
|
|
|
|
onExport={exportDeck}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{dialog?.kind === "create" && (
|
|
|
|
|
<NewDeckDialog
|
|
|
|
|
defaultFormat={newDeckFormat}
|
|
|
|
|
error={dialogError}
|
|
|
|
|
onConfirm={confirmCreate}
|
|
|
|
|
onCancel={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with
a ConfirmDialog for delete and PromptDialog (with friendly name-
collision errors) for rename/duplicate.
- Add a shared IconButton (plus overlay/danger variants) and replace
every ad hoc close/remove button in the app with it: deck actions,
remove-card-from-deck (list and tile), clear-commander, the card-
preview close, and the ingest-progress dismiss.
- Fix native <select> (Group by, Format, In deck): only color/border were
overridden, leaving native OS chrome (height, arrow, popup) in place.
Drop appearance and draw a custom chevron so they match text inputs.
- Click-to-preview: show both faces of a double-faced card side by side
on wide viewports instead of requiring Flip, and size the preview
images larger overall.
- Sidebar footer: replace the plain "N cards" line with a "Your library"
panel (unique/copies/decks tiles + a WUBRG color-distribution bar),
polling get_library_stats while card data is loaded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:47 -05:00
|
|
|
{dialog?.kind === "delete" && (
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
title="Delete deck"
|
|
|
|
|
message={`Delete "${dialog.deck.name}"? This can't be undone.`}
|
|
|
|
|
confirmLabel="Delete"
|
|
|
|
|
danger
|
|
|
|
|
onConfirm={confirmDelete}
|
|
|
|
|
onCancel={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{dialog?.kind === "rename" && (
|
|
|
|
|
<PromptDialog
|
|
|
|
|
title="Rename deck"
|
|
|
|
|
label="Deck name"
|
|
|
|
|
defaultValue={dialog.deck.name}
|
|
|
|
|
confirmLabel="Rename"
|
|
|
|
|
error={dialogError}
|
|
|
|
|
onConfirm={confirmRename}
|
|
|
|
|
onCancel={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{dialog?.kind === "duplicate" && (
|
|
|
|
|
<PromptDialog
|
|
|
|
|
title="Duplicate deck"
|
|
|
|
|
label="Name for the copy"
|
|
|
|
|
defaultValue={`${dialog.deck.name} (copy)`}
|
|
|
|
|
confirmLabel="Duplicate"
|
|
|
|
|
error={dialogError}
|
|
|
|
|
onConfirm={confirmDuplicate}
|
|
|
|
|
onCancel={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-07-20 00:21:22 -05:00
|
|
|
{dialog?.kind === "import-name" && (
|
|
|
|
|
<PromptDialog
|
|
|
|
|
title="Import deck"
|
|
|
|
|
label="Deck name"
|
|
|
|
|
defaultValue={dialog.suggestedName}
|
|
|
|
|
confirmLabel="Import"
|
|
|
|
|
error={dialogError}
|
|
|
|
|
onConfirm={confirmImportName}
|
|
|
|
|
onCancel={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{dialog?.kind === "import-result" && (
|
|
|
|
|
<InfoDialog
|
|
|
|
|
title="Deck imported"
|
|
|
|
|
message={
|
|
|
|
|
dialog.result.unresolved.length > 0
|
|
|
|
|
? `Imported "${dialog.result.deck.name}". ${dialog.result.unresolved.length} card name${dialog.result.unresolved.length === 1 ? "" : "s"} couldn't be matched:`
|
|
|
|
|
: `Imported "${dialog.result.deck.name}" — every card matched.`
|
|
|
|
|
}
|
|
|
|
|
items={dialog.result.unresolved}
|
|
|
|
|
onClose={() => setDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Decks tab: a deck list page, with the builder on its own screen
The deck list is no longer a sidebar pinned beside the builder; on every
screen size the Decks tab opens to a deck list page (DeckList), and picking
a deck opens its builder full-width with an "All decks" back button. The
list shows each deck as an art tile (or a compact row, toggleable) with its
colors, size against its format, owned %, cost and legality, and can be
searched, filtered by format and sorted (recently opened, name, format,
size, cost, owned %). Rename/duplicate/export/delete live in each deck's
menu and beside the builder's title; New deck now asks for a format too,
defaulting to the last one used.
A deck's cover art is the commander, else its priciest nonland, unless a
card is chosen with "Use as cover" in the card preview. The cover is stored
in decks.cover_oracle_id (migration 0007) and synced as DeckDoc.cover; a
document carrying one is version 3, so the sync server must be updated
before clients publish covers. The list's stats come from one
all_deck_contents query, summarised with the builder's own analysis code,
now in deckAnalysis.ts.
Closes #9
Closes #11
Closes #12
2026-10-25 15:04:41 -05:00
|
|
|
</>
|
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
|
|
|
);
|
|
|
|
|
}
|