Files
daily_console_web/src/features/store-admin/Drawer.tsx
2026-08-25 18:00:16 +05:30

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