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" && }