"use client"; import { RiCheckLine as Check, RiFileCopyLine as Copy, RiRepeat2Line as Repeat2, RiCloseLine as X } from "@remixicon/react"; import { useEffect, useRef, useState } from "react"; import { formatAmount, formatUsd } from "@/app/lib/units"; import type { Aisle, GroceryLine, GrocerySummary, ItemId } from "@/app/types"; import { cx } from "@/utils/cx"; import { BudgetMeter } from "./BudgetMeter"; interface PanelProps { summary: GrocerySummary; budget: number; priceSourceLabel: string; onToggleOnHand: (itemId: ItemId, onHand: boolean) => void; } export function GrocerySummaryPanel({ summary, budget, priceSourceLabel, onToggleOnHand }: PanelProps) { const [copied, setCopied] = useState(false); const byAisle = groupByAisle(summary.lines); const remaining = budget - summary.checkoutTotal; async function copyList() { const text = summary.lines .filter((l) => !l.onHand) .map((l) => `☐ ${l.packages} × ${l.item.name} (${l.item.packageLabel})`) .join("\n"); try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Clipboard can be blocked; list remains visible on screen. } } return (
{/* --- Tactile Market Receipt Header -------------------------------- */}
= 0 ? "Left in budget" : "Over budget"} value={formatUsd(Math.abs(remaining))} tone={remaining >= 0 ? "text-status-ok" : "text-status-danger"} />
{summary.sharedItemCount > 0 && (

{summary.sharedItemCount} store item{summary.sharedItemCount > 1 ? "s" : ""} shared across meals

)}
{/* --- Shopping List Header ----------------------------------------- */}

Shopping list

Tap “I have this” on anything in your kitchen and it drops off the bill.

{summary.lines.length > 0 && ( )}
{summary.lines.length === 0 ? (

Add meals to build your aisle-sorted grocery receipt.

) : (
{byAisle.map(([aisle, lines]) => (

{aisle}

    {lines.map((line) => ( ))}
))}
)}

{priceSourceLabel}. Actual store prices vary by location.

); } function GroceryRow({ line, onToggleOnHand }: { line: GroceryLine; onToggleOnHand: PanelProps["onToggleOnHand"] }) { const { item, onHand } = line; return (
  • {line.packages > 1 && {line.packages} × } {item.name} {item.packageLabel} · need {formatAmount(line.neededAmount, item.baseUnit)} {line.usedBy.length > 1 && ( Used in {line.usedBy.length} meals )} {onHand ? "$0.00" : formatUsd(line.checkoutCost)}
  • ); } function Stat({ label, value, hint, tone = "text-ink" }: { label: string; value: string; hint?: string; tone?: string }) { return (
    {label}
    {value}
    ); } function groupByAisle(lines: GroceryLine[]): [Aisle, GroceryLine[]][] { const groups = new Map(); for (const line of lines) { const group = groups.get(line.item.aisle) ?? []; group.push(line); groups.set(line.item.aisle, group); } return [...groups]; } // --------------------------------------------------------------------------- // GroceryListDrawer: Native mobile bottom sheet with focus trapping, // scroll locking, and @starting-style spring transition. // --------------------------------------------------------------------------- interface DrawerProps extends PanelProps { open: boolean; onClose: () => void; } export function GroceryListDrawer({ open, onClose, ...panelProps }: DrawerProps) { const dialogRef = useRef(null); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; if (open && !dialog.open) dialog.showModal(); if (!open && dialog.open) dialog.close(); return () => { if (dialog.open) dialog.close(); }; }, [open]); return ( e.target === dialogRef.current && onClose()} aria-labelledby="grocery-drawer-title" className="sheet-dialog m-0 mt-auto max-h-[88dvh] w-full max-w-none overflow-y-auto rounded-t-3xl border-t border-line bg-surface p-0 shadow-2xl lg:hidden" >

    Market receipt & list

    ); }