drawer and upload
This commit is contained in:
@@ -1,35 +1,51 @@
|
||||
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';
|
||||
import './drawer.css';
|
||||
|
||||
/**
|
||||
* The side drawer, matched to `OrderDetailDrawer`.
|
||||
* The console's side drawer. Every drawer in the product is this one.
|
||||
*
|
||||
* 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`.
|
||||
* ── Why the panel is a flex column ──────────────────────────────────────────
|
||||
*
|
||||
* 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.
|
||||
* It used to be a single scrolling box with a sticky header. That cannot hold a
|
||||
* footer: an action bar inside a scrolling container scrolls away with the
|
||||
* content, which is exactly wrong for the button you opened the drawer to
|
||||
* press. Header, scrolling body and action bar are now three rows of a column,
|
||||
* so only the middle one moves.
|
||||
*
|
||||
* ── What belongs here and what does not ─────────────────────────────────────
|
||||
*
|
||||
* The shell owns the chrome — scrim, panel, header, close, footer — and nothing
|
||||
* else. Content comes from `drawerKit`. A drawer that styles its own card or
|
||||
* badge is the beginning of the drift this component exists to end.
|
||||
*/
|
||||
export interface DrawerProps {
|
||||
title: string;
|
||||
/** One line under the title. */
|
||||
/** One line under the title — a brand, an SKU, a store name. */
|
||||
subtitle?: string;
|
||||
/** Sits beside the title — a status chip, usually. */
|
||||
titleAside?: ReactNode;
|
||||
/** Renders the title in the mono face. For codes: order ids, SKUs. */
|
||||
isTitleMono?: boolean;
|
||||
/** The status pill and any metadata beside it, on the row under the title. */
|
||||
meta?: 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.
|
||||
* Panel width in px. The design system's range is 480–560; 520 is the
|
||||
* default and only content that genuinely cannot fold should exceed it.
|
||||
*/
|
||||
width?: number;
|
||||
/** Renders the body unpadded, for panels that manage their own layout. */
|
||||
isBare?: boolean;
|
||||
/**
|
||||
* The fixed action bar. Omit it entirely for a read-only drawer — an empty
|
||||
* bar is a band of chrome that costs height and offers nothing.
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
/** Stretch footer actions to fill the width, for a single primary action. */
|
||||
isFooterFilled?: boolean;
|
||||
/**
|
||||
* Push the first action to the left and the rest right — a destructive
|
||||
* action kept away from the one people mean to press.
|
||||
*/
|
||||
isFooterSpread?: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -37,12 +53,23 @@ export interface DrawerProps {
|
||||
export function Drawer({
|
||||
title,
|
||||
subtitle,
|
||||
titleAside,
|
||||
width = 460,
|
||||
isTitleMono,
|
||||
meta,
|
||||
width = 520,
|
||||
isBare,
|
||||
footer,
|
||||
isFooterFilled,
|
||||
isFooterSpread,
|
||||
onClose,
|
||||
children,
|
||||
}: DrawerProps) {
|
||||
/*
|
||||
Escape closes.
|
||||
|
||||
A dialog dismissible only by finding the scrim with a mouse is not dismissible
|
||||
at all for anyone on a keyboard, and this one sits over the list the operator
|
||||
is working through.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') onClose();
|
||||
@@ -52,83 +79,36 @@ export function Drawer({
|
||||
}, [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>
|
||||
<div className="drawer-root" role="dialog" aria-modal="true" aria-label={title}>
|
||||
<button type="button" className="drawer-scrim" aria-label="Close" onClick={onClose} />
|
||||
|
||||
{isBare ? children : <VStack gap={3} padding={2}>{children}</VStack>}
|
||||
<div className="drawer-panel dialog-panel" style={{ width }}>
|
||||
<header className="drawer-head">
|
||||
<div className="drawer-head-text">
|
||||
<h2 className="drawer-title" {...(isTitleMono ? { 'data-mono': 'true' } : {})}>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle ? <span className="drawer-sub">{subtitle}</span> : null}
|
||||
{meta ? <div className="drawer-meta">{meta}</div> : null}
|
||||
</div>
|
||||
<button type="button" className="drawer-close" aria-label="Close" onClick={onClose}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="drawer-body" {...(isBare ? { 'data-bare': 'true' } : {})}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{footer ? (
|
||||
<footer
|
||||
className="drawer-foot"
|
||||
{...(isFooterFilled ? { 'data-fill': 'true' } : {})}
|
||||
{...(isFooterSpread ? { 'data-spread': 'true' } : {})}
|
||||
>
|
||||
{footer}
|
||||
</footer>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user