136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
import { useEffect, type ReactNode } from 'react';
|
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
import { Text } from '@astryxdesign/core/Text';
|
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
import { X } from 'lucide-react';
|
|
|
|
/**
|
|
* The side drawer, matched to `OrderDetailDrawer`.
|
|
*
|
|
* That one is the site's drawer and every measurement here is taken from it:
|
|
* the flex-end wrapper at z-index 70, a full-bleed scrim BUTTON rather than a
|
|
* div (so it is reachable and announces itself), a 460px panel capped at 92vw,
|
|
* `-24px 0 56px -12px` of shadow, a STICKY header on a hairline, and a 30px
|
|
* rounded close button on `--color-surface-sunken`.
|
|
*
|
|
* Extracted rather than copied a third time. The drawers I added had drifted —
|
|
* different scrim opacity, different shadow, a bare `X`, a header that scrolled
|
|
* away — which is exactly how two screens stop looking like one product.
|
|
*/
|
|
export interface DrawerProps {
|
|
title: string;
|
|
/** One line under the title. */
|
|
subtitle?: string;
|
|
/** Sits beside the title — a status chip, usually. */
|
|
titleAside?: ReactNode;
|
|
/**
|
|
* Panel width in px. 460 is the site default; only widen for content that
|
|
* genuinely cannot fold, such as a grid of product cards.
|
|
*/
|
|
width?: number;
|
|
/** Renders the body unpadded, for panels that manage their own layout. */
|
|
isBare?: boolean;
|
|
onClose: () => void;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function Drawer({
|
|
title,
|
|
subtitle,
|
|
titleAside,
|
|
width = 460,
|
|
isBare,
|
|
onClose,
|
|
children,
|
|
}: DrawerProps) {
|
|
useEffect(() => {
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={title}
|
|
style={{ position: 'fixed', inset: 0, zIndex: 70, display: 'flex', justifyContent: 'flex-end' }}
|
|
>
|
|
<button
|
|
type="button"
|
|
aria-label="Close"
|
|
onClick={onClose}
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
border: 0,
|
|
background: 'rgb(15 23 42 / .32)',
|
|
cursor: 'pointer',
|
|
}}
|
|
/>
|
|
<div
|
|
className="dialog-panel"
|
|
style={{
|
|
position: 'relative',
|
|
width,
|
|
maxWidth: '92vw',
|
|
height: '100%',
|
|
overflowY: 'auto',
|
|
background: 'var(--color-surface)',
|
|
borderLeft: '1px solid var(--color-line)',
|
|
boxShadow: '-24px 0 56px -12px rgb(15 23 42 / .18)',
|
|
}}
|
|
>
|
|
<HStack
|
|
justify="between"
|
|
align="start"
|
|
gap={1}
|
|
style={{
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 1,
|
|
padding: 20,
|
|
background: 'var(--color-surface)',
|
|
borderBottom: '1px solid var(--color-line)',
|
|
}}
|
|
>
|
|
<VStack gap={0.5} style={{ minWidth: 0 }}>
|
|
<Text type="label" size="lg" weight="semibold">
|
|
{title}
|
|
</Text>
|
|
{subtitle ? (
|
|
<Text type="body" size="sm" color="secondary">
|
|
{subtitle}
|
|
</Text>
|
|
) : null}
|
|
{titleAside}
|
|
</VStack>
|
|
<button
|
|
type="button"
|
|
aria-label="Close"
|
|
onClick={onClose}
|
|
style={{
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 10,
|
|
border: 0,
|
|
flex: 'none',
|
|
display: 'grid',
|
|
placeItems: 'center',
|
|
background: 'var(--color-surface-sunken)',
|
|
color: 'var(--color-ink-3)',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<X size={15} />
|
|
</button>
|
|
</HStack>
|
|
|
|
{isBare ? children : <VStack gap={3} padding={2}>{children}</VStack>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|