diff --git a/components/BottomSheet.tsx b/components/BottomSheet.tsx new file mode 100644 index 0000000..a9fa6f2 --- /dev/null +++ b/components/BottomSheet.tsx @@ -0,0 +1,70 @@ +'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 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( +
+
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)]" + > + +
, + document.body, + ); +} diff --git a/components/PoActions.tsx b/components/PoActions.tsx index 4ad605d..8b19eb0 100644 --- a/components/PoActions.tsx +++ b/components/PoActions.tsx @@ -13,6 +13,8 @@ interface PoActionsProps { onAddInvoicePage: (items: OrderItem[], file: File) => Promise; /** False hides the photo button (e.g. the caller can't upload). */ canPhoto?: boolean; + /** 'lg' = full-width stacked buttons, for the PO bottom sheet. */ + size?: 'sm' | 'lg'; className?: string; } @@ -24,6 +26,7 @@ export default function PoActions({ items, onAddInvoicePage, canPhoto: photoAllowed = true, + size = 'sm', className, }: PoActionsProps) { const inputRef = useRef(null); @@ -53,19 +56,27 @@ export default function PoActions({ }; const poLabel = purchaseOrder?.po_number ? `PO ${purchaseOrder.po_number}` : 'this PO'; + const lg = size === 'lg'; + const btn = lg + ? 'h-12 w-full rounded-xl text-[15px] font-semibold flex items-center justify-center gap-2 transition-colors touch-manipulation' + : 'h-7 px-2 rounded-md text-[11px] font-semibold flex items-center gap-1 transition-colors touch-manipulation'; + const icon = lg ? 18 : 11; return ( -
+
{pdfUrl && ( - )} {canPhoto && ( @@ -81,26 +92,30 @@ export default function PoActions({ : `Add page ${pages + 1} of the invoice for ${poLabel}` } className={cn( - 'h-7 px-2 rounded-md text-[11px] font-semibold flex items-center gap-1 transition-colors touch-manipulation', - pages === 0 - ? 'bg-[#26251e] text-white active:bg-black' - : 'bg-[#e6f1ec] text-[#125a3e] active:bg-[#cfe6db]', + btn, + pages > 0 + ? 'bg-[#e6f1ec] text-[#125a3e] active:bg-[#cfe6db]' + : lg + ? 'bg-white text-[#26251e] border border-[rgba(38,37,30,0.2)] active:bg-[#f2f1ed]' + : 'bg-[#26251e] text-white active:bg-black', uploading && 'opacity-60', )} > {uploading ? ( -