List views: choose which columns to show
A Columns menu next to the List/Tiles toggle hides or shows optional columns in the Library, Card Search, deck builder and Decks list row views. Choices are saved per view on each device. The selection checkbox and name always show. Closes #8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1994,6 +1994,97 @@ export function ViewToggle({
|
||||
);
|
||||
}
|
||||
|
||||
// --- List columns ----------------------------------------------------------
|
||||
|
||||
/** An optional column of a list view — one the user can hide. */
|
||||
export interface ColumnDef<K extends string> {
|
||||
id: K;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Columns<K extends string> {
|
||||
defs: ColumnDef<K>[];
|
||||
shown: (id: K) => boolean;
|
||||
toggle: (id: K) => void;
|
||||
reset: () => void;
|
||||
/** Whether any column is hidden. */
|
||||
customized: boolean;
|
||||
}
|
||||
|
||||
/** Which optional columns a list view shows, persisted per view on this
|
||||
* device. Stores the *hidden* ids, so a column added later shows by default. */
|
||||
export function useColumns<K extends string>(key: string, defs: ColumnDef<K>[]): Columns<K> {
|
||||
const [stored, setStored] = usePersistedState<K[]>(key, []);
|
||||
// Drop ids for columns that no longer exist.
|
||||
const hidden = stored.filter((id) => defs.some((d) => d.id === id));
|
||||
return {
|
||||
defs,
|
||||
shown: (id) => !hidden.includes(id),
|
||||
toggle: (id) => setStored(hidden.includes(id) ? hidden.filter((h) => h !== id) : [...hidden, id]),
|
||||
reset: () => setStored([]),
|
||||
customized: hidden.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** "Columns" button opening a checklist of a list view's optional columns.
|
||||
* Stays open while toggling, so several can change in one go. */
|
||||
export function ColumnsMenu<K extends string>({ columns }: { columns: Columns<K> }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
|
||||
document.addEventListener("mousedown", onDown);
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onDown);
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<span className="columns-menu" ref={ref}>
|
||||
<button
|
||||
className={columns.customized ? "columns-menu-btn customized" : "columns-menu-btn"}
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
title="Choose which columns to show"
|
||||
>
|
||||
Columns ▾
|
||||
</button>
|
||||
{open && (
|
||||
<div className="library-menu columns-menu-list" role="menu">
|
||||
{columns.defs.map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={columns.shown(d.id)}
|
||||
onClick={() => columns.toggle(d.id)}
|
||||
>
|
||||
<span className="library-menu-check">{columns.shown(d.id) ? "✓" : ""}</span>
|
||||
{d.label}
|
||||
</button>
|
||||
))}
|
||||
{columns.customized && (
|
||||
<>
|
||||
<hr />
|
||||
<button role="menuitem" onClick={columns.reset}>
|
||||
<span className="library-menu-check" />
|
||||
Show all
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Tiles per row --------------------------------------------------------
|
||||
|
||||
// Mirror `.tile-grid`'s `repeat(auto-fill, minmax(190px, 1fr))` and `gap` in
|
||||
|
||||
@@ -2673,6 +2673,31 @@ button.pull-action {
|
||||
.tile-columns .tile-columns-count:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
/* List-view column picker (see `ColumnsMenu`): its checklist opens under the
|
||||
button, right-aligned since it sits at the toolbar's right end. */
|
||||
.columns-menu {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.columns-menu-btn {
|
||||
font-size: 13px;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
.columns-menu-btn.customized {
|
||||
border-color: var(--accent-glow);
|
||||
}
|
||||
.columns-menu .columns-menu-list {
|
||||
left: auto;
|
||||
right: 0;
|
||||
min-width: 190px;
|
||||
}
|
||||
/* On a phone the toolbar wraps and the button can land at the left edge. */
|
||||
@media (max-width: 720px) {
|
||||
.columns-menu .columns-menu-list {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Deck list (the Decks tab's front page)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { api, Deck, LibraryCard, LibraryFacets, PreviewContext } from "../api";
|
||||
import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
ColumnDef,
|
||||
ColumnsMenu,
|
||||
DeckChip,
|
||||
ColorPips,
|
||||
GroupBySelect,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
groupCards,
|
||||
sortCards,
|
||||
usePersistedState,
|
||||
useColumns,
|
||||
useTileColumns,
|
||||
useViewMode,
|
||||
} from "../components";
|
||||
@@ -27,6 +30,19 @@ import { CardFilterPanel, NO_FACETS, useCardFilter } from "../cardFilter";
|
||||
// db, not just what's owned, so an unbounded/loose filter needs a limit.
|
||||
const CAP = 300;
|
||||
|
||||
type SearchColumn = "image" | "colors" | "type" | "mv" | "owned" | "decks" | "add";
|
||||
|
||||
// The List view's optional columns; the checkbox and name always show.
|
||||
const SEARCH_COLUMNS: ColumnDef<SearchColumn>[] = [
|
||||
{ id: "image", label: "Image" },
|
||||
{ id: "colors", label: "Colors" },
|
||||
{ id: "type", label: "Type" },
|
||||
{ id: "mv", label: "MV" },
|
||||
{ id: "owned", label: "Owned" },
|
||||
{ id: "decks", label: "In decks" },
|
||||
{ id: "add", label: "Add to deck" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Browse the whole downloaded card universe (every card in reference.db,
|
||||
* owned or not) — the Library tab's sibling, reusing its filter/group/sort/
|
||||
@@ -58,6 +74,7 @@ export function CardSearch({
|
||||
|
||||
const [view, setView] = useViewMode("cardsearch-view");
|
||||
const tiles = useTileColumns();
|
||||
const columns = useColumns("cardsearch-columns", SEARCH_COLUMNS);
|
||||
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("cardsearch-group", "none");
|
||||
const [sortBy, setSortBy] = usePersistedState<SortMode>("cardsearch-sort", "name");
|
||||
const [sortDir, setSortDir] = usePersistedState<SortDir>("cardsearch-sort-dir", "asc");
|
||||
@@ -216,14 +233,14 @@ export function CardSearch({
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>Colors</th>
|
||||
{columns.shown("image") && <th></th>}
|
||||
{columns.shown("colors") && <th>Colors</th>}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>MV</th>
|
||||
<th>Owned</th>
|
||||
<th>In decks</th>
|
||||
<th></th>
|
||||
{columns.shown("type") && <th>Type</th>}
|
||||
{columns.shown("mv") && <th>MV</th>}
|
||||
{columns.shown("owned") && <th>Owned</th>}
|
||||
{columns.shown("decks") && <th>In decks</th>}
|
||||
{columns.shown("add") && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -236,36 +253,46 @@ export function CardSearch({
|
||||
onChange={() => toggleOne(c.oracle_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} preview={preview} />
|
||||
</td>
|
||||
<td>
|
||||
<ColorPips mask={c.color_identity} />
|
||||
</td>
|
||||
{columns.shown("image") && (
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} preview={preview} />
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("colors") && (
|
||||
<td>
|
||||
<ColorPips mask={c.color_identity} />
|
||||
</td>
|
||||
)}
|
||||
<td className="card-name">{c.name}</td>
|
||||
<td className="card-type">{c.type_line}</td>
|
||||
<td data-label="MV">{c.mana_value}</td>
|
||||
<td data-label="Owned">
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
onDecrement={() => adjust(c.oracle_id, -1)}
|
||||
onIncrement={() => adjust(c.oracle_id, 1)}
|
||||
/>
|
||||
</td>
|
||||
<td data-label="In decks">
|
||||
{c.decks.map((d) => (
|
||||
<DeckChip key={d} name={d} idea={!c.physical_decks.includes(d)} />
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => addToDeck(c)}
|
||||
disabled={deckId === "" || busy.has(c.oracle_id)}
|
||||
title={deckId === "" ? "Choose a deck above first" : "Add 1 copy to the selected deck"}
|
||||
>
|
||||
Add to deck
|
||||
</button>
|
||||
</td>
|
||||
{columns.shown("type") && <td className="card-type">{c.type_line}</td>}
|
||||
{columns.shown("mv") && <td data-label="MV">{c.mana_value}</td>}
|
||||
{columns.shown("owned") && (
|
||||
<td data-label="Owned">
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
onDecrement={() => adjust(c.oracle_id, -1)}
|
||||
onIncrement={() => adjust(c.oracle_id, 1)}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("decks") && (
|
||||
<td data-label="In decks">
|
||||
{c.decks.map((d) => (
|
||||
<DeckChip key={d} name={d} idea={!c.physical_decks.includes(d)} />
|
||||
))}
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("add") && (
|
||||
<td>
|
||||
<button
|
||||
onClick={() => addToDeck(c)}
|
||||
disabled={deckId === "" || busy.has(c.oracle_id)}
|
||||
title={deckId === "" ? "Choose a deck above first" : "Add 1 copy to the selected deck"}
|
||||
>
|
||||
Add to deck
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -425,6 +452,7 @@ export function CardSearch({
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
{view === "list" && <ColumnsMenu columns={columns} />}
|
||||
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
|
||||
<label
|
||||
className="row"
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { api, Deck, DECK_STATUSES, DeckEntry, DeckStatus } from "../api";
|
||||
import { artCrop, ColorPips, fold, IconButton, Icons, usePersistedState, useViewMode, ViewToggle } from "../components";
|
||||
import {
|
||||
artCrop,
|
||||
ColorPips,
|
||||
ColumnDef,
|
||||
Columns,
|
||||
ColumnsMenu,
|
||||
fold,
|
||||
IconButton,
|
||||
Icons,
|
||||
useColumns,
|
||||
usePersistedState,
|
||||
useViewMode,
|
||||
ViewToggle,
|
||||
} from "../components";
|
||||
import { DeckSummary, formatCost, summarizeDeck } from "../deckAnalysis";
|
||||
|
||||
type SortMode = "recent" | "name" | "format" | "size" | "cost" | "owned";
|
||||
@@ -14,6 +27,20 @@ const SORTS: { id: SortMode; label: string }[] = [
|
||||
{ id: "owned", label: "Owned %" },
|
||||
];
|
||||
|
||||
type DeckListColumn = "colors" | "subtitle" | "status" | "format" | "size" | "owned" | "cost" | "legality";
|
||||
|
||||
// The row view's optional columns; the art, name and ⋯ menu always show.
|
||||
const DECK_LIST_COLUMNS: ColumnDef<DeckListColumn>[] = [
|
||||
{ id: "colors", label: "Colors" },
|
||||
{ id: "subtitle", label: "Commander" },
|
||||
{ id: "status", label: "Status" },
|
||||
{ id: "format", label: "Format" },
|
||||
{ id: "size", label: "Size" },
|
||||
{ id: "owned", label: "Owned" },
|
||||
{ id: "cost", label: "Cost" },
|
||||
{ id: "legality", label: "Legality" },
|
||||
];
|
||||
|
||||
interface DeckActions {
|
||||
onOpen: (id: number) => void;
|
||||
onRename: (deck: Deck) => void;
|
||||
@@ -239,7 +266,15 @@ function DeckTile({ row, actions }: { row: Row; actions: DeckActions }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DeckRow({ row, actions }: { row: Row; actions: DeckActions }) {
|
||||
function DeckRow({
|
||||
row,
|
||||
actions,
|
||||
columns,
|
||||
}: {
|
||||
row: Row;
|
||||
actions: DeckActions;
|
||||
columns: Columns<DeckListColumn>;
|
||||
}) {
|
||||
const { deck, summary: s } = row;
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
return (
|
||||
@@ -248,20 +283,22 @@ function DeckRow({ row, actions }: { row: Row; actions: DeckActions }) {
|
||||
<div className="deck-row-main">
|
||||
<div className="deck-row-name">
|
||||
<span className="card-name">{deck.name}</span>
|
||||
{s.size > 0 && <ColorPips mask={s.colors} />}
|
||||
{columns.shown("colors") && s.size > 0 && <ColorPips mask={s.colors} />}
|
||||
</div>
|
||||
<div className="deck-row-sub">{subtitle(row)}</div>
|
||||
{columns.shown("subtitle") && <div className="deck-row-sub">{subtitle(row)}</div>}
|
||||
</div>
|
||||
<div className="deck-row-stats">
|
||||
<StatusChip row={row} />
|
||||
{deck.format && <span className="deck-chip">{deck.format}</span>}
|
||||
<DeckSize summary={s} />
|
||||
<span className="deck-row-owned" title={ownedLabel(s)}>
|
||||
<OwnedBar summary={s} />
|
||||
<span>{s.size ? `${Math.floor(s.owned * 100)}%` : "—"}</span>
|
||||
</span>
|
||||
<span className="deck-row-cost">{formatCost(s.cost)}</span>
|
||||
<LegalityBadge summary={s} compact />
|
||||
{columns.shown("status") && <StatusChip row={row} />}
|
||||
{columns.shown("format") && deck.format && <span className="deck-chip">{deck.format}</span>}
|
||||
{columns.shown("size") && <DeckSize summary={s} />}
|
||||
{columns.shown("owned") && (
|
||||
<span className="deck-row-owned" title={ownedLabel(s)}>
|
||||
<OwnedBar summary={s} />
|
||||
<span>{s.size ? `${Math.floor(s.owned * 100)}%` : "—"}</span>
|
||||
</span>
|
||||
)}
|
||||
{columns.shown("cost") && <span className="deck-row-cost">{formatCost(s.cost)}</span>}
|
||||
{columns.shown("legality") && <LegalityBadge summary={s} compact />}
|
||||
</div>
|
||||
<DeckMenu deck={deck} actions={actions} onOpenChange={setMenuOpen} />
|
||||
</div>
|
||||
@@ -290,6 +327,7 @@ export function DeckList({
|
||||
} & DeckActions) {
|
||||
const [contents, setContents] = useState<Record<number, DeckEntry[]> | null>(null);
|
||||
const [view, setView] = useViewMode("deck-list-view", "tiles");
|
||||
const columns = useColumns("deck-list-columns", DECK_LIST_COLUMNS);
|
||||
const [sort, setSort] = usePersistedState<SortMode>("deck-list-sort", "recent");
|
||||
const [query, setQuery] = useState("");
|
||||
const [format, setFormat] = useState<string | null>(null);
|
||||
@@ -361,7 +399,7 @@ export function DeckList({
|
||||
) : (
|
||||
<div className="deck-gallery-rows">
|
||||
{list.map((r) => (
|
||||
<DeckRow key={r.deck.id} row={r} actions={actions} />
|
||||
<DeckRow key={r.deck.id} row={r} actions={actions} columns={columns} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -420,6 +458,7 @@ export function DeckList({
|
||||
))}
|
||||
</select>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
{view === "list" && <ColumnsMenu columns={columns} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
CardTile,
|
||||
ColorDistributionBar,
|
||||
ColorPips,
|
||||
ColumnDef,
|
||||
ColumnsMenu,
|
||||
ConfirmDialog,
|
||||
DialogFrame,
|
||||
GroupBySelect,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
groupCards,
|
||||
useClickPreview,
|
||||
useHoverPreview,
|
||||
useColumns,
|
||||
usePersistedState,
|
||||
useTileColumns,
|
||||
useViewMode,
|
||||
@@ -70,6 +73,19 @@ function boardToggleProps(e: DeckEntry) {
|
||||
: { icon: Icons.helpCircle, label: "Move to maybeboard" };
|
||||
}
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
type DeckDialog =
|
||||
| { kind: "create" }
|
||||
| { kind: "delete"; deck: Deck }
|
||||
@@ -227,6 +243,7 @@ function DeckBuilder({
|
||||
const [contents, setContents] = useState<DeckEntry[]>([]);
|
||||
const [view, setView] = useViewMode("deck-view");
|
||||
const tiles = useTileColumns();
|
||||
const columns = useColumns("deck-columns", DECK_COLUMNS);
|
||||
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("deck-group", "none");
|
||||
const [combineSections, setCombineSections] = usePersistedState("deck-combine-sections", false);
|
||||
const [filterQuery, setFilterQuery] = useState("");
|
||||
@@ -811,14 +828,14 @@ function DeckBuilder({
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>Colors</th>
|
||||
{columns.shown("image") && <th></th>}
|
||||
{columns.shown("colors") && <th>Colors</th>}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>In deck</th>
|
||||
<th>Owned</th>
|
||||
<th>Where</th>
|
||||
<th></th>
|
||||
{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>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -831,12 +848,16 @@ function DeckBuilder({
|
||||
onChange={() => toggleCard(e.oracle_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={e} preview={preview} />
|
||||
</td>
|
||||
<td>
|
||||
<ColorPips mask={e.color_identity} />
|
||||
</td>
|
||||
{columns.shown("image") && (
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={e} preview={preview} />
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("colors") && (
|
||||
<td>
|
||||
<ColorPips mask={e.color_identity} />
|
||||
</td>
|
||||
)}
|
||||
<td className="card-name">
|
||||
{e.is_commander && <span title="Commander">★ </span>}
|
||||
{e.name}
|
||||
@@ -846,26 +867,30 @@ function DeckBuilder({
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="card-type">{e.type_line}</td>
|
||||
<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>
|
||||
<td data-label="Owned">{e.quantity_owned}</td>
|
||||
<td data-label="Where">{whereabouts(e)}</td>
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -1124,6 +1149,7 @@ function DeckBuilder({
|
||||
<span className="spacer" />
|
||||
<GroupBySelect value={groupBy} onChange={setGroupBy} />
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
{view === "list" && <ColumnsMenu columns={columns} />}
|
||||
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
ColumnDef,
|
||||
ColumnsMenu,
|
||||
DeckChip,
|
||||
DialogFrame,
|
||||
ColorPips,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
groupCards,
|
||||
sortCards,
|
||||
usePersistedState,
|
||||
useColumns,
|
||||
useTileColumns,
|
||||
useViewMode,
|
||||
} from "../components";
|
||||
@@ -40,6 +43,18 @@ import { CardFilterPanel, NO_FACETS, useCardFilter } from "../cardFilter";
|
||||
// user a window to undo an accidental removal by clicking + again.
|
||||
const REMOVAL_GRACE_MS = 5000;
|
||||
|
||||
type LibraryColumn = "image" | "colors" | "type" | "mv" | "owned" | "decks";
|
||||
|
||||
// The List view's optional columns; the checkbox and name always show.
|
||||
const LIBRARY_COLUMNS: ColumnDef<LibraryColumn>[] = [
|
||||
{ id: "image", label: "Image" },
|
||||
{ id: "colors", label: "Colors" },
|
||||
{ id: "type", label: "Type" },
|
||||
{ id: "mv", label: "MV" },
|
||||
{ id: "owned", label: "Owned" },
|
||||
{ id: "decks", label: "Decks / Location" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Browse & filter the owned library by name and card criteria.
|
||||
*
|
||||
@@ -61,6 +76,7 @@ export function LibraryBrowser({
|
||||
const [rows, setRows] = useState<LibraryCard[]>([]);
|
||||
const [view, setView] = useViewMode("library-view");
|
||||
const tiles = useTileColumns();
|
||||
const columns = useColumns("library-columns", LIBRARY_COLUMNS);
|
||||
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("library-group", "none");
|
||||
const [sortBy, setSortBy] = usePersistedState<SortMode>("library-sort", "name");
|
||||
const [sortDir, setSortDir] = usePersistedState<SortDir>("library-sort-dir", "asc");
|
||||
@@ -322,13 +338,13 @@ export function LibraryBrowser({
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>Colors</th>
|
||||
{columns.shown("image") && <th></th>}
|
||||
{columns.shown("colors") && <th>Colors</th>}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>MV</th>
|
||||
<th>Owned</th>
|
||||
<th>Decks / Location</th>
|
||||
{columns.shown("type") && <th>Type</th>}
|
||||
{columns.shown("mv") && <th>MV</th>}
|
||||
{columns.shown("owned") && <th>Owned</th>}
|
||||
{columns.shown("decks") && <th>Decks / Location</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -341,36 +357,42 @@ export function LibraryBrowser({
|
||||
onChange={() => toggleOne(c.oracle_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} preview={preview} />
|
||||
</td>
|
||||
<td>
|
||||
<ColorPips mask={c.color_identity} />
|
||||
</td>
|
||||
{columns.shown("image") && (
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} preview={preview} />
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("colors") && (
|
||||
<td>
|
||||
<ColorPips mask={c.color_identity} />
|
||||
</td>
|
||||
)}
|
||||
<td className="card-name">{c.name}</td>
|
||||
<td className="card-type">{c.type_line}</td>
|
||||
<td data-label="MV">{c.mana_value}</td>
|
||||
<td
|
||||
data-label="Owned"
|
||||
className={c.quantity_assigned > c.quantity_owned ? "over-assigned" : ""}
|
||||
>
|
||||
<span className="row" style={{ gap: 8 }}>
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
onDecrement={() => adjust(c.oracle_id, -1)}
|
||||
onIncrement={() => adjust(c.oracle_id, 1)}
|
||||
warn={c.quantity_assigned > c.quantity_owned}
|
||||
pending={pendingRemoval.has(c.oracle_id)}
|
||||
/>
|
||||
{c.quantity_assigned > c.quantity_owned && (
|
||||
<span title="Assigned to more decks than you own">
|
||||
⚠ {c.quantity_assigned} assigned
|
||||
</span>
|
||||
)}
|
||||
{spareNote(c)}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Decks / Location">{deckOrLocation(c)}</td>
|
||||
{columns.shown("type") && <td className="card-type">{c.type_line}</td>}
|
||||
{columns.shown("mv") && <td data-label="MV">{c.mana_value}</td>}
|
||||
{columns.shown("owned") && (
|
||||
<td
|
||||
data-label="Owned"
|
||||
className={c.quantity_assigned > c.quantity_owned ? "over-assigned" : ""}
|
||||
>
|
||||
<span className="row" style={{ gap: 8 }}>
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
onDecrement={() => adjust(c.oracle_id, -1)}
|
||||
onIncrement={() => adjust(c.oracle_id, 1)}
|
||||
warn={c.quantity_assigned > c.quantity_owned}
|
||||
pending={pendingRemoval.has(c.oracle_id)}
|
||||
/>
|
||||
{c.quantity_assigned > c.quantity_owned && (
|
||||
<span title="Assigned to more decks than you own">
|
||||
⚠ {c.quantity_assigned} assigned
|
||||
</span>
|
||||
)}
|
||||
{spareNote(c)}
|
||||
</span>
|
||||
</td>
|
||||
)}
|
||||
{columns.shown("decks") && <td data-label="Decks / Location">{deckOrLocation(c)}</td>}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -627,6 +649,7 @@ export function LibraryBrowser({
|
||||
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
|
||||
/>
|
||||
<ViewToggle mode={view} onChange={setView} />
|
||||
{view === "list" && <ColumnsMenu columns={columns} />}
|
||||
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user