Tiles view: +/- control for cards per row

Adjusts the tile grid's column count as an offset from the existing
width-based auto-fill default, shared across Library, Decks, Card search,
and set card lists. Clicking the count resets to the default.
This commit is contained in:
2026-10-16 21:14:43 -05:00
parent 214a535588
commit 6072d44ebe
6 changed files with 149 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import {
SortDir,
SortMode,
TagInput,
TileColumnsControl,
ViewToggle,
CardCriteriaFields,
applyCardCriteria,
@@ -22,6 +23,7 @@ import {
sortCards,
useCardCriteria,
usePersistedState,
useTileColumns,
useViewMode,
} from "../components";
@@ -76,6 +78,7 @@ export function CardSearch({
const selectAllRef = useRef<HTMLInputElement>(null);
const [view, setView] = useViewMode("cardsearch-view");
const tiles = useTileColumns();
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");
@@ -335,7 +338,7 @@ export function CardSearch({
function refTiles(cards: LibraryCard[]) {
return (
<div className="tile-grid">
<div {...tiles.gridProps}>
{cards.map((c) => (
<CardTile
key={c.oracle_id}
@@ -572,6 +575,7 @@ export function CardSearch({
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
/>
<ViewToggle mode={view} onChange={setView} />
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
<label
className="row"
style={{ gap: 4, cursor: view === "tiles" ? "pointer" : "default", opacity: view === "tiles" ? 1 : 0.5 }}

View File

@@ -18,11 +18,13 @@ import {
QtyStepper,
SampleHandModal,
StatTile,
TileColumnsControl,
ViewToggle,
groupCards,
useClickPreview,
useHoverPreview,
usePersistedState,
useTileColumns,
useViewMode,
} from "../components";
import { SuggestionsModal } from "../SuggestionsModal";
@@ -363,6 +365,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
const [newName, setNewName] = useState("");
const [contents, setContents] = useState<DeckEntry[]>([]);
const [view, setView] = useViewMode("deck-view");
const tiles = useTileColumns();
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("deck-group", "none");
const [combineSections, setCombineSections] = usePersistedState("deck-combine-sections", false);
const [filterQuery, setFilterQuery] = useState("");
@@ -754,7 +757,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
function deckTiles(entries: DeckEntry[]) {
return (
<div className="tile-grid">
<div {...tiles.gridProps}>
{entries.map((e) => (
<CardTile
key={e.oracle_id}
@@ -1120,6 +1123,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
<span className="spacer" />
<GroupBySelect value={groupBy} onChange={setGroupBy} />
<ViewToggle mode={view} onChange={setView} />
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
</div>
{selectedCards.size > 0 && (

View File

@@ -24,6 +24,7 @@ import {
SortDir,
SortMode,
TagInput,
TileColumnsControl,
ViewToggle,
CardCriteriaFields,
applyCardCriteria,
@@ -31,6 +32,7 @@ import {
sortCards,
useCardCriteria,
usePersistedState,
useTileColumns,
useViewMode,
} from "../components";
@@ -78,6 +80,7 @@ export function LibraryBrowser({
const [decks, setDecks] = useState<Deck[]>([]);
const [rows, setRows] = useState<LibraryCard[]>([]);
const [view, setView] = useViewMode("library-view");
const tiles = useTileColumns();
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");
@@ -405,7 +408,7 @@ export function LibraryBrowser({
function libTiles(cards: LibraryCard[]) {
return (
<div className="tile-grid">
<div {...tiles.gridProps}>
{cards.map((c) => (
<CardTile
key={c.oracle_id}
@@ -693,6 +696,7 @@ export function LibraryBrowser({
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
/>
<ViewToggle mode={view} onChange={setView} />
{view === "tiles" && <TileColumnsControl tiles={tiles} />}
</div>
</div>

View File

@@ -6,8 +6,10 @@ import {
CardTile,
ColorToggles,
QtyStepper,
TileColumnsControl,
applyCardCriteria,
useCardCriteria,
useTileColumns,
} from "../components";
import { useBackButton } from "../backButton";
@@ -75,6 +77,7 @@ export function NewsView() {
// doing nothing. Null while we work out which.
const [source, setSource] = useState<"local" | "live" | null>(null);
const [cardsError, setCardsError] = useState<string | null>(null);
const tiles = useTileColumns();
// The same criteria the Library and Card Search panels use — the set browser
// runs through search_reference rather than its own path, so every filter
// works here identically instead of being reimplemented.
@@ -197,6 +200,7 @@ export function NewsView() {
{cards && <span className="badge">{cards.length}</span>}
</h2>
<div className="page-header-actions">
<TileColumnsControl tiles={tiles} />
<button onClick={() => setOpenSet(null)}>← Back to news</button>
</div>
</div>
@@ -253,7 +257,7 @@ export function NewsView() {
: "No cards listed for this set yet."}
</p>
) : (
<div className="tile-grid" style={{ marginTop: "var(--space-4)" }}>
<div {...tiles.gridProps} style={{ ...tiles.gridProps.style, marginTop: "var(--space-4)" }}>
{cards.map((c) => (
<CardTile
key={c.oracle_id}