From d9f92c74fd5335fe74041a098c582ee04ed6a33b Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Mon, 26 Oct 2026 20:35:21 -0500 Subject: [PATCH] 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 --- src/components.tsx | 91 +++++++++++++++++++++++++++++++ src/styles.css | 25 +++++++++ src/views/CardSearch.tsx | 100 ++++++++++++++++++++++------------- src/views/DeckList.tsx | 67 ++++++++++++++++++----- src/views/DeckView.tsx | 92 ++++++++++++++++++++------------ src/views/LibraryBrowser.tsx | 93 ++++++++++++++++++++------------ 6 files changed, 350 insertions(+), 118 deletions(-) diff --git a/src/components.tsx b/src/components.tsx index 01e663c..48df040 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -1994,6 +1994,97 @@ export function ViewToggle({ ); } +// --- List columns ---------------------------------------------------------- + +/** An optional column of a list view — one the user can hide. */ +export interface ColumnDef { + id: K; + label: string; +} + +export interface Columns { + defs: ColumnDef[]; + 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(key: string, defs: ColumnDef[]): Columns { + const [stored, setStored] = usePersistedState(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({ columns }: { columns: Columns }) { + const [open, setOpen] = useState(false); + const ref = useRef(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 ( + + + {open && ( +
+ {columns.defs.map((d) => ( + + ))} + {columns.customized && ( + <> +
+ + + )} +
+ )} +
+ ); +} + // --- Tiles per row -------------------------------------------------------- // Mirror `.tile-grid`'s `repeat(auto-fill, minmax(190px, 1fr))` and `gap` in diff --git a/src/styles.css b/src/styles.css index 33930ee..9a009a4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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) diff --git a/src/views/CardSearch.tsx b/src/views/CardSearch.tsx index f5d1bcc..c910c9e 100644 --- a/src/views/CardSearch.tsx +++ b/src/views/CardSearch.tsx @@ -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[] = [ + { 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("cardsearch-group", "none"); const [sortBy, setSortBy] = usePersistedState("cardsearch-sort", "name"); const [sortDir, setSortDir] = usePersistedState("cardsearch-sort-dir", "asc"); @@ -216,14 +233,14 @@ export function CardSearch({ - - Colors + {columns.shown("image") && } + {columns.shown("colors") && Colors} Name - Type - MV - Owned - In decks - + {columns.shown("type") && Type} + {columns.shown("mv") && MV} + {columns.shown("owned") && Owned} + {columns.shown("decks") && In decks} + {columns.shown("add") && } @@ -236,36 +253,46 @@ export function CardSearch({ onChange={() => toggleOne(c.oracle_id)} /> - - - - - - + {columns.shown("image") && ( + + + + )} + {columns.shown("colors") && ( + + + + )} {c.name} - {c.type_line} - {c.mana_value} - - adjust(c.oracle_id, -1)} - onIncrement={() => adjust(c.oracle_id, 1)} - /> - - - {c.decks.map((d) => ( - - ))} - - - - + {columns.shown("type") && {c.type_line}} + {columns.shown("mv") && {c.mana_value}} + {columns.shown("owned") && ( + + adjust(c.oracle_id, -1)} + onIncrement={() => adjust(c.oracle_id, 1)} + /> + + )} + {columns.shown("decks") && ( + + {c.decks.map((d) => ( + + ))} + + )} + {columns.shown("add") && ( + + + + )} ))} @@ -425,6 +452,7 @@ export function CardSearch({ onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")} /> + {view === "list" && } {view === "tiles" && }