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:
parent
b02c7a024b
commit
df5dd3a70e
56 changed files with 4749 additions and 788 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue