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,10 +1,13 @@
"use client";
import { useRef, type KeyboardEvent, type ReactNode } from "react";
import type { ReactNode } from "react";
import { SegmentedControl, SegmentedControlItem } from "@/components/base/segmented-control/segmented-control";
import { cx } from "@/utils/cx";
export interface RadioOption<T extends string | number> {
value: T;
label: ReactNode;
/** Extra classes on the selected segment (e.g. a status tint). */
activeClass?: string;
}
@ -17,79 +20,32 @@ interface RadioGroupProps<T extends string | number> {
fullWidth?: boolean;
}
/**
* Accessible segmented control implementing WAI-ARIA `radiogroup` roving tabIndex
* and arrow/Home/End keyboard navigation (1 Tab stop per group instead of N).
*/
export function RadioGroup<T extends string | number>({
name,
options,
value,
onChange,
size = "sm",
fullWidth = false,
}: RadioGroupProps<T>) {
const buttonsRef = useRef<(HTMLButtonElement | null)[]>([]);
function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
const currentIndex = Math.max(0, options.findIndex((o) => o.value === value));
let nextIndex = -1;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
nextIndex = (currentIndex + 1) % options.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
nextIndex = (currentIndex - 1 + options.length) % options.length;
} else if (e.key === "Home") {
nextIndex = 0;
} else if (e.key === "End") {
nextIndex = options.length - 1;
}
if (nextIndex !== -1) {
e.preventDefault();
onChange(options[nextIndex].value);
buttonsRef.current[nextIndex]?.focus();
}
}
const pad =
size === "xs"
? fullWidth
? "min-h-8 flex-1 min-w-0 px-1.5 py-1 text-xs"
: "min-h-8 px-2.5 py-1 text-xs"
: fullWidth
? "min-h-9 flex-1 min-w-0 px-2 py-1.5 text-sm"
: "min-h-9 min-w-9 px-3 py-1.5 text-sm";
/** Single-select segmented control: BoardUI's SegmentedControl (React Aria handles roving focus + arrow keys). */
export function RadioGroup<T extends string | number>({ name, options, value, onChange, size = "sm", fullWidth = false }: RadioGroupProps<T>) {
return (
<div
role="radiogroup"
<SegmentedControl
aria-label={name}
onKeyDown={handleKeyDown}
className={`${fullWidth ? "flex w-full" : "inline-flex shrink-0"} max-w-full rounded-lg border border-line-strong bg-raised p-0.5`}
selectedKeys={[value]}
onSelectionChange={(keys) => {
const next = options.find((o) => keys.has(o.value));
if (next) onChange(next.value);
}}
className={cx("max-w-full", fullWidth ? "flex w-full" : "shrink-0")}
>
{options.map((option, i) => {
const checked = option.value === value;
const activeStyle = option.activeClass ?? "bg-surface text-ink border border-selected-line/60 shadow-2xs";
return (
<button
key={String(option.value)}
ref={(el) => {
buttonsRef.current[i] = el;
}}
type="button"
role="radio"
aria-checked={checked}
tabIndex={checked ? 0 : -1}
onClick={() => onChange(option.value)}
className={`trim-cap inline-flex items-center justify-center rounded-md font-medium tabular-nums transition-colors ${pad} ${
checked ? activeStyle : "border border-transparent text-ink-muted hover:text-ink"
}`}
>
{option.label}
</button>
);
})}
</div>
{options.map((option) => (
<SegmentedControlItem
key={String(option.value)}
id={option.value}
className={cx(
"tabular-nums",
size === "xs" && "min-h-7 px-2 text-caption-1-medium",
fullWidth && "min-w-0 flex-1",
option.value === value && option.activeClass,
)}
>
{option.label}
</SegmentedControlItem>
))}
</SegmentedControl>
);
}