feat(ui): present page-action editors as sheets on mobile
The Add-tags and Add-to-collection editors opened as centered Modals on every form factor, so a mobile user went action-Sheet → centered Modal — an inconsistent hop, and AddTagsSheet's docstring promised a sheet it never got. Introduce AdaptiveDialog, which renders a bottom Sheet under 640px and a centered Modal above it behind one shared contract (open / title / onClose / children / testid). Route both editors through it: - AddToCollectionModal now wraps AdaptiveDialog (both its call sites — manga detail and the reader — adapt automatically). - The reader hosts AddTagsSheet in an AdaptiveDialog instead of a Modal, so its "slots into a Sheet (mobile) or Modal (desktop)" contract is now true. Modal/Sheet already share an API, so the switch is transparent: the dialog keeps the same role, title, testid, and close affordance. Desktop is unchanged. AdaptiveDialog has its own unit test (Sheet on mobile, Modal on desktop); an e2e proves the mobile long-press → Add-tag flow now opens a Sheet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
53
frontend/src/lib/components/AdaptiveDialog.svelte
Normal file
53
frontend/src/lib/components/AdaptiveDialog.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Modal from './Modal.svelte';
|
||||
import Sheet from './Sheet.svelte';
|
||||
|
||||
// One overlay that adapts to the form factor: a bottom Sheet on phones,
|
||||
// a centered Modal on desktop. Both share the same chrome contract
|
||||
// (open / title / onClose / children / testid), so callers stay agnostic
|
||||
// and the page-action editors present consistently with the rest of the
|
||||
// mobile UI (long-press → action sheet → editor sheet, no Modal hop).
|
||||
let {
|
||||
open,
|
||||
title,
|
||||
onClose,
|
||||
children,
|
||||
size = 'md',
|
||||
testid
|
||||
}: {
|
||||
open: boolean;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: Snippet;
|
||||
/** Desktop modal width; ignored by the mobile sheet. */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
testid?: string;
|
||||
} = $props();
|
||||
|
||||
// Bottom sheet under the 640px breakpoint. Guarded on matchMedia rather
|
||||
// than $app/environment so it's SSR-safe (no window) and unit-testable
|
||||
// (stub matchMedia). First paint defaults to the modal; the effect
|
||||
// corrects to a sheet on phones before the dialog is opened.
|
||||
let isMobile = $state(false);
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||
const mql = window.matchMedia('(max-width: 640px)');
|
||||
isMobile = mql.matches;
|
||||
const onChange = (e: MediaQueryListEvent) => {
|
||||
isMobile = e.matches;
|
||||
};
|
||||
mql.addEventListener('change', onChange);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isMobile}
|
||||
<Sheet {open} {title} {onClose} {testid}>
|
||||
{@render children()}
|
||||
</Sheet>
|
||||
{:else}
|
||||
<Modal {open} {title} {onClose} {size} {testid}>
|
||||
{@render children()}
|
||||
</Modal>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user