"use client"; import { AlertTriangle, ArrowLeftRight, Plus, X } from "lucide-react"; import { useState, type Dispatch } from "react"; import { APPLIANCES, DAY_NAMES, activeSlots, describeBlockers, isCookable, mealLabel, recipeBlockers, slotKey } from "@/app/lib/plan"; import { RECIPES_BY_ID } from "@/app/lib/recipes"; import { formatUsd } from "@/app/lib/units"; import type { PlanAction } from "@/app/lib/usePlanState"; import type { MealPlanState } from "@/app/types"; import { ApplianceIcon } from "./Icons"; interface MealGridProps { state: MealPlanState; dispatch: Dispatch; mealCosts: Map; onOpenSlot: (key: string) => void; onViewSlot: (key: string) => void; onRemoveSlot: (key: string, recipeTitle: string) => void; } const applianceLabel = (id: string) => APPLIANCES.find((a) => a.id === id)?.label ?? id; export function MealGrid({ state, dispatch, mealCosts, onOpenSlot, onViewSlot, onRemoveSlot }: MealGridProps) { const [dragFrom, setDragFrom] = useState(null); const [dragOver, setDragOver] = useState(null); /** Mobile & keyboard slot-swap picker: holds the origin slot key currently choosing a destination. */ const [moveSourceKey, setMoveSourceKey] = useState(null); const allSlots = activeSlots(state); const days = Array.from({ length: state.daysCount }, (_, day) => allSlots.filter((s) => s.dayIndex === day)); return (

Weekly menu

Drag meals or use the swap icon to rearrange · Tap any planned dish for its scaled recipe

{days.map((slots, day) => { const dayCost = slots.reduce((sum, s) => sum + (s.recipeId ? (mealCosts.get(s.recipeId) ?? 0) : 0), 0); const hasAnyMeal = slots.some((s) => s.recipeId !== null); return (
{/* Architectural day header */}

{DAY_NAMES[day]}

{hasAnyMeal && ( {formatUsd(dayCost)} eaten )}
    {slots.map((slot) => { const key = slotKey(slot.dayIndex, slot.mealNumber); const recipe = slot.recipeId ? RECIPES_BY_ID.get(slot.recipeId) : undefined; const blocked = recipe ? !isCookable(recipe, state) : false; const label = mealLabel(slot.mealNumber); const isDropTarget = dragOver === key && dragFrom !== key; const isMovingThis = moveSourceKey === key; return (
  • { if (!dragFrom) return; e.preventDefault(); setDragOver(key); }} onDragLeave={() => setDragOver((current) => (current === key ? null : current))} onDrop={(e) => { e.preventDefault(); if (dragFrom && dragFrom !== key) dispatch({ type: "swapSlots", from: dragFrom, to: key }); setDragFrom(null); setDragOver(null); }} className={`transition-colors ${isDropTarget ? "bg-brand-950 ring-2 ring-inset ring-brand-600" : ""}`} > {recipe ? (
    setDragFrom(key)} onDragEnd={() => { setDragFrom(null); setDragOver(null); }} className={`p-3.5 transition-colors ${ blocked ? "bg-status-warn-bg/65" : "hover:bg-raised/45" }`} >
    {/* Non-overlapping 36x36px touch & keyboard action controls */}
    {allSlots.length > 1 && ( )}
    {/* Touch & keyboard accessible Swap/Move picker */} {isMovingThis && (

    Swap or move to another slot:

    {allSlots .filter((target) => slotKey(target.dayIndex, target.mealNumber) !== key) .map((target) => { const targetKey = slotKey(target.dayIndex, target.mealNumber); const occupant = target.recipeId ? RECIPES_BY_ID.get(target.recipeId)?.title : null; return ( ); })}
    )}
    ) : ( )}
  • ); })}
); })}
); }