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

@@ -138,7 +138,9 @@ export const Icons = {
/**
* A centered modal dialog (backdrop + card) shared by [`ConfirmDialog`],
* [`PromptDialog`], and other dialogs built outside this module (e.g. the
* command palette). Dismisses on Escape or backdrop click.
* command palette). Dismisses on Escape, backdrop click, or the title-bar
* close button — the explicit button matters most on mobile, where there's
* no Escape key and a `wide` dialog can leave little to no backdrop to tap.
*/
export function DialogFrame({
title,
@@ -161,7 +163,10 @@ export function DialogFrame({
return (
<div className="dialog-backdrop" onClick={onCancel}>
<div className={`dialog ${wide ? "dialog-wide" : ""}`} onClick={(e) => e.stopPropagation()}>
<h3 className="dialog-title">{title}</h3>
<div className="dialog-title-row">
<h3 className="dialog-title">{title}</h3>
<IconButton icon={Icons.close} label="Close" onClick={onCancel} />
</div>
{children}
</div>
</div>
@@ -1256,11 +1261,19 @@ export function usePersistedState<T>(key: string, initial: T): [T, (v: T) => voi
export type ViewMode = "list" | "tiles";
/** View-mode state persisted to localStorage so it survives tab switches. */
/** View-mode state persisted to localStorage so it survives tab switches.
*
* With nothing persisted yet, the default itself is width-aware: List's wide
* multi-column table forces horizontal scrolling on a phone, so narrow
* screens start on Tiles (a reflowing grid, no horizontal scroll) instead.
* This only picks the *default* — once a mode is explicitly chosen it's
* remembered and used everywhere, same as before. */
export function useViewMode(key: string): [ViewMode, (m: ViewMode) => void] {
const [mode, setMode] = useState<ViewMode>(
() => (localStorage.getItem(key) as ViewMode) || "list",
);
const [mode, setMode] = useState<ViewMode>(() => {
const saved = localStorage.getItem(key) as ViewMode | null;
if (saved) return saved;
return typeof window !== "undefined" && window.innerWidth <= 720 ? "tiles" : "list";
});
const set = useCallback(
(m: ViewMode) => {
localStorage.setItem(key, m);