fix(ui): mobile layout overflows across admin, reader, and book pages

Every fix is a minimal class-level change; desktop layouts are untouched
because each fixed row already fit on one line there.

- progress: CFI string is one unbreakable token — it inflated the page
  layout width to 398-501px (whole-page zoom-out on phones) and rendered
  debug-looking text edge to edge. min-w-0 + break-all lets it wrap.
- audio reader: settings strip min-content (~470px) exceeded the 390px
  viewport, clipping Skip/Sleep and — because the page ICB inflated —
  pushing the chapters drawer (w-full of the viewport) past the screen
  edge so its timestamps clipped too. flex-wrap fixes both symptoms.
- book detail edit-metadata modal footer: Rescan/Reset/Write-to-file row
  overflowed the modal; clipped buttons were unreachable. flex-wrap.
- bookshelf save-filter modal footer: three buttons overflowed both
  screen edges. flex-wrap.
- admin libraries: rows overprinted (badge chips shrink-0 vs actions
  shrink-0, no wrap) — 'manga⟳Scan▸1 folders⌄Manage'. flex-wrap stacks
  actions under the name on phones.
- admin podcasts: maintenance table forced a 892px page (44% zoom).
  overflow-x-auto on the card scrolls the table inside it instead.
- stats: 4-pill range row forced 421px; page permanently ~7% zoomed out.
  flex-wrap.
- podcast feed detail: label+select pairs now travel in their own flex
  pairs so flex-wrap can never split 'Order' from its select; ids and
  htmx change-triggers preserved.
- cosmetics: conflicts stat labels tracking-wide→normal (UNRESOLVED
  clipped), archived file path line-clamp-3, rules-page Smart badge
  wraps cleanly, dashboard View All shrink-0 nowrap (was wrapping to
  two lines).
This commit is contained in:
John O'Keefe committed 2026-11-02 09:30:55 -05:00
1 parent e3ca3426ba
commit 88adf30aa9
24 files changed
+109 -103

No files matched your search

+36 -30
View File
@@ -120,21 +120,25 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
<span>Refresh</span>
</button>
<!-- Order override (D5): per-user; refreshes the episode list -->
<label class="text-sm ml-2" for="podcast-order-select" style="color: var(--text-secondary)">Order</label>
<select
id="podcast-order-select"
name="order"
class="input w-auto py-1.5 cursor-pointer"
hx-put={ "/api/podcasts/feeds/" + data.Feed.ID + "/order" }
hx-target="#episode-list"
hx-swap="outerHTML"
>
<option value="native" selected?={ data.Order == "native" }>Feed order</option>
<option value="reverse" selected?={ data.Order == "reverse" }>Feed order (reversed)</option>
<option value="oldest" selected?={ data.Order == "oldest" }>Oldest first (pub date)</option>
<option value="newest" selected?={ data.Order == "newest" }>Newest first (pub date)</option>
</select>
<!-- Order override (D5): per-user; refreshes the episode list.
Label+select travel as a pair so flex-wrap can never
separate them on narrow screens. -->
<div class="flex items-center gap-1.5">
<label class="text-sm" for="podcast-order-select" style="color: var(--text-secondary)">Order</label>
<select
id="podcast-order-select"
name="order"
class="input w-auto py-1.5 cursor-pointer"
hx-put={ "/api/podcasts/feeds/" + data.Feed.ID + "/order" }
hx-target="#episode-list"
hx-swap="outerHTML"
>
<option value="native" selected?={ data.Order == "native" }>Feed order</option>
<option value="reverse" selected?={ data.Order == "reverse" }>Feed order (reversed)</option>
<option value="oldest" selected?={ data.Order == "oldest" }>Oldest first (pub date)</option>
<option value="newest" selected?={ data.Order == "newest" }>Newest first (pub date)</option>
</select>
</div>
<!-- Serial suggestion (D5, §0 item 3 rename): one-click,
never silent. "reverse" sorts by FEED POSITION DESC —
@@ -152,21 +156,23 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
</button>
}
<!-- Auto-download policy (D6) -->
<label class="text-sm ml-2" for="podcast-autodl-select" style="color: var(--text-secondary)">Auto-download</label>
@PodcastAutoDownloadSelect(data.Feed)
if data.Feed.AutoDownload == "newest" {
<input
type="number"
id="podcast-autodl-count"
name="auto_download_count"
min="1"
max="100"
value={ podcastInt(data.Feed.AutoDownloadCount) }
class="input w-20 py-1.5"
aria-label="Newest N episodes"
/>
}
<!-- Auto-download policy (D6) — label+controls as one pair -->
<div class="flex items-center gap-1.5">
<label class="text-sm" for="podcast-autodl-select" style="color: var(--text-secondary)">Auto-download</label>
@PodcastAutoDownloadSelect(data.Feed)
if data.Feed.AutoDownload == "newest" {
<input
type="number"
id="podcast-autodl-count"
name="auto_download_count"
min="1"
max="100"
value={ podcastInt(data.Feed.AutoDownloadCount) }
class="input w-20 py-1.5"
aria-label="Newest N episodes"
/>
}
</div>
<!-- Unsubscribe (issues #4/#7): the sole subscriber who is
an admin gets the three-choice modal (unsubscribe /