WFDASHDRIVER-APP/components/BottomSheet.tsx
Dave b9a112881d feat: readable compact rows and a full-width PO sheet
- Item names wrap and get the full row width; the quantity sits beside
  the name and the PO / notes / Collect / Problem buttons move to their
  own line underneath, all 44px. Similar bolts truncated to
  "M22 x 150 H…" were impossible to tell apart on a phone.
- The PO and notes buttons open a full-width bottom sheet (portalled to
  <body>, above the BottomNav) instead of a 256px popover pinned to a
  mid-row button, which ran off the left edge of the screen.
- The PO sheet shows status, order and item count, with big Open PO PDF
  and Invoice photo buttons (PoActions size="lg").
- Dropped the popover up/down placement measuring and the
  click-outside listener; the sheet closes on backdrop, X or Escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:27:36 +02:00

70 lines
2.4 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
interface BottomSheetProps {
title: string;
subtitle?: string;
onClose: () => void;
children: React.ReactNode;
}
// Full-width panel that slides up from the bottom of the screen.
//
// Replaces the small popovers on the compact rows: those were 256px
// wide and pinned to a button in the middle of the row, so on a phone
// they ran off the left edge. Rendered into <body> through a portal so
// no parent's stacking context or overflow can clip it, and it sits
// above the BottomNav.
export default function BottomSheet({ title, subtitle, onClose, children }: BottomSheetProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [onClose]);
if (!mounted) return null;
return createPortal(
<div
data-bottom-sheet
className="fixed inset-0 z-[90] flex items-end bg-black/35 animate-fade-in"
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(e) => e.stopPropagation()}
className="w-full max-h-[80vh] overflow-y-auto bg-white rounded-t-2xl px-4 pt-2 pb-[calc(1.25rem+env(safe-area-inset-bottom))] shadow-[0_-8px_24px_rgba(0,0,0,0.12)]"
>
<div className="w-10 h-1 rounded-full bg-[rgba(38,37,30,0.2)] mx-auto mb-3" aria-hidden="true" />
<div className="flex items-start justify-between gap-3 mb-3">
<div className="min-w-0">
<p className="text-[13px] text-[rgba(38,37,30,0.6)]">{title}</p>
{subtitle && (
<p className="text-[18px] font-semibold text-[#26251e] break-words leading-snug">{subtitle}</p>
)}
</div>
<button
type="button"
onClick={onClose}
aria-label="Close"
className="w-10 h-10 flex-shrink-0 rounded-lg border border-[rgba(38,37,30,0.15)] flex items-center justify-center active:bg-[rgba(38,37,30,0.06)] touch-manipulation"
>
<X size={18} aria-hidden="true" />
</button>
</div>
{children}
</div>
</div>,
document.body,
);
}