spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1'use client';2import { X } from 'lucide-react';3import { type ReactNode, useEffect, useRef } from 'react';4import { cn } from '@/lib/cn';56/**7 * Sheet / drawer primitive shared by the event drawer, filter sheets and the mobile "More" menu.8 * `side="auto"` (default): right-side panel ≥ lg, bottom sheet below. Focus trap, Esc, backdrop click, body scroll lock.9 */10export function Sheet({11 open,12 onClose,13 title,14 eyebrow,15 children,16 side = 'auto',17 width = 'lg:w-[30rem]',18 className,19 footer,20 id,21 headerExtra,22}: {23 open: boolean;24 onClose: () => void;25 title?: ReactNode;26 eyebrow?: ReactNode;27 children: ReactNode;28 side?: 'auto' | 'right' | 'bottom';29 width?: string;30 className?: string;31 footer?: ReactNode;32 id?: string;33 headerExtra?: ReactNode;34}) {35 const panel = useRef<HTMLDivElement>(null);36 const closeBtn = useRef<HTMLButtonElement>(null);37 const restore = useRef<HTMLElement | null>(null);3839 useEffect(() => {40 if (!open) return;41 restore.current = document.activeElement as HTMLElement | null;42 const prevOverflow = document.body.style.overflow;43 document.body.style.overflow = 'hidden';44 const t = setTimeout(() => closeBtn.current?.focus(), 20);45 const onKey = (e: KeyboardEvent) => {46 if (e.key === 'Escape') {47 e.stopPropagation();48 onClose();49 } else if (e.key === 'Tab' && panel.current) {50 const nodes = panel.current.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])');51 if (!nodes.length) return;52 const first = nodes[0] as HTMLElement;53 const last = nodes[nodes.length - 1] as HTMLElement;54 if (e.shiftKey && document.activeElement === first) {55 e.preventDefault();56 last.focus();57 } else if (!e.shiftKey && document.activeElement === last) {58 e.preventDefault();59 first.focus();60 }61 }62 };63 document.addEventListener('keydown', onKey);64 return () => {65 clearTimeout(t);66 document.removeEventListener('keydown', onKey);67 document.body.style.overflow = prevOverflow;68 restore.current?.focus?.();69 };70 }, [open, onClose]);7172 if (!open) return null;73 const right = side === 'right';74 return (75 <div className="fixed inset-0 z-[90] flex bg-black/45 backdrop-blur-[1px]" onClick={onClose} role="presentation">76 <div77 ref={panel}78 id={id}79 role="dialog"80 aria-modal="true"81 onClick={(e) => e.stopPropagation()}82 className={cn(83 'sheet-enter panel flex flex-col overflow-hidden shadow-2xl',84 !right && 'absolute inset-x-0 bottom-0 max-h-[88dvh] rounded-b-none rounded-t-lg',85 side === 'auto' && `lg:inset-y-0 lg:right-0 lg:left-auto lg:h-full lg:max-h-none lg:rounded-none lg:border-y-0 lg:border-r-0 ${width} lg:max-w-[92vw]`,86 right && `absolute inset-y-0 right-0 h-full max-w-[92vw] rounded-none border-y-0 border-r-0 ${width}`,87 className,88 )}89 >90 <div className="flex items-start gap-3 border-b border-rule px-4 py-3">91 <div className="min-w-0 flex-1">92 {eyebrow && <p className="eyebrow">{eyebrow}</p>}93 {title && <h2 className="mt-0.5 text-[15px] font-semibold leading-snug text-ink">{title}</h2>}94 </div>95 {headerExtra}96 <button ref={closeBtn} type="button" onClick={onClose} className="-mr-1 flex size-11 shrink-0 items-center justify-center rounded-sm text-ink-3 hover:bg-surface-2 hover:text-ink" aria-label="Close">97 <X className="size-5" aria-hidden />98 </button>99 </div>100 <div className="scrollbar-thin flex-1 overflow-y-auto px-4 py-3">{children}</div>101 {footer && <div className="safe-bottom border-t border-rule px-4 py-3">{footer}</div>}102 </div>103 </div>104 );105}106