Files
Mangalord/frontend/src/lib/components/AdaptiveDialog.svelte
MechaCat02 e9331747d0 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>
2026-06-25 21:27:58 +02:00

54 lines
1.8 KiB
Svelte

<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}