Mobile UI pass: responsive tables, deck-view drill-down, dialog fixes

- Library/Card Search/Deck tables stack into dense card-style rows below
  720px instead of scrolling horizontally (floated thumbnail, wrapping
  chips for MV/Owned/color pips).
- Deck view: fix desktop min-widths and a non-wrapping button row that
  forced horizontal scroll on phones; split the deck list and the
  deck-building pane into separate drill-down screens with a back button
  so a long deck list doesn't bury the builder.
- Suggested Cards modal: fix action buttons crushing the card name down to
  one word per line by giving them their own full-width row.
- Add a close (X) button to the shared dialog title bar — mobile has no
  Escape key and wide dialogs leave little backdrop to tap.
- Info tab, bottom-nav loading/safe-area fixes, and Gradle NDK_HOME
  inheritance fix carried over from the Android bring-up work.
This commit is contained in:
2026-08-02 16:10:25 -05:00
parent 94e4f25ec4
commit 663af32562
10 changed files with 646 additions and 99 deletions

View File

@@ -346,6 +346,11 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
const [filterQuery, setFilterQuery] = useState("");
const [dialog, setDialog] = useState<DeckDialog>(null);
const [dialogError, setDialogError] = useState<string | null>(null);
// On narrow screens the deck list and the deck-building interface are
// separate "screens" (only one visible at a time) rather than stacked —
// this only matters below the .deck-layout stacking breakpoint; CSS keeps
// both panes visible side by side above it regardless of this flag.
const [mobileShowList, setMobileShowList] = useState(true);
const [libQuery, setLibQuery] = useState("");
const [libResults, setLibResults] = useState<CardRef[]>([]);
@@ -467,6 +472,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
setNewName("");
await loadDecks();
setSelected(d.id);
setMobileShowList(false);
}
async function exportDeck(d: Deck) {
@@ -497,6 +503,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
setDialogError(null);
await loadDecks();
setSelected(result.deck.id);
setMobileShowList(false);
} catch (err) {
setDialogError(friendlyDeckError(err));
}
@@ -638,7 +645,10 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
async function confirmDelete() {
if (dialog?.kind !== "delete") return;
await api.deleteDeck(dialog.deck.id);
if (selected === dialog.deck.id) setSelected(null);
if (selected === dialog.deck.id) {
setSelected(null);
setMobileShowList(true);
}
setDialog(null);
await loadDecks();
}
@@ -765,14 +775,14 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
<tbody>
{entries.map((e) => (
<tr key={e.oracle_id}>
<td>
<td className="cell-check">
<input
type="checkbox"
checked={selectedCards.has(e.oracle_id)}
onChange={() => toggleCard(e.oracle_id)}
/>
</td>
<td>
<td className="cell-thumb">
<CardThumb card={e} />
</td>
<td>
@@ -783,7 +793,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
{e.name}
</td>
<td className="card-type">{e.type_line}</td>
<td>
<td data-label="In deck">
<QtyStepper
value={e.quantity}
onDecrement={() => setQty(e.oracle_id, e.quantity - 1)}
@@ -792,7 +802,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
pending={pendingRemoval.has(e.oracle_id)}
/>
</td>
<td>{e.quantity_owned}</td>
<td data-label="Owned">{e.quantity_owned}</td>
<td>
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
</td>
@@ -818,7 +828,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
}
return (
<div className="deck-layout">
<div className={`deck-layout ${mobileShowList ? "show-list" : "show-detail"}`}>
<div className="deck-sidebar">
<div className="row" style={{ justifyContent: "space-between" }}>
<h2>Decks</h2>
@@ -840,7 +850,10 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
<li
key={d.id}
style={d.id === selected ? { background: "var(--accent-subtle)" } : undefined}
onClick={() => setSelected(d.id)}
onClick={() => {
setSelected(d.id);
setMobileShowList(false);
}}
>
<div className="deck-list-row">
<span className="card-name">{d.name}</span>
@@ -887,13 +900,22 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
</ul>
</div>
<div style={{ flex: 1 }}>
<div className="deck-detail-pane">
{selected === null || !deck ? (
<p className="hint">Select or create a deck.</p>
) : (
<>
<div className="row" style={{ justifyContent: "space-between" }}>
<h2>{deck.name}</h2>
<div className="row" style={{ gap: 8 }}>
<span className="deck-back-btn">
<IconButton
icon={Icons.arrowLeftCircle}
label="Back to decks"
onClick={() => setMobileShowList(true)}
/>
</span>
<h2>{deck.name}</h2>
</div>
<div className="row" style={{ gap: 8 }}>
<button
onClick={() => setSuggestOpen(true)}