refactor: shopping list item row — direct buttons instead of "..." overflow menu

Replaced the hidden dropdown (change category submenu + delete, behind a
single unlabeled MoreVertical icon) with two directly visible controls,
matching the recipe detail page's icon-row pattern: a compact category
Select showing the item's current category at a glance (still supports
picking a predefined category, "Other", or typing a new custom one), and
a direct trash icon button with a tooltip. Row stays single-line via
truncation on the name and a fixed-width category trigger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-10 11:44:02 +02:00
parent 70ad1bec9c
commit 0022d807d0
@@ -3,7 +3,7 @@
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Check, Package, Loader2, GripVertical, MoreVertical, Trash2, Search, Sparkles } from "lucide-react"; import { Check, Package, Loader2, GripVertical, Trash2, Search, Sparkles } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -14,16 +14,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -624,12 +615,12 @@ type ItemRowProps = {
function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, onToggle, onChangeCategory, onDeleteRequest, dragHandle }: ItemRowProps) { function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, onToggle, onChangeCategory, onDeleteRequest, dragHandle }: ItemRowProps) {
const [newCategory, setNewCategory] = useState(""); const [newCategory, setNewCategory] = useState("");
return ( return (
<div className="w-full flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors"> <div className="w-full flex items-center gap-2 px-4 py-3 hover:bg-muted/30 transition-colors">
{dragHandle} {dragHandle}
<button <button
onClick={() => onToggle(item)} onClick={() => onToggle(item)}
disabled={readOnly} disabled={readOnly}
className="flex flex-1 items-center gap-3 text-left disabled:cursor-default" className="flex flex-1 min-w-0 items-center gap-3 text-left disabled:cursor-default"
> >
<div className={cn( <div className={cn(
"h-5 w-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors", "h-5 w-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors",
@@ -637,7 +628,7 @@ function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, on
)}> )}>
{item.checked && <Check className="h-3 w-3 text-primary-foreground" />} {item.checked && <Check className="h-3 w-3 text-primary-foreground" />}
</div> </div>
<span className={cn("flex-1 text-sm", item.checked && "line-through text-muted-foreground")}> <span className={cn("flex-1 min-w-0 truncate text-sm", item.checked && "line-through text-muted-foreground")}>
{item.rawName} {item.rawName}
</span> </span>
{item.inPantry && ( {item.inPantry && (
@@ -652,55 +643,63 @@ function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, on
)} )}
</button> </button>
{!readOnly && ( {!readOnly && (
<DropdownMenu> <div className="flex items-center gap-1 shrink-0">
<DropdownMenuTrigger className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"> <Select
<MoreVertical className="h-4 w-4" /> value={item.aisle ?? OTHER_KEY}
</DropdownMenuTrigger> onValueChange={(v) => onChangeCategory(item, v === OTHER_KEY ? null : v)}
<DropdownMenuContent align="end"> >
<DropdownMenuSub> <SelectTrigger className="h-7 max-w-28 border-none bg-transparent px-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-3">
<DropdownMenuSubTrigger>{tShopping("changeCategory")}</DropdownMenuSubTrigger> <SelectValue>
<DropdownMenuSubContent className="w-56"> {(v: string) => <span className="truncate">{v === OTHER_KEY ? tShopping("aisleOther") : categoryLabel(v)}</span>}
{categoryOptions.map((category) => ( </SelectValue>
<DropdownMenuItem key={category} onClick={() => onChangeCategory(item, category)}> </SelectTrigger>
{categoryLabel(category)} <SelectContent className="w-56">
</DropdownMenuItem> {categoryOptions.map((category) => (
))} <SelectItem key={category} value={category}>{categoryLabel(category)}</SelectItem>
<DropdownMenuItem onClick={() => onChangeCategory(item, null)}> ))}
{tShopping("aisleOther")} <SelectItem value={OTHER_KEY}>{tShopping("aisleOther")}</SelectItem>
</DropdownMenuItem> <div className="flex items-center gap-1 p-1.5" onClick={(e) => e.stopPropagation()}>
<DropdownMenuSeparator /> <Input
<div className="flex items-center gap-1 p-1.5" onClick={(e) => e.stopPropagation()}> value={newCategory}
<Input onChange={(e) => setNewCategory(e.target.value)}
value={newCategory} onKeyDown={(e) => {
onChange={(e) => setNewCategory(e.target.value)} if (e.key === "Enter" && newCategory.trim()) {
onKeyDown={(e) => { onChangeCategory(item, newCategory.trim());
if (e.key === "Enter" && newCategory.trim()) { setNewCategory("");
onChangeCategory(item, newCategory.trim()); }
setNewCategory(""); }}
} placeholder={tShopping("newCategoryPlaceholder")}
}} className="h-7 text-xs"
placeholder={tShopping("newCategoryPlaceholder")} />
className="h-7 text-xs" <Button
/> size="sm"
<Button variant="ghost"
size="sm" className="h-7 shrink-0 px-2 text-xs"
variant="ghost" disabled={!newCategory.trim()}
className="h-7 shrink-0 px-2 text-xs" onClick={() => { onChangeCategory(item, newCategory.trim()); setNewCategory(""); }}
disabled={!newCategory.trim()} >
onClick={() => { onChangeCategory(item, newCategory.trim()); setNewCategory(""); }} {tShopping("addCategory")}
> </Button>
{tShopping("addCategory")} </div>
</Button> </SelectContent>
</div> </Select>
</DropdownMenuSubContent> <TooltipProvider>
</DropdownMenuSub> <Tooltip>
<DropdownMenuSeparator /> <TooltipTrigger render={
<DropdownMenuItem variant="destructive" onClick={() => onDeleteRequest(item)}> <Button
<Trash2 className="h-4 w-4" /> variant="ghost"
{tShopping("deleteItem")} size="icon"
</DropdownMenuItem> className="h-7 w-7 text-muted-foreground hover:text-destructive"
</DropdownMenuContent> aria-label={tShopping("deleteItem")}
</DropdownMenu> onClick={() => onDeleteRequest(item)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
} />
<TooltipContent>{tShopping("deleteItem")}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)} )}
</div> </div>
); );