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,6 +1,6 @@
"use client";
import { ChevronDown, Search, X } from "lucide-react";
import { RiArrowDownSLine as ChevronDown, RiSearchLine as Search, RiCloseLine as X } from "@remixicon/react";
import { useMemo, useState, type Dispatch } from "react";
import { CATALOG, CATALOG_BY_ID, PANTRY_STAPLE_IDS } from "@/app/lib/catalog";
import { RECIPES } from "@/app/lib/recipes";
@ -8,6 +8,9 @@ import type { PlanAction } from "@/app/lib/usePlanState";
import type { Aisle, ItemId, ItemPreference, MealPlanState, StoreItem } from "@/app/types";
import { RadioGroup, type RadioOption } from "./RadioGroup";
// Shown beside the staples toggle so people know exactly what it marks as $0.
const STAPLE_NAMES = PANTRY_STAPLE_IDS.map((id) => CATALOG_BY_ID.get(id)?.name).filter(Boolean).join(", ");
interface IngredientPanelProps {
state: MealPlanState;
dispatch: Dispatch<PlanAction>;
@ -63,7 +66,7 @@ export function IngredientPanel({ state, dispatch }: IngredientPanelProps) {
const staplesAllOnHand = PANTRY_STAPLE_IDS.every((id) => state.onHandItems.includes(id));
return (
<section aria-labelledby="ingredients-heading" className="rounded-xl border border-line bg-surface">
<section aria-labelledby="ingredients-heading" className="rounded-3xl border border-border-button-default bg-surface">
<details className="group">
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 p-4 sm:px-5 [&::-webkit-details-marker]:hidden">
<div>
@ -115,10 +118,11 @@ export function IngredientPanel({ state, dispatch }: IngredientPanelProps) {
<RadioGroup name="Filter ingredients" options={FILTER_OPTIONS} value={filter} onChange={setFilter} size="xs" />
</div>
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs sm:text-sm">
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs sm:text-sm">
<button
type="button"
onClick={() => setPreference(PANTRY_STAPLE_IDS, staplesAllOnHand ? "use" : "have")}
aria-describedby="pantry-staples-list"
className="min-h-8 font-medium text-brand-300 hover:underline"
>
{staplesAllOnHand ? "Unmark pantry staples" : "Mark basic pantry staples as on hand"}
@ -132,6 +136,9 @@ export function IngredientPanel({ state, dispatch }: IngredientPanelProps) {
Reset all to &ldquo;Use&rdquo;
</button>
)}
<p id="pantry-staples-list" className="basis-full text-xs text-ink-muted">
Staples: {STAPLE_NAMES}.
</p>
</div>
<div className="mt-4 max-h-[28rem] space-y-5 overflow-y-auto pr-1">
@ -139,7 +146,8 @@ export function IngredientPanel({ state, dispatch }: IngredientPanelProps) {
{groups.map(([aisle, items]) => (
<section key={aisle}>
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-line bg-surface py-1.5">
{/* z-20: BoardUI segment buttons are z-10, so the sticky header must sit above them while rows scroll under it */}
<div className="sticky top-0 z-20 flex items-center justify-between border-b border-line bg-surface py-1.5">
<h3 className="font-mono text-xs font-semibold tracking-wider text-ink-muted uppercase">{aisle}</h3>
<AisleBulkToggle itemIds={items.map((i) => i.id)} avoidedItems={state.avoidedItems} onSet={setPreference} />
</div>