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:
@@ -50,6 +50,10 @@ export function CardSearch({
|
||||
const [subtypes, setSubtypes] = useState<string[]>([]);
|
||||
const [keywords, setKeywords] = useState<string[]>([]);
|
||||
const [oracleText, setOracleText] = useState("");
|
||||
// Collapsed by default (relevant on narrow/mobile screens only — see
|
||||
// .filters-more's CSS, which is always expanded above that breakpoint
|
||||
// regardless of this).
|
||||
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
||||
const [facets, setFacets] = useState<LibraryFacets>(NO_FACETS);
|
||||
|
||||
const [decks, setDecks] = useState<Deck[]>([]);
|
||||
@@ -185,7 +189,7 @@ export function CardSearch({
|
||||
<tbody>
|
||||
{cards.map((c) => (
|
||||
<tr key={c.oracle_id}>
|
||||
<td>
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} />
|
||||
</td>
|
||||
<td>
|
||||
@@ -193,15 +197,15 @@ export function CardSearch({
|
||||
</td>
|
||||
<td className="card-name">{c.name}</td>
|
||||
<td className="card-type">{c.type_line}</td>
|
||||
<td>{c.mana_value}</td>
|
||||
<td>
|
||||
<td data-label="MV">{c.mana_value}</td>
|
||||
<td data-label="Owned">
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
onDecrement={() => adjust(c.oracle_id, -1)}
|
||||
onIncrement={() => adjust(c.oracle_id, 1)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="In decks">
|
||||
{c.decks.map((d) => (
|
||||
<span key={d} className="deck-chip">
|
||||
{d}
|
||||
@@ -292,6 +296,7 @@ export function CardSearch({
|
||||
<label>Name contains</label>
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. bolt" />
|
||||
</div>
|
||||
<div className={`filters-more ${filtersExpanded ? "expanded" : ""}`}>
|
||||
<div className="field">
|
||||
<label>Playable in commander (color identity ⊆)</label>
|
||||
<ColorToggles value={commander} onChange={setCommander} />
|
||||
@@ -351,6 +356,13 @@ export function CardSearch({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="filters-toggle"
|
||||
onClick={() => setFiltersExpanded((v) => !v)}
|
||||
>
|
||||
{filtersExpanded ? "▴ Fewer filters" : "▾ More filters"}
|
||||
</button>
|
||||
|
||||
<div className="workspace-bar">
|
||||
{hasActiveFilter && (
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -54,6 +54,10 @@ export function LibraryBrowser({
|
||||
const [inDeck, setInDeck] = useState<number | "">("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [oracleText, setOracleText] = useState("");
|
||||
// Collapsed by default (relevant on narrow/mobile screens only — see
|
||||
// .filters-more's CSS, which is always expanded above that breakpoint
|
||||
// regardless of this).
|
||||
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
||||
const [facets, setFacets] = useState<LibraryFacets>(NO_FACETS);
|
||||
|
||||
const [decks, setDecks] = useState<Deck[]>([]);
|
||||
@@ -321,14 +325,14 @@ export function LibraryBrowser({
|
||||
<tbody>
|
||||
{cards.map((c) => (
|
||||
<tr key={c.oracle_id}>
|
||||
<td>
|
||||
<td className="cell-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(c.oracle_id)}
|
||||
onChange={() => toggleOne(c.oracle_id)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td className="cell-thumb">
|
||||
<CardThumb card={c} />
|
||||
</td>
|
||||
<td>
|
||||
@@ -336,8 +340,11 @@ export function LibraryBrowser({
|
||||
</td>
|
||||
<td className="card-name">{c.name}</td>
|
||||
<td className="card-type">{c.type_line}</td>
|
||||
<td>{c.mana_value}</td>
|
||||
<td className={c.quantity_assigned > c.quantity_owned ? "over-assigned" : ""}>
|
||||
<td data-label="MV">{c.mana_value}</td>
|
||||
<td
|
||||
data-label="Owned"
|
||||
className={c.quantity_assigned > c.quantity_owned ? "over-assigned" : ""}
|
||||
>
|
||||
<span className="row" style={{ gap: 8 }}>
|
||||
<QtyStepper
|
||||
value={c.quantity_owned}
|
||||
@@ -353,7 +360,7 @@ export function LibraryBrowser({
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{deckOrLocation(c)}</td>
|
||||
<td data-label="Decks / Location">{deckOrLocation(c)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -522,6 +529,7 @@ export function LibraryBrowser({
|
||||
<label>Name contains</label>
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. bolt" />
|
||||
</div>
|
||||
<div className={`filters-more ${filtersExpanded ? "expanded" : ""}`}>
|
||||
<div className="field">
|
||||
<label>Playable in commander (color identity ⊆)</label>
|
||||
<ColorToggles value={commander} onChange={setCommander} />
|
||||
@@ -600,6 +608,13 @@ export function LibraryBrowser({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="filters-toggle"
|
||||
onClick={() => setFiltersExpanded((v) => !v)}
|
||||
>
|
||||
{filtersExpanded ? "▴ Fewer filters" : "▾ More filters"}
|
||||
</button>
|
||||
|
||||
<div className="workspace-bar">
|
||||
<label className="workspace-bar-check" title="Select all filtered cards">
|
||||
|
||||
Reference in New Issue
Block a user