- 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>
70 lines
2.4 KiB
TypeScript
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,
|
|
);
|
|
}
|