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:
2026-10-26 20:35:21 -05:00
parent a92b650dab
commit d9f92c74fd
6 changed files with 350 additions and 118 deletions

View File

@@ -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>