feat(design): side panels open as a drawer on touch and below 768 (E28 S3f3)
C2 as decided: on a coarse pointer, or a window below 768, the panel's tab (shown at every width, 44 wide on touch) opens the panel as a start-edge drawer over the content, with a scrim. It is ui/Modal's new drawer-start placement, so Esc, Back, the scrim, the focus trap and the scroll lock are the dialog's own. A tap never peeks; pinning is a button in the drawer's header, offered only at 1024 and up; a pin saved on a desktop is left alone below that. A mouse on a wide window keeps hover-to-peek and click-to-pin. DESIGN: C2 as built, rows answer to their own width, phone-only layout deferred to its own epic (principle 2).
This commit is contained in:
@@ -61,19 +61,23 @@ export default function Modal({
|
||||
maxWidth = "max-w-lg",
|
||||
placement = "center",
|
||||
initialFocus,
|
||||
headerActions,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
initialFocus: ModalFocus;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
maxWidth?: string;
|
||||
/** `drawer` slides in from the right edge and runs full height; `top` pins the panel near the top
|
||||
/** `drawer` slides in from the right edge and runs full height (`drawer-start`: from the left, where
|
||||
* a side panel lives); `top` pins the panel near the top
|
||||
* of the viewport (where a search-first dialog like the command palette belongs, so the list grows
|
||||
* downwards instead of the whole thing jumping as results change). Everything else about the shell
|
||||
* — the layer stack, ESC, the focus trap, Back-to-close, the scroll lock — is identical. A
|
||||
* placement is not a second dialog implementation: duplicating that behaviour is exactly how two
|
||||
* shells drift apart. */
|
||||
placement?: "center" | "drawer" | "top";
|
||||
placement?: "center" | "drawer" | "drawer-start" | "top";
|
||||
/** Controls beside the ✕ in the header (a side panel's pin and customize, C2). */
|
||||
headerActions?: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// Browser/mouse Back closes the topmost modal instead of navigating away.
|
||||
@@ -150,6 +154,7 @@ export default function Modal({
|
||||
}, []);
|
||||
|
||||
const drawer = placement === "drawer";
|
||||
const drawerStart = placement === "drawer-start";
|
||||
const top = placement === "top";
|
||||
// Only on a server render, where the hook has no `document` to build a host from.
|
||||
if (!host) return null;
|
||||
@@ -159,9 +164,11 @@ export default function Modal({
|
||||
className={`fixed inset-0 z-overlay flex bg-black/70 overlay-backdrop ${
|
||||
drawer
|
||||
? "justify-end"
|
||||
: top
|
||||
? "items-start justify-center p-4 pt-[8dvh] sm:p-6 sm:pt-[10dvh]"
|
||||
: "items-center justify-center p-4 sm:p-6"
|
||||
: drawerStart
|
||||
? "justify-start"
|
||||
: top
|
||||
? "items-start justify-center p-4 pt-[8dvh] sm:p-6 sm:pt-[10dvh]"
|
||||
: "items-center justify-center p-4 sm:p-6"
|
||||
}`}
|
||||
onMouseDown={(e) => {
|
||||
pressedOnBackdrop.current = e.target === e.currentTarget;
|
||||
@@ -178,8 +185,10 @@ export default function Modal({
|
||||
tabIndex={-1}
|
||||
className={
|
||||
drawer
|
||||
? `${SURFACE.drawer} ${PANEL} w-full ${maxWidth} h-full rounded-none sm:rounded-l-2xl overlay-drawer`
|
||||
: `${SURFACE.panel} ${PANEL} w-full ${maxWidth} max-h-full rounded-2xl`
|
||||
? `${SURFACE.drawer} ${PANEL} w-full ${maxWidth} h-full overlay-drawer`
|
||||
: drawerStart
|
||||
? `${SURFACE.drawerStart} ${PANEL} w-full ${maxWidth} h-full overlay-drawer-start`
|
||||
: `${SURFACE.panel} ${PANEL} w-full ${maxWidth} max-h-full`
|
||||
}
|
||||
>
|
||||
{/* C4: the header and the footer stay put and only the body scrolls, with the B9 fade at
|
||||
@@ -189,6 +198,9 @@ export default function Modal({
|
||||
<h2 id={titleId} className="text-lg font-semibold leading-snug">
|
||||
{title}
|
||||
</h2>
|
||||
{headerActions && (
|
||||
<div className="-my-1 ml-auto flex shrink-0 items-center gap-1">{headerActions}</div>
|
||||
)}
|
||||
<IconButton
|
||||
ref={closeRef}
|
||||
label={t("common.close")}
|
||||
|
||||
Reference in New Issue
Block a user