planmymeal/app/components/RecipeServings.tsx
Cayden Schwechten df5dd3a70e 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

101 lines
3.9 KiB
TypeScript

"use client";
import { RiFireLine as Flame, RiSubtractLine as Minus, RiAddLine as Plus } from "@remixicon/react";
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import type { Recipe } from "@/app/types";
import { RecipeDetail } from "./RecipeDetail";
const noopSubscribe = () => () => {};
/** RecipeDetail with an accessible servings stepper + Screen Wake Lock ("Cook mode") toggle. */
export function RecipeServings({ recipe }: { recipe: Recipe }) {
const [servings, setServings] = useState(recipe.servings);
const [cookMode, setCookMode] = useState(false);
const wakeSupported = useSyncExternalStore(noopSubscribe, () => "wakeLock" in navigator, () => false);
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
useEffect(() => {
if (!cookMode || !wakeSupported) {
wakeLockRef.current?.release().catch(() => {});
wakeLockRef.current = null;
return;
}
let cancelled = false;
async function acquire() {
try {
const sentinel = await navigator.wakeLock.request("screen");
if (cancelled) {
await sentinel.release();
return;
}
wakeLockRef.current = sentinel;
} catch {
// Battery saver or background tab can refuse wakeLock; fail gracefully.
}
}
acquire();
const onVisibility = () => {
if (document.visibilityState === "visible" && cookMode) acquire();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
cancelled = true;
document.removeEventListener("visibilitychange", onVisibility);
wakeLockRef.current?.release().catch(() => {});
wakeLockRef.current = null;
};
}, [cookMode, wakeSupported]);
return (
<div>
<div className="mb-6 flex flex-wrap items-center justify-between gap-3 border-b border-line pb-4">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-ink-muted">Scale servings</span>
<div className="inline-flex items-center rounded-lg border border-line-strong bg-raised p-0.5">
<button
type="button"
onClick={() => setServings((s) => Math.max(1, s - 1))}
className="grid size-9 place-items-center rounded-md text-ink-muted transition-colors hover:bg-surface hover:text-ink"
aria-label="Fewer servings"
>
<Minus className="size-4" aria-hidden />
</button>
<span className="min-w-9 text-center font-mono text-sm font-semibold tabular-nums" aria-live="polite">
{servings}
</span>
<button
type="button"
onClick={() => setServings((s) => Math.min(12, s + 1))}
className="grid size-9 place-items-center rounded-md text-ink-muted transition-colors hover:bg-surface hover:text-ink"
aria-label="More servings"
>
<Plus className="size-4" aria-hidden />
</button>
</div>
<span className="hidden text-xs text-ink-faint sm:inline">Tap any ingredient or step below to check it off</span>
</div>
{wakeSupported && (
<button
type="button"
aria-pressed={cookMode}
onClick={() => setCookMode((c) => !c)}
className={`trim-cap inline-flex min-h-9 items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold transition-colors ${
cookMode
? "border-status-ok-line bg-status-ok-bg text-status-ok"
: "border-line-strong bg-surface text-ink-muted hover:bg-raised hover:text-ink"
}`}
>
<Flame className="size-3.5" aria-hidden />
{cookMode ? "Screen kept awake" : "Keep screen awake"}
</button>
)}
</div>
<RecipeDetail recipe={recipe} servings={servings} showMeta={false} interactive />
</div>
);
}