feat(mobile): responsive UI overhaul and PWA shell#15
Open
AshDevFr wants to merge 13 commits into
Open
Conversation
Deploying codex with
|
| Latest commit: |
5552439
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5de21ab4.codex-asm.pages.dev |
| Branch Preview URL: | https://mobile-support.codex-asm.pages.dev |
…rflow fixes Lays the groundwork for genuine mobile usability before tackling per-surface polish in follow-up work. - Add an `xs` breakpoint at 30.125em (~482px) to the Mantine theme as a phone-only line, while keeping `sm`/`md`/`lg`/`xl` at Mantine defaults so existing `visibleFrom="sm"` sites are unaffected. - Auto-close the mobile sidebar drawer on navigation. `Sidebar` now accepts an `onNavigate` prop wired from `AppLayout`'s `closeMobile`; every navigable `NavLink` plus Logout calls it. The Settings parent toggle intentionally doesn't, so opening the submenu doesn't collapse the drawer. - Global CSS for long-string overflow: `overflow-wrap: anywhere` on Title, Breadcrumbs, and Anchor surfaces, plus an override of Mantine's `white-space: nowrap` on breadcrumb items so long filenames wrap inside the row instead of pushing the layout wider than the viewport. - Phone-only typography scale: Title `data-order` 1–3 shrink via overrides of Mantine's `--title-fz` CSS variable, avoiding 10-line wrapped headings on 390px screens. - Touch-target helpers: a `.touch-target` opt-in class enforces 44×44 below `xs`, and the Header `Burger` is bumped from `sm` to `md` with a state-reflecting `aria-label`. Mantine doesn't accept responsive object literals for `Title fz` or `ActionIcon size`, so the global CSS approach substitutes for the originally planned theme component defaults. - Tighten the Book Detail FILE row (`flexShrink: 0` on label, `overflowWrap: anywhere` + `minWidth: 0` on value) so long filenames don't blow out the page on phones. Tests added for the auto-close behavior covering positive cases (Home link, Settings submenu link) and the negative case (Settings toggle).
Restores the ability to search from the mobile UI. Below 482px the header renders a search ActionIcon that opens a top-anchored Drawer with an auto-focused input and grouped Series/Books results. Selecting a result navigates and closes the sheet; pressing Enter on a 2+ char query routes to the full search page. SearchInput's visibleFrom moves from sm to xs, so tablet portrait (482-767px) keeps the inline combobox rather than degrading to icon only. A new SearchResultItem module factors out SeriesResultContent and BookResultContent so the desktop Combobox.Option rows and the mobile UnstyledButton rows render identically and can't drift. The mobile sheet intentionally does not nest a Mantine Combobox inside the Drawer (portal and focus-trap conflicts), and arrow-key navigation is not useful on touch. Includes unit tests for the new sheet's open/close, query gating, result selection, Enter routing, loading and empty states.
Below the xs breakpoint, ReaderToolbar collapses fit mode, page layout, prev/next book, and fullscreen into a single overflow Menu, keeping only close, title, settings, and the menu trigger in the top bar. Touch targets bumped to size "xl" and toolbar padding now respects env(safe-area-inset-*) for installed-PWA standalone mode. Introduces MobileReaderBottomBar (mounted by ComicReader and PdfReader, self-gated on the xs breakpoint) that restores prev/next/page-count/ slider as a sticky bottom strip. Page-count tap opens a jump-to-page modal with a numeric input clamped to the page range, useful when the slider thumb is too narrow for precise long jumps on a phone viewport. EPUB reader surfaces its TOC, bookmarks, and search through a new mobileMenuItems toolbar slot. The EPUB drawer bodies stay mounted on mobile (display:none on their wrapper) so the overflow-menu items can still toggle them via their portaled content. ComicReader pinch-zoom posture now respects the active fit mode: the "original" mode allows native pinch-zoom while scaling modes use "manipulation" so the gesture does not fight the fit logic. PdfReader always allows pinch-zoom since small PDF text commonly needs it. The PdfReader search bar width is clamped to never overflow narrow viewports. Includes tests for the new bottom bar and the toolbar's mobile mode (matchMedia override per test). Real-device iPhone verification is deferred and tracked in the implementation plan's known limitations.
Introduce a shared <ResponsiveTable> primitive (web/src/components/ui) that renders a Mantine Table above the xs breakpoint and a stack of label/value Cards below it. Also export MOBILE_MEDIA_QUERY so callers that don't fit the data-driven model can switch trees with useMediaQuery at the same breakpoint. Migrate every admin Table on the frontend so phones can manage settings without horizontal clipping: - Direct ResponsiveTable ports: UsersSettings, PluginStorageSettings, SharingTagsSettings, DuplicatesSettings (inner book table per group), SeriesExportsSettings, TasksSettings (both task list and by-type stats), and the ServerSettings change-history modal. - Refactor ReleaseTrackingSettings: split the row component into pure SourceCell / PluginCell / LastPollCell / StatusCell and a stateful CronCell so the column-based model fits. - useMediaQuery splits where colSpan-expandable rows or stateful inline editors don't fit the data-driven model: PluginsSettings, the two MetricsSettings tables (with Collapse-based details in the mobile cards), the ServerSettings per-category settings table, and the shared ReleasesTable (which also gives the Series Releases panel the same mobile treatment). Includes tests for the new component. All existing settings/releases tests still pass.
Make the library page usable on a 390px viewport — fix the two outstanding audit findings without changing the desktop layout. LibraryToolbar: below the shared MOBILE_MEDIA_QUERY breakpoint, switch from a single `<Group justify="space-between">` row to a `<Stack>` with the tabs on top and the page-size/sort/filter icon controls right-aligned underneath. Keeps every control one tap away rather than hiding them behind an overflow menu. AlphabetFilter: below the same breakpoint, render a Mantine `<Select>` with "All series", "#", and A–Z options (including counts and disabled state from the alphabetical-groups endpoint) instead of the 28-button A–Z strip that gets clipped off the right with no scroll indicator. Tests cover both render modes; reuses the matchMedia override pattern from ResponsiveTable and MobileReaderBottomBar so the breakpoint stays consistent.
…update prompts Makes Codex installable to the home screen on iOS and Android with a proper icon set, theme color, and standalone display mode, and caches the app shell for fast cold loads in standalone mode. - Add vite-plugin-pwa configured with generateSW; precaches the JS/CSS/HTML shell, NetworkFirst for /api/* with a 5s timeout and 5min TTL, CacheFirst for fonts/images with a 30-day TTL, navigateFallback wired to index.html with backend routes (/api, /opds, /komga, /docs, /health) on the denylist. clientsClaim: false + skipWaiting: false so updates only apply after the user confirms a reload. - Hand-author web/public/manifest.webmanifest with name, theme_color matching the dark step of the primary palette, dark splash background, and 192/512 any + 192/512 maskable icons (plus a 180px apple-touch-icon) generated from the existing logo SVG. - Wire iOS / PWA meta tags into index.html: viewport-fit=cover, theme-color, manifest link, apple-touch-icon, apple-mobile-web-app-capable, and apple-mobile-web-app-status-bar-style=black-translucent so the reader can claim the notch in fullscreen mode. - New PwaUpdatePrompt component using useRegisterSW from virtual:pwa-register/react; shows a sticky Mantine notification with Reload / Later actions when a new SW is waiting. Registration is mounted only under import.meta.env.PROD so MSW continues to own the dev SW slot. - New InstallPrompt component handling Chromium/Android (beforeinstallprompt → Install button), iOS Safari (UA detection including the MacIntel-iPad fingerprint → "Show me how" modal with Safari Share-sheet instructions), and standalone/recently-dismissed (renders nothing). Dismissal persists for 30 days in localStorage. Includes unit tests covering all three paths. - Add a @media (display-mode: standalone) rule in index.css that pads the AppShell header, navbar, and main content by env(safe-area-inset-*) so installed-mode chrome respects the iOS notch and home indicator. The reader's own toolbar and bottom bar keep their independent safe-area logic so fullscreen reading continues to reclaim the notch.
Close the gaps the post-merge mobile audit flagged in the EPUB and PDF
readers so touch users can actually reach the toolbar and read PDFs at a
legible default zoom.
- EPUB tap-to-toggle toolbar: wire useTouchNav onto the outer container
and add a complementary rendition.on("click", ...) handler so taps
inside the epub.js iframe (which never bubble across the iframe
boundary) also toggle the toolbar. Skip clicks on links and form
controls to avoid double-trigger. Outer-container swipes call
rendition.next/prev directly.
- PDF mobile default zoom: read the mobile media query synchronously in
the zoomLevel initializer (via { getInitialValueInEffect: false }) and
default to fit-width on phones instead of fit-page, which previously
squashed portrait PDFs to ~33% width.
- EPUB side arrows: react-reader renders the chevrons as inline-styled
buttons with no class hook, so hide them via the existing
readerStyles.arrow override when on mobile.
Tests cover the touch hook wiring, the rendition click handler (including
link-target suppression), the arrow display override on mobile vs
default, and the PDF zoom default flipping with the viewport.
Address the codebase-cleanup items the mobile audit surfaced: - Delete web/src/App.css. The file was unimported anywhere in the workspace and consisted of leftover create-vite template rules. - Strip the Vite starter defaults from web/src/index.css (the :root color/font block, a / a:hover, the template h1 / button rules, and the @media (prefers-color-scheme: light) overrides). Keep the body reset, the #root height rule, the focus-visible outline reset, and the PWA standalone safe-area + Mantine theming blocks that the app actually depends on. - Add `?? []` guards inside ReleasesInbox's buildSeriesOptions / buildLibraryOptions / buildLanguageOptions so a facets response missing the series, libraries, or languages array no longer crashes the page on first paint. Export the helpers and unit-test the partial-response paths. - Add web/src/mocks/handlers/releases.ts covering release-sources, releases, releases/facets, the per-series listing, and the per-row and bulk writes the inbox invokes, then wire it into the handler index. Mock-mode /releases now renders representative data instead of crashing on a missing handler. Type-check, lint, and the full frontend test suite are clean; the production build still passes.
- Header: add aria-label to the theme toggle so the three mobile-header buttons (burger, search, theme) all expose accessible names. - GenreTagChips: thread an optional per-group badge variant through BadgeGroup and switch tags to variant="outline". The default "light + gray" combo resolves to a near-transparent surface in dark mode, which the audit flagged as "near-invisible". Outline keeps a visible border in both themes while preserving the GENRES-primary / TAGS-secondary hierarchy. - PluginStatusBanner: stack the "View Plugins" anchor under the message below the xs breakpoint so it stops crowding the alert close button. Reuses MOBILE_MEDIA_QUERY so the gate stays aligned with the rest of the mobile responsive layer. - HorizontalCarousel: hide the < > chevron group below xs and rely on native horizontal scroll + swipe. Single change covers every Home strip (Keep Reading, Recently Added Books/Series, Recommended, etc.). Tests added for the new GenreTagChips variant behavior; full frontend suite, type-check, lint, and build all clean.
…B iframe swipe)
Three real-iPhone bugs that the desktop and Chrome-emulation audit didn't
catch.
* Replace `100vh` with `100dvh` on reader root containers so the iOS
Safari URL bar stops clipping the mobile bottom toolbar. Per-image
`maxHeight: 100vh` in the continuous-scroll readers and the EPUB
drawer-body `calc(100vh - ...)` heights are intentional large-viewport
sizes and stay unchanged.
* Rework `useTouchNav` on Pointer Events so a single code path handles
finger taps/swipes and mouse-drag in Chrome's mobile-viewport emulation
without needing Sensors > Touch. Primary-pointer and pointer-cancel
edge cases handled; public API is preserved so reader call sites are
untouched.
* Bind tap/swipe pointer listeners inside the EPUB iframe via
`rendition.hooks.content`, since touches that land on chapter text
don't bubble out across the iframe boundary. Reading direction
honours the OPF's `page-progression-direction` first, then the user
setting. The previous `rendition.on("click")` becomes redundant and
is removed.
Gesture classification is extracted into a shared `classifySwipe`
helper used by both the outer-container hook and the EPUB iframe
listener.
Tests added for the gesture classifier, the pointer-event variant of
`useTouchNav`, and the EPUB iframe pointer hook.
Address the remaining UX gaps from the mobile audit. Each change is small and independent but they share a common theme: making mobile state more discoverable and dismissals more durable. - Add a one-time first-run hint shown in the reader on phones, teaching users that a center tap reveals the toolbar. Once per browser session via sessionStorage; auto-fades after a few seconds or on tap. Mounted by all three reader formats so the copy lives in one place. - Add an EPUB chapter pill to the mobile reader bottom bar. EPUB pagination is reflowable, so the bar drops the slider for an EPUB layout (prev / "Ch N / total" / next) where tapping the pill opens the existing TOC drawer. The chapter index is derived by matching the current href against the top-level TOC. - Add a bottom fade cue to the mobile sidebar when the navbar overflows (e.g. Settings expanded) and the user isn't scrolled to the bottom. Driven by a scroll listener plus a ResizeObserver so it updates as the Settings group toggles. - Switch the plugin-failure banner from session-scoped dismissals to a localStorage map keyed by failureCount at dismissal time. The banner resurfaces when a plugin's failureCount exceeds the stored value, i.e. on a new failure rather than every reload. - Add an OfflineBanner that listens to window online/offline events and renders a thin alert at the top of AppShell.Main when the browser reports the user is offline. Tests added for each component.
Two handlers were firing on every tap: `useTouchNav.onTap` toggled the
toolbar globally while each reader surface also ran its own per-zone
React `onClick`. The net effect was that center taps double-toggled
(no visible change) and edge taps both navigated and flickered the
toolbar. Double-page mode had no center zone at all, so middle taps
navigated. TTB used horizontal zones, ignoring the reading axis.
Move zone classification into a single shared helper and let
`useTouchNav` own tap dispatch:
- `classifyTapZone(x, y, w, h, {readingDirection})` splits the surface
into thirds along the reading axis. LTR/RTL use horizontal thirds
(with RTL flipping prev/next polarity); TTB and webtoon use vertical
thirds (top → prev, bottom → next). The middle third always returns
`center`.
- `useTouchNav` calls `onPrevPage` / `onTap` / `onNextPage` based on
the tap location, with a `tapZones: false` opt-out for surfaces
that should treat every tap as a toolbar toggle.
- Drop the duplicate page-level `onClick` zone handlers from
`ComicReaderPage`, `DoublePageSpread`, and `PdfReader`. The EPUB
inside-iframe pointer handler now uses the same classifier against
the iframe's viewport.
Continuous-scroll modes (CBZ continuous, webtoon, PDF continuous) keep
their existing behavior.
Added unit tests for `classifyTapZone`, zone-dispatch tests in
`useTouchNav` covering LTR/RTL/TTB and the `tapZones: false` opt-out,
and an EPUB iframe zone test. Stale per-zone tests on the page
components were removed since the behavior is centralized now.
Two real-iPhone regressions that desktop Chrome emulation didn't catch. * touchAction "manipulation" (ComicReader) and "pan-x pan-y pinch-zoom" (PdfReader) let iOS WebKit claim horizontal pans for its own scroll/back-navigation gesture, firing pointercancel mid-swipe before our handler could classify it. Drop pan-x (keep pan-y for tall fit-width pages and pinch-zoom for detail) so horizontal swipes flow through to useTouchNav. * ReaderToolbar and MobileReaderBottomBar pad themselves with safe-area-inset to clear the iOS notch and home indicator. In PWA standalone mode those insets push the bars well past the visible icons, and the bars' transparent gradient absorbed side taps the user intended for the page underneath. Set pointer-events: none on the outer Box and re-enable it on the actual controls so the gradient is purely visual. * As a defensive fallback, treat pointercancel after a swipe-sized movement the same as pointerup so any remaining iOS gesture interception still navigates rather than silently discarding the swipe. Taps with no movement stay discarded since a canceled tap usually means the browser took the press for something else. Tests added for the cancel-as-swipe behavior and the pointerup-after- cancel no-double-fire case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the Codex frontend usable on a phone (390px viewport) and ships a PWA shell so it installs to the home screen. Frontend-only; no backend or schema changes. Full plan and progress log: tmp/implementation/planned/mobile-support.md. Source audit: tmp/impl/mobile-audit.md.
Six phases delivered in order:
Phase 1: Foundation
xsbreakpoint at 482px intheme.ts(Mantine's defaultsm/md/lg/xluntouched).Sidebarnow acceptsonNavigate?: () => voidandAppLayoutthreadscloseMobilefromuseDisclosureinto it.overflow-wrap: anywherefor.mantine-Title-root,.mantine-Breadcrumbs-root,.mantine-Anchor-rootso long filenames stop pushing layouts past viewport.[data-order]CSS, plus a.touch-targetopt-in utility class enforcing 44×44 min on touch elements.sm(18px) tomd(28px) with anaria-labelreflecting drawer state.Phase 2: Mobile search affordance
SearchInputmigrated fromvisibleFrom="sm"tovisibleFrom="xs"; tablet portrait keeps the desktop combobox.MobileSearchSheetrenders a full-heightDrawer position="top"with auto-focused input, Series/Books grouped results (max 5 each), loading and empty states, and "See all results" routing to/search?q=.ActionIconwithhiddenFrom="xs"that opens the sheet.SearchResultItemmodule extractsSeriesResultContentandBookResultContentso the desktop combobox and the mobile sheet share rendering (~55 lines of duplicated JSX removed).Phase 3: Reader mobile polish
ReaderToolbarcollapses secondary actions (prev-book, next-book, fit-mode, page-layout) into an overflowMenubelowxs; keeps close, title, page count, and primary settings visible.MobileReaderBottomBarslide-up control strip for prev / page-count / next / slider belowxs.xlbelowxs; slider thumb increased for touch.touch-actionreviewed per fit mode so pinch-zoom is allowed where users want it (notablyoriginal).Phase 4: Tables to cards (ResponsiveTable + admin port)
ResponsiveTable<T>component. Abovexs: classic Mantine<Table>(existing styling and sort hooks preserved). Belowxs: stacked label-value cards with row actions in a footer.Phase 5: Library page polish
xsso tabs stay on one row.Phase 6: PWA shell
vite-plugin-pwaconfigured invite.config.ts.manifest.webmanifestwith name, theme color, background color,display: standalone, scope,start_url, and a 192 / 512 / 192-maskable / 512-maskable icon set generated fromcodex-logo-color.svg.index.html:apple-touch-icon,apple-mobile-web-app-capable,apple-mobile-web-app-status-bar-style,theme-color,viewport-fit=cover.import.meta.env.PRODto avoid colliding with MSW'smockServiceWorker.jsin dev. Update-available toast prompts a reload./api/*, StaleWhileRevalidate for static assets, CacheFirst for icons/fonts. No write-request background sync in v1.beforeinstallprompton iOS).Key decisions
xs = 482px,smstays at 768px. Adds precision without retroactively changing every existingvisibleFrom="sm"site. ThevisibleFrom/hiddenFromaudit (in the plan) showed onlySearchInputneeded migration; 8 other call sites stay onsm.ResponsiveTablerather than refactoring each admin page independently. Settings-row variant covers the bulk of admin layouts.