feat(mobile): responsive UI overhaul and PWA shell#15
Open
AshDevFr wants to merge 20 commits into
Open
Conversation
Deploying codex with
|
| Latest commit: |
8f25b7f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2754ad65.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.
… IDB layer Move the service worker from vite-plugin-pwa's generateSW mode to injectManifest with a hand-written src/sw.ts so it can host a per-book CacheFirst route, gated on a downloaded-id set hydrated from IndexedDB at boot and kept in sync via BroadcastChannel. The previous runtime caching (NetworkFirst /api/* with a 5s timeout, CacheFirst for fonts and images, navigation fallback with the API/OPDS/Komga/docs/health denylist) is reimplemented in the new SW; cache names, TTLs, and the manual SKIP_WAITING update flow are unchanged so PwaUpdatePrompt continues to work. Add a hand-rolled IndexedDB access layer at src/lib/offline/db.ts with two stores: downloads (per-book metadata, keyed by book id) and outbox (auto-incremented queue for reading-progress mutations that fail offline). Drain is sequential and stops at the first failure with the record's retryCount bumped, so subsequent online events resume cleanly without reordering writes to the same book. Extract the URL matcher into src/lib/offline/routeMatcher.ts as a pure function so the route predicate and per-book cache-name helper can be unit-tested without bootstrapping a SW. Tests added for both modules; fake-indexeddb is added as a dev dep so jsdom can run them. No user-visible change yet: the downloaded-id set stays empty until the first download writes to IDB. dist/sw.js replaces the previously generated SW; precache size matches the prior baseline.
Add downloadSingleFileBook to the page-side offline module. It fetches
/api/v1/books/{id}/file, streams the body via ReadableStream.getReader
so progress can be reported against Content-Length, assembles the
chunks into a fresh Response (preserving Content-Type and the other
upstream headers), and stores it under the codex-book-{id} Cache
Storage entry that the service worker's per-book route already
intercepts. The IDB metadata row flips from "downloading" to "complete"
in two writes, broadcast on both edges so the SW's in-memory
downloaded-id set picks the change up immediately and starts serving
the cached response.
Cancellation via AbortSignal deletes the IDB row and the per-book cache
so a retry starts from a clean slate. Other failures (fetch throwing,
non-OK response, mid-stream error) keep the row but flip it to "error"
with the message preserved for the future downloads page to surface.
fetch and CacheStorage are both injectable via the options bag so unit
tests can drive a fake Cache (jsdom does not ship the Cache API). The
cached body is built as a Uint8Array rather than a Blob since jsdom's
Response constructor stringifies Blob inputs; a Uint8Array body works
identically in both environments. Tests cover the success path,
progress reporting (including missing Content-Length), the three error
paths, and abort cleanup.
Wire a per-book DownloadButton onto the BookDetail action row that hydrates from IndexedDB on mount, subscribes to the downloads BroadcastChannel for cross-tab updates, and on click drives downloadSingleFileBook with its own AbortController. The button cycles through five visible states: a brief loading placeholder (with a distinct aria-label so accessible queries do not snag it before hydration finishes), the not-downloaded trigger, a downloading state with a RingProgress and a cancel control, the downloaded state that opens a Menu with Re-download and Remove offline copy, and an error state that retries on click. Unsupported formats render null so the BookDetail row stays uncluttered until per-page comic downloads land. The series-level "Download series" affordance is held back until the queue infrastructure ships. Hydration uses a functional setState that bails out if the user has already started a local action during the async IDB read, so a fast click cannot be clobbered by hydration's late completion. The remove flow deletes the IDB row before evicting the per-book cache so the service-worker route stops matching as soon as possible. Tests cover format support, hydration of each IDB state, the download trigger with progress forwarding, abort, remove, and cross-tab broadcasts. First end-to-end user-visible slice: tapping the icon on a BookDetail for an EPUB or PDF, refreshing the page with the network off, and reading the book now works through the UI.
Add downloadComicBook to the offline manager. It spawns a bounded
worker pool (default concurrency 5) that pulls page numbers from a
shared counter, fetches /api/v1/books/{id}/pages/{n}, and stashes
each response in the per-book Cache Storage entry the service worker
already intercepts. Progress is reported as pages-done over total
page count so the existing RingProgress can show a real percentage
without an extra HEAD roundtrip per page; bytes are still tracked
and stored in the IDB row for the future downloads page to surface.
Failure on any page aborts the in-flight siblings via an internal
AbortController, flips the IDB row to status "error" with the
offending page in the message ("HTTP 404 fetching page 3 of book X"),
and deletes the entire per-book cache. Partial caches are useless
for comic reading, so cleaning the whole cache on failure is both
correct and simpler than tracking partial entries. The caller's
signal is composed alongside the internal one, so an external cancel
takes the dedicated cleanup branch (delete IDB row + delete cache +
throw AbortError) for a clean retry.
The DownloadButton now accepts an optional pageCount and dispatches
by format: epub and pdf still call downloadSingleFileBook, cbz and
cbr call the new downloadComicBook. BookDetail passes book.pageCount
so all four formats are downloadable from the action row.
The IDB DownloadFormat union tightened from "comic" | "epub" | "pdf"
to "cbr" | "cbz" | "epub" | "pdf" so the downloaded record preserves
the exact format the user is storing. Existing single-file callsites
unchanged. Tests cover success, concurrency cap, monotonic progress,
pageCount validation, 404-mid-download cleanup, early-exit after
first failure, cancellation, and the new comic dispatch in
DownloadButton.
…stence New /settings/downloads page lists every book currently saved offline on this device, with format, status, size, last-read distance, per-book remove, and a Clear-all that asks for confirmation. The header card shows navigator.storage.estimate() usage against quota with a colored progress bar, plus a durability indicator that explains the navigator.storage.persist() result in plain text: persistent (green), not marked persistent with an install-to-home-screen hint (orange), or unknown in this browser (dimmed). The page subscribes to the codex:downloads BroadcastChannel so the list refreshes when a download completes or another tab edits the store. Desktop renders the list as a Mantine Table; below the shared mobile breakpoint it switches to a Card stack laid out for narrow viewports. Wire requestStoragePersistence into the download manager so it fires once per session after the first successful download. The result is cached and exposed via getStoragePersistence so the page can render the indicator without re-prompting the browser. Concurrent calls deduplicate via an in-flight promise; a missing StorageManager API returns null silently rather than throwing. The page is added to the non-admin section of the sidebar (Offline Downloads, IconCloudDownload) since this is device-local data, not server admin. Route mounted under the existing ProtectedRoute / AppLayout pattern in App.tsx. Tests cover the persist request in isolation (called once, cached, null on missing API, false on throw, dedup, fires on first successful download) and the settings page (empty state, list rendering with totals, quota meter, indicator copy for granted and denied, per-book remove, clear-all with confirmation, cancel keeps data, broadcast picks up new downloads).
…n reconnect Add an offline-write outbox so reading-progress updates made without a network connection are persisted locally and delivered when the browser comes back online, instead of being silently dropped. The new lib/offline/outbox module exposes four primitives: isOfflineError recognises both the project's ApiError "Network Error" shape and raw axios codes (and treats opaque errors as offline when navigator.onLine is false), enqueueOfflineWrite normalises the request and persists it into the existing IDB outbox store, drainOfflineOutbox replays records sequentially with in-flight dedupe so concurrent triggers don't start overlapping drains, and installOutboxDrainListeners wires the window `online` and document `visibilitychange` events idempotently. readProgressApi.update, updateProgression, and delete are now wrapped in try/catch: on network failure they capture the current JWT into headers, enqueue the request descriptor, and throw an OfflineQueuedError so callers can distinguish "saved locally" from a real failure. Server errors still rethrow unchanged. The default drain sender uses plain fetch with credentials: "include" so cookie-based auth keeps working without dragging axios into the outbox module, and sequential drain order is preserved end-to-end so a later page write cannot be overwritten by an earlier queued one. useReadProgress and useEpubProgress now check isOfflineQueuedError in their existing catch handlers and short-circuit without logging, keeping the offline path quiet. main.tsx installs the drain listeners once during bootstrap so every drain trigger is wired by the time any reader mounts. Tests cover the offline-error heuristic, enqueue/drain semantics, sequential ordering with failure-stops-and-bumps-retry-count, in-flight dedupe, idempotent listener install, automatic drains on online and visibilitychange, plus the new offline path through each readProgressApi method.
…tch expansion, docs Wrap the per-book download functions in an in-process queue so a "Download series" action can fan out across every book in a series with sequential concurrency by default, per-book and queue-wide cancel, and a typed QuotaExceededError raised from a pre-flight check before any IDB row is written. The series-detail Modal renders idle / preflight-error / running / done phases and keeps a compact aggregate badge visible when the user navigates away mid-queue. Gate the first download tap in an iOS Safari tab behind a soft modal that explains the ~7-day eviction risk and the Add-to-Home-Screen flow. Dismissal persists for 30 days under codex-offline-install-nudge-dismissed so subsequent taps skip the modal. Applies to both the per-book button and the series download button. Does not block: "Continue anyway" always proceeds with the download. Floor the comic reader's prefetch window at 5 pages when the book is not downloaded (so cellular readers get a responsive next-page tap regardless of the preloadPages setting) and at 10 pages when the book is in the SW cache (cache hits are free; prime the image decoder). ComicReader hydrates the downloaded flag from IDB and listens on the downloads broadcast channel so the window stays accurate if the user removes or re-downloads the book from another tab. Add a Docusaurus page covering single-book and series downloads, the storage management surface, reading-progress sync semantics, the per-platform durability matrix with the iOS Safari caveat called out, home-screen install instructions, and a troubleshooting section. Tests added across the queue, button, install nudge, and prefetch helper.
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.