Move UI to BoardUI with slate + salmon theme; add PDF plan export

- Adopt BoardUI (Pro init): theme tokens, Inter, .dark mode, Button,
  SegmentedControl, Meter, Skeleton and ThemeToggle; remap the app's
  color aliases onto BoardUI tokens and swap lucide for Remix icons
- Retint BoardUI's neutral ramp to cool slate with a salmon accent;
  drop the dotted background; raise muted text contrast
- Simplify the landing page and remove the overlap explorer
- Planner: explicit "I have this" toggle on grocery lines, pantry panel
  above the menu with the staples list spelled out, sticky aisle
  header fix, and a "Download PDF" plan export (jsPDF, lazy-loaded)
- Recipes: replace the family cabbage soup with Cade's Cabbage Soup
  (adds Bone-In Ham Steak to the catalog); remove Ham, Bean & Cabbage Soup
- Remove unused BoardUI components, dependencies and exports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cayden Schwechten 2026-11-06 16:36:33 -06:00
parent b02c7a024b
commit df5dd3a70e
56 changed files with 4749 additions and 788 deletions

View file

@ -1,9 +1,10 @@
"use client";
import { Check, Copy, Repeat2, Square, SquareCheckBig, X } from "lucide-react";
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 {
@ -35,7 +36,7 @@ export function GrocerySummaryPanel({ summary, budget, priceSourceLabel, onToggl
return (
<div>
{/* --- Tactile Market Receipt Header -------------------------------- */}
<div className="paper-grain rounded-xl border border-line bg-canvas p-4">
<div className="rounded-3xl border border-border-button-default bg-canvas p-4">
<BudgetMeter spent={summary.checkoutTotal} budget={budget} />
<dl className="receipt-rule mt-4 grid grid-cols-2 gap-3 pt-3.5 text-xs">
@ -61,7 +62,7 @@ export function GrocerySummaryPanel({ summary, budget, priceSourceLabel, onToggl
<div className="mt-5 flex items-center justify-between">
<div>
<h3 className="font-serif text-base font-semibold">Shopping list</h3>
<p className="text-xs text-ink-faint">Tap items you already have on hand</p>
<p className="text-xs text-ink-faint">Tap &ldquo;I have this&rdquo; on anything in your kitchen and it drops off the bill.</p>
</div>
{summary.lines.length > 0 && (
<button
@ -102,48 +103,46 @@ export function GrocerySummaryPanel({ summary, budget, priceSourceLabel, onToggl
}
function GroceryRow({ line, onToggleOnHand }: { line: GroceryLine; onToggleOnHand: PanelProps["onToggleOnHand"] }) {
const { item } = line;
const { item, onHand } = line;
return (
<li>
<button
type="button"
onClick={() => onToggleOnHand(item.id, !line.onHand)}
aria-pressed={line.onHand}
aria-label={line.onHand ? `Mark ${item.name} as needed at the store` : `Mark ${item.name} as already on hand`}
className={`flex min-h-12 w-full items-start justify-between gap-3 py-2.5 text-left text-sm transition-colors hover:bg-raised/50 ${
line.onHand ? "text-ink-faint" : "text-ink"
}`}
>
<span className="flex min-w-0 items-start gap-2.5">
<span className="mt-0.5 shrink-0 text-ink-muted">
{line.onHand ? (
<SquareCheckBig className="size-4 text-status-ok" strokeWidth={2} aria-hidden />
) : (
<Square className="size-4 text-line-strong" strokeWidth={1.75} aria-hidden />
)}
</span>
<span className="min-w-0">
<span className={`block leading-snug ${line.onHand ? "line-through" : "font-medium"}`}>
{line.packages > 1 && <span className="font-mono tabular-nums">{line.packages} × </span>}
{item.name}
</span>
<span className="mt-0.5 block text-xs text-ink-muted">
{item.packageLabel} · need {formatAmount(line.neededAmount, item.baseUnit)}
{line.onHand && " · on hand"}
</span>
{line.usedBy.length > 1 && (
<span className="mt-0.5 inline-flex items-center gap-1 text-xs text-ink-faint" title={line.usedBy.join(", ")}>
<Repeat2 className="size-3 shrink-0" aria-hidden />
Used in {line.usedBy.length} meals
</span>
)}
</span>
<li className="flex min-h-12 items-start justify-between gap-3 py-2.5 text-sm">
<span className="min-w-0">
<span className={cx("block leading-snug", onHand ? "text-ink-faint line-through" : "font-medium text-ink")}>
{line.packages > 1 && <span className="font-mono tabular-nums">{line.packages} × </span>}
{item.name}
</span>
<span className="mt-0.5 block text-xs text-ink-muted">
{item.packageLabel} · need {formatAmount(line.neededAmount, item.baseUnit)}
</span>
{line.usedBy.length > 1 && (
<span className="mt-0.5 inline-flex items-center gap-1 text-xs text-ink-faint" title={line.usedBy.join(", ")}>
<Repeat2 className="size-3 shrink-0" aria-hidden />
Used in {line.usedBy.length} meals
</span>
)}
</span>
<span className="shrink-0 text-right font-mono text-sm font-medium tabular-nums">
{line.onHand ? "$0.00" : formatUsd(line.checkoutCost)}
<span className="flex shrink-0 flex-col items-end gap-1.5">
<span className={cx("font-mono text-sm font-medium tabular-nums", onHand ? "text-ink-faint" : "text-ink")}>
{onHand ? "$0.00" : formatUsd(line.checkoutCost)}
</span>
</button>
<button
type="button"
onClick={() => onToggleOnHand(item.id, !onHand)}
aria-pressed={onHand}
aria-label={`I have ${item.name}`}
title={onHand ? "Counted as $0. Tap to add it back to the bill." : "Already have it? Tap to take it off the bill."}
className={cx(
"inline-flex min-h-7 cursor-pointer items-center gap-1 rounded-full border px-2.5 text-caption-1-medium whitespace-nowrap transition-colors duration-150",
"outline-none focus-visible:ring-2 focus-visible:ring-border-focus-ring",
onHand
? "border-status-ok-line bg-status-ok-bg text-status-ok"
: "border-border-button-default bg-background-primary-default text-ink-muted hover:border-border-button-hover hover:text-ink",
)}
>
{onHand && <Check className="size-3.5 shrink-0" aria-hidden />}I have this
</button>
</span>
</li>
);
}
@ -198,7 +197,7 @@ export function GroceryListDrawer({ open, onClose, ...panelProps }: DrawerProps)
onClose={onClose}
onClick={(e) => 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-2xl border-t border-line bg-surface p-0 shadow-2xl lg:hidden"
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"
>
<div className="p-4 pb-8 sm:px-6">
<div className="mb-4 flex items-center justify-between border-b border-line pb-3">