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:
@@ -3,7 +3,7 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,16 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -624,12 +615,12 @@ type ItemRowProps = {
|
||||
function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, onToggle, onChangeCategory, onDeleteRequest, dragHandle }: ItemRowProps) {
|
||||
const [newCategory, setNewCategory] = useState("");
|
||||
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}
|
||||
<button
|
||||
onClick={() => onToggle(item)}
|
||||
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(
|
||||
"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" />}
|
||||
</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}
|
||||
</span>
|
||||
{item.inPantry && (
|
||||
@@ -652,55 +643,63 @@ function ItemRow({ item, readOnly, categoryOptions, categoryLabel, tShopping, on
|
||||
)}
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>{tShopping("changeCategory")}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-56">
|
||||
{categoryOptions.map((category) => (
|
||||
<DropdownMenuItem key={category} onClick={() => onChangeCategory(item, category)}>
|
||||
{categoryLabel(category)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem onClick={() => onChangeCategory(item, null)}>
|
||||
{tShopping("aisleOther")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center gap-1 p-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && newCategory.trim()) {
|
||||
onChangeCategory(item, newCategory.trim());
|
||||
setNewCategory("");
|
||||
}
|
||||
}}
|
||||
placeholder={tShopping("newCategoryPlaceholder")}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 shrink-0 px-2 text-xs"
|
||||
disabled={!newCategory.trim()}
|
||||
onClick={() => { onChangeCategory(item, newCategory.trim()); setNewCategory(""); }}
|
||||
>
|
||||
{tShopping("addCategory")}
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onClick={() => onDeleteRequest(item)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
{tShopping("deleteItem")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Select
|
||||
value={item.aisle ?? OTHER_KEY}
|
||||
onValueChange={(v) => onChangeCategory(item, v === OTHER_KEY ? null : v)}
|
||||
>
|
||||
<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">
|
||||
<SelectValue>
|
||||
{(v: string) => <span className="truncate">{v === OTHER_KEY ? tShopping("aisleOther") : categoryLabel(v)}</span>}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-56">
|
||||
{categoryOptions.map((category) => (
|
||||
<SelectItem key={category} value={category}>{categoryLabel(category)}</SelectItem>
|
||||
))}
|
||||
<SelectItem value={OTHER_KEY}>{tShopping("aisleOther")}</SelectItem>
|
||||
<div className="flex items-center gap-1 p-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && newCategory.trim()) {
|
||||
onChangeCategory(item, newCategory.trim());
|
||||
setNewCategory("");
|
||||
}
|
||||
}}
|
||||
placeholder={tShopping("newCategoryPlaceholder")}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 shrink-0 px-2 text-xs"
|
||||
disabled={!newCategory.trim()}
|
||||
onClick={() => { onChangeCategory(item, newCategory.trim()); setNewCategory(""); }}
|
||||
>
|
||||
{tShopping("addCategory")}
|
||||
</Button>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
aria-label={tShopping("deleteItem")}
|
||||
onClick={() => onDeleteRequest(item)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
} />
|
||||
<TooltipContent>{tShopping("deleteItem")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user