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:
346
src/styles.css
346
src/styles.css
@@ -84,6 +84,9 @@
|
||||
|
||||
/* Interactive control height — bumped on coarse pointers (see bottom). */
|
||||
--control-h: 34px;
|
||||
/* Height of .bottom-nav's own content — excludes the safe-area inset added
|
||||
on top of it, so consumers computing clearance must add that separately. */
|
||||
--bottom-nav-h: 58px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
@@ -399,6 +402,57 @@ input[type="checkbox"] {
|
||||
display: inline-flex;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Android-style bottom tab bar — replaces .nav-rail on narrow viewports (see
|
||||
the ≤720px block below), rather than the rail collapsing to an icon strip.
|
||||
Hidden by default; enabled only in that breakpoint. Fixed to the viewport
|
||||
(not `.app`) since `.app` uses `overflow: hidden` and z-indexes elsewhere
|
||||
(dialogs 400+, the click-preview modal 450) are the reason this sits at
|
||||
200 — above ordinary content, below anything that should cover the whole
|
||||
screen. */
|
||||
.bottom-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 200;
|
||||
height: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom));
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: var(--bg-rail);
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
.bottom-nav-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
height: 100%;
|
||||
padding: 0 2px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bottom-nav-item.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
.bottom-nav-item.active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
.nav-foot {
|
||||
margin-top: auto;
|
||||
padding: 14px 10px 10px;
|
||||
@@ -574,12 +628,19 @@ input[type="checkbox"] {
|
||||
background: var(--bg-content);
|
||||
}
|
||||
.topbar {
|
||||
height: 52px;
|
||||
/* min-height (not height): on Android the window draws edge-to-edge behind
|
||||
the status bar, so the safe-area inset has to ADD to the bar's height
|
||||
rather than eat into it — a fixed height would squash the buttons under
|
||||
the status bar instead of below it. Inert (0) anywhere without a system
|
||||
bar to avoid, e.g. desktop. */
|
||||
min-height: 52px;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 18px;
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
background: var(--bg-topbar);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
@@ -715,6 +776,15 @@ input[type="checkbox"] {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
/* Mobile's Info tab — card-database management, which lives in the topbar
|
||||
instead on desktop (see INFO_TAB in App.tsx). */
|
||||
.info-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Typography helpers
|
||||
============================================================ */
|
||||
@@ -1056,8 +1126,15 @@ li:focus-within .row-actions {
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dialog-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dialog-title {
|
||||
margin: 0 0 10px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
@@ -1363,6 +1440,107 @@ tbody tr {
|
||||
tbody tr:hover {
|
||||
background: rgba(255, 255, 255, 0.022);
|
||||
}
|
||||
|
||||
/* On a phone, the List view's 8-column table has no room to lay out
|
||||
side-by-side and forced horizontal scrolling of the whole page (not just
|
||||
the table) — see the .content padding/overflow rules elsewhere, nothing
|
||||
scoped the scroll to the table itself. Below this breakpoint each row
|
||||
becomes its own stacked card instead: every cell drops to its own line,
|
||||
headers (meaningless once stacked) are hidden, and cells that need a label
|
||||
outside their normal column context get one from `data-label` (set in the
|
||||
view components — see LibraryBrowser/CardSearch/DeckView's table JSX). */
|
||||
@media (max-width: 720px) {
|
||||
table,
|
||||
tbody,
|
||||
tr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
tbody tr {
|
||||
position: relative;
|
||||
overflow: hidden; /* contains .cell-thumb's float below */
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-3);
|
||||
padding-right: calc(var(--space-3) + 28px); /* clears .cell-check */
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
td {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
/* Selection checkbox: pinned to the row-card's corner instead of taking a
|
||||
line of its own. */
|
||||
.cell-check {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: auto;
|
||||
}
|
||||
/* Thumbnail floats so the name/type text wraps beside it — that's what
|
||||
buys back the vertical space the old one-cell-per-line layout wasted. */
|
||||
.cell-thumb {
|
||||
float: left;
|
||||
width: 56px;
|
||||
margin-right: var(--space-3);
|
||||
}
|
||||
.cell-thumb .card-thumb,
|
||||
.cell-thumb .thumb-empty {
|
||||
display: block;
|
||||
width: 56px;
|
||||
height: auto;
|
||||
}
|
||||
td.card-name {
|
||||
display: block;
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
td.card-type {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-caption);
|
||||
}
|
||||
/* Color pips and any other small cell (MV, Owned, In deck, Decks /
|
||||
Location, ...) sit inline as wrapping chips instead of one per line —
|
||||
as long as they fit within the floated thumbnail's height they tuck in
|
||||
beside it, otherwise they wrap below it like normal text. */
|
||||
td:has(.pips),
|
||||
td[data-label] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: var(--space-2) var(--space-3) 0 0;
|
||||
font-size: var(--fs-caption);
|
||||
}
|
||||
td[data-label]::before {
|
||||
content: attr(data-label) ":";
|
||||
font-size: var(--fs-micro);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--faint);
|
||||
}
|
||||
/* Buttons (Add to deck, maybeboard toggle, ...) always get their own full
|
||||
row below everything else. */
|
||||
td:has(button) {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
.over-assigned {
|
||||
color: var(--warn);
|
||||
font-weight: 600;
|
||||
@@ -1459,6 +1637,28 @@ tbody tr:hover {
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
/* Wraps every field after "Name contains" so it can be hidden as a group on
|
||||
narrow screens — a 9-10 field grid is a wall to scroll past on a phone
|
||||
before any results appear. `display: contents` on the wrapper (rather than
|
||||
e.g. a nested grid) keeps every field a direct child of `.filters` at every
|
||||
width, so it doesn't disturb the shared grid's column sizing/auto-flow;
|
||||
only the max-width:720px rule below actually hides anything. */
|
||||
.filters-more {
|
||||
display: contents;
|
||||
}
|
||||
/* Irrelevant above the breakpoint (every field is already visible there). */
|
||||
.filters-toggle {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.filters-toggle {
|
||||
display: inline-flex;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
.filters-more:not(.expanded) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* The results toolbar strip at the bottom of the workspace panel: selection +
|
||||
count on the left, group/sort/view on the right, divided from the filters. */
|
||||
.workspace-bar {
|
||||
@@ -1639,6 +1839,13 @@ tbody tr:hover {
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
padding: 24px 12px;
|
||||
/* The close button pokes up above the (flex-centered) modal content, so on
|
||||
a tall/near-fullscreen card image it can land right under the Android
|
||||
status bar — same edge-to-edge issue as .topbar, same fix: pad the safe
|
||||
area instead of a fixed inset. Degrades to the base 24px on anything
|
||||
without a system bar to avoid. */
|
||||
padding-top: calc(24px + env(safe-area-inset-top));
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
background: rgba(10, 10, 12, 0.72);
|
||||
backdrop-filter: blur(2px);
|
||||
cursor: zoom-out;
|
||||
@@ -2036,6 +2243,10 @@ tbody tr:hover {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.deck-detail-pane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Surface from `.panel`; layout only here. */
|
||||
.deck-meta {
|
||||
@@ -2205,45 +2416,128 @@ tbody tr:hover {
|
||||
}
|
||||
}
|
||||
|
||||
/* Back button into the deck list — only meaningful once the list and the
|
||||
deck-building pane become separate screens below, so it's hidden outside
|
||||
that breakpoint (and on the tablet stacked layout, where both panes are
|
||||
still visible together). */
|
||||
.deck-back-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Compact: tighten page gutters and collapse the nav rail to an icon strip
|
||||
(same items — labels, wordmark, and the library summary hide; hover/title
|
||||
still names each tab). No separate mobile navigation is introduced. */
|
||||
the left rail's job moves to .bottom-nav below instead, the customary
|
||||
placement on Android). */
|
||||
@media (max-width: 720px) {
|
||||
.content {
|
||||
padding: var(--space-4);
|
||||
/* Clear the fixed bottom nav, or its last row of content is hidden
|
||||
behind it — the safe-area inset is already baked into the nav's own
|
||||
height, so it isn't added again here. */
|
||||
padding-bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom) + var(--space-4));
|
||||
}
|
||||
.nav-rail {
|
||||
width: 60px;
|
||||
padding: var(--space-3) var(--space-2) var(--space-2);
|
||||
}
|
||||
.brand {
|
||||
justify-content: center;
|
||||
padding: var(--space-1) 0 var(--space-4);
|
||||
}
|
||||
.brand-text {
|
||||
display: none;
|
||||
}
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
.nav-item.active::before {
|
||||
left: -8px;
|
||||
}
|
||||
.nav-foot {
|
||||
display: none;
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
}
|
||||
.topbar {
|
||||
padding: 0 var(--space-3);
|
||||
padding-left: var(--space-3);
|
||||
padding-right: var(--space-3);
|
||||
/* padding-top (the safe-area inset) is untouched here — this must not
|
||||
reintroduce the shorthand `padding: 0 ...` that was zeroing it out. */
|
||||
}
|
||||
/* The library path is the first thing to go when horizontal space is tight —
|
||||
the library actions stay reachable. */
|
||||
.path {
|
||||
display: none;
|
||||
}
|
||||
/* Deck view's header (name + Suggest/Export/Sample buttons) and several
|
||||
panels below assume desktop width via `min-width` (280/220px) and a
|
||||
non-wrapping `.row` — those don't fit a phone and forced the whole page
|
||||
to scroll horizontally. Let everything wrap and drop the min-widths
|
||||
instead. */
|
||||
.row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.commander-box,
|
||||
.deck-stats-numbers,
|
||||
.deck-stats-curve {
|
||||
min-width: 0;
|
||||
}
|
||||
/* `.deck-meta` puts the Format selector and the commander search box side
|
||||
by side as flex-wrap row items — with `.commander-box`'s min-width now
|
||||
0, it no longer wraps onto its own line, it just gets squeezed into
|
||||
whatever sliver is left next to "Format", crushing the search results.
|
||||
Stack them instead. */
|
||||
.deck-meta {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
/* Deck list and deck-building pane become separate screens (only one
|
||||
visible at a time) instead of stacking above one another — with many
|
||||
decks, scrolling past the whole list to reach the builder doesn't
|
||||
scale. `.deck-layout`'s show-list/show-detail class is toggled by
|
||||
DeckView in response to picking a deck / the back button. */
|
||||
.deck-back-btn {
|
||||
display: inline-flex;
|
||||
}
|
||||
.deck-layout.show-list .deck-detail-pane {
|
||||
display: none;
|
||||
}
|
||||
.deck-layout.show-detail .deck-sidebar {
|
||||
display: none;
|
||||
}
|
||||
/* The Suggested Cards dialog: `.suggestion-actions`' two nowrap buttons
|
||||
are flex-shrink:0, so they claimed their full fixed width no matter how
|
||||
little room was left, crushing `.suggestion-main` down to almost
|
||||
nothing — the name text then wrapped one word (sometimes one syllable)
|
||||
per line instead of actually gaining any space. `flex-basis: 100%` on
|
||||
`.suggestion-actions` forces it onto its own full-width line below the
|
||||
thumbnail + name/reasons instead of competing with them for the same
|
||||
line. */
|
||||
.dialog-wide {
|
||||
width: 96vw;
|
||||
padding: 14px;
|
||||
}
|
||||
.suggestion-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.suggestion-head {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
}
|
||||
.suggestion-actions {
|
||||
flex-basis: 100%;
|
||||
flex-direction: row;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.suggestion-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
/* Clear the bottom nav the same way .content does, so the undo snackbar
|
||||
doesn't land underneath it. */
|
||||
.undo-bar {
|
||||
bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom) + var(--space-3));
|
||||
}
|
||||
/* The ingest bar's segmented layout assumes room for 4 side-by-side
|
||||
stage labels — on a phone-width screen the tiny-weighted stages
|
||||
("Index", "Indexing") can't fit theirs and it collides with its
|
||||
neighbor's. Drop the per-segment labels (the header message above
|
||||
already names the current stage in more detail) and let that header
|
||||
message wrap instead of ellipsis-truncating, so status text like a
|
||||
download's running byte count stays readable. */
|
||||
.ingest-bar {
|
||||
padding: var(--space-2) var(--space-3) var(--space-3);
|
||||
}
|
||||
.ingest-bar-message {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
.ingest-segment-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch input (future mobile / touchscreen laptops): grow tap targets to a
|
||||
|
||||
Reference in New Issue
Block a user