2026-10-31 20:44:29 -05:00
|
|
|
"use client";
|
|
|
|
|
|
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>
2026-11-06 16:36:33 -06:00
|
|
|
import { RiPauseLine as Pause, RiPlayLine as Play, RiRepeat2Line as Repeat2, RiShoppingBag3Line as ShoppingBag } from "@remixicon/react";
|
2026-10-31 20:44:29 -05:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
|
|
|
|
export interface PreviewSet {
|
|
|
|
|
theme: string;
|
|
|
|
|
blurb: string;
|
|
|
|
|
meals: { slot: string; title: string; cost: string }[];
|
|
|
|
|
total: string;
|
|
|
|
|
sharedItems: number;
|
|
|
|
|
itemCount: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const INTERVAL_MS = 5000;
|
|
|
|
|
|
|
|
|
|
export function RotatingPreview({ sets }: { sets: PreviewSet[] }) {
|
|
|
|
|
const [index, setIndex] = useState(0);
|
|
|
|
|
const [paused, setPaused] = useState(false);
|
|
|
|
|
const [hovering, setHovering] = useState(false);
|
|
|
|
|
const running = !paused && !hovering;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!running || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
|
const timer = window.setInterval(() => setIndex((i) => (i + 1) % sets.length), INTERVAL_MS);
|
|
|
|
|
return () => window.clearInterval(timer);
|
|
|
|
|
}, [running, index, sets.length]);
|
|
|
|
|
|
|
|
|
|
const set = sets[index];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
aria-label="Example meal plans"
|
|
|
|
|
className="relative"
|
|
|
|
|
onMouseEnter={() => setHovering(true)}
|
|
|
|
|
onMouseLeave={() => setHovering(false)}
|
|
|
|
|
onFocus={() => setHovering(true)}
|
|
|
|
|
onBlur={() => setHovering(false)}
|
|
|
|
|
>
|
|
|
|
|
{/* Architectural editorial frame — crisp 1px border & tactile offset shadow instead of blurred glow blobs */}
|
|
|
|
|
<div className="rounded-xl border border-line-strong bg-surface shadow-[6px_6px_0_0_var(--color-line)]">
|
|
|
|
|
{/* Top progress rule */}
|
|
|
|
|
<div className="h-1 overflow-hidden rounded-t-xl bg-raised">
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
className="h-full origin-left animate-preview-timer bg-brand-600"
|
|
|
|
|
style={{ animationDuration: `${INTERVAL_MS}ms`, animationPlayState: running ? "running" : "paused" }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-5 sm:p-6">
|
|
|
|
|
<div key={set.theme} className="animate-fade-in">
|
|
|
|
|
<div className="flex items-baseline justify-between gap-3 border-b border-line pb-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-mono text-[11px] tracking-wider text-ink-faint uppercase">Sample menu · 2 people</p>
|
|
|
|
|
<h2 className="mt-0.5 font-serif text-xl font-semibold tracking-tight text-ink">{set.theme}</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="rounded-md border border-line bg-raised px-2.5 py-1 text-xs font-medium text-ink-muted">
|
|
|
|
|
{set.blurb}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ul className="divide-y divide-line">
|
|
|
|
|
{set.meals.map((m) => (
|
|
|
|
|
<li key={m.slot} className="flex items-baseline justify-between gap-4 py-3">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="font-mono text-[11px] tracking-wide text-ink-faint uppercase">{m.slot}</p>
|
|
|
|
|
<p className="truncate font-medium text-ink">{m.title}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="shrink-0 font-mono text-xs text-ink-muted tabular-nums">{m.cost}/meal</p>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
{/* Receipt tear-off footer */}
|
|
|
|
|
<div className="receipt-rule mt-2 pt-3.5">
|
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-raised px-3.5 py-2.5 text-xs">
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 font-medium text-status-ok">
|
|
|
|
|
<Repeat2 className="size-3.5 shrink-0" strokeWidth={2} aria-hidden />
|
|
|
|
|
{set.sharedItems} shared store items
|
|
|
|
|
</span>
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 font-mono font-semibold text-ink tabular-nums">
|
|
|
|
|
<ShoppingBag className="size-3.5 shrink-0 text-ink-muted" strokeWidth={1.75} aria-hidden />
|
|
|
|
|
{set.itemCount} pkgs · {set.total}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Accessible 40x40px pagination & playback controls */}
|
|
|
|
|
<div className="mt-3 flex items-center justify-between border-t border-line pt-2">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
{sets.map((s, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s.theme}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setIndex(i)}
|
|
|
|
|
aria-label={`Show example: ${s.theme}`}
|
|
|
|
|
aria-current={i === index}
|
|
|
|
|
className="grid size-9 place-items-center rounded-md hover:bg-raised"
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`block h-2 rounded-full transition-all ${
|
|
|
|
|
i === index ? "w-5 bg-brand-600" : "w-2 bg-line-strong"
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setPaused((p) => !p)}
|
|
|
|
|
className="trim-cap inline-flex min-h-9 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-ink-muted transition-colors hover:bg-raised hover:text-ink"
|
|
|
|
|
>
|
|
|
|
|
{paused ? <Play className="size-3.5" aria-hidden /> : <Pause className="size-3.5" aria-hidden />}
|
|
|
|
|
{paused ? "Play" : "Pause"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|