From e501679416928aa24e962d40457806283ac1efb1 Mon Sep 17 00:00:00 2001 From: Bonna Moon Date: Mon, 29 Jun 2026 17:48:44 -0500 Subject: [PATCH] =?UTF-8?q?v0.18.1=20=E2=80=94=20Pantry:=20add=20custom=20?= =?UTF-8?q?categories=20on=20the=20fly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type a new name in the Category or Subcategory dropdown and pick "Add '...'" at the bottom. Custom categories persist (stored as strings on the item) and reappear merged into future dropdowns. Co-Authored-By: Claude Sonnet 4.6 --- src/components/pantry/pantry-item-dialog.tsx | 27 ++++++-- src/components/ui/combobox.tsx | 69 +++++++++++++------- src/lib/changelog.ts | 7 ++ src/lib/version.ts | 2 +- 4 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/components/pantry/pantry-item-dialog.tsx b/src/components/pantry/pantry-item-dialog.tsx index 1b6cc95..a1aefed 100644 --- a/src/components/pantry/pantry-item-dialog.tsx +++ b/src/components/pantry/pantry-item-dialog.tsx @@ -148,7 +148,22 @@ export function PantryItemDialog({ item, locations, plants, onSuccess }: Props) set("name", s.name); } - const subcategoryOptions = getSubcategoryOptions(form.pantryCategory); + // Merge static category list with any custom categories previously saved to the DB. + const catalogCategories: string[] = catalog?.categories ?? []; + const knownCategoryValues = new Set(CATEGORY_OPTIONS.map(o => o.value.toLowerCase())); + const extraCategoryOptions = catalogCategories + .filter(c => !knownCategoryValues.has(c.toLowerCase())) + .map(c => ({ value: c, label: c })); + const allCategoryOptions = [...CATEGORY_OPTIONS, ...extraCategoryOptions]; + + // Static subcategories for this category + any custom ones from the catalog. + const staticSubcategoryOptions = getSubcategoryOptions(form.pantryCategory); + const catalogSubcategories: { category: string; subcategory: string }[] = catalog?.subcategories ?? []; + const knownSubValues = new Set(staticSubcategoryOptions.map(o => o.value.toLowerCase())); + const extraSubOptions = catalogSubcategories + .filter(cs => cs.category.toLowerCase() === form.pantryCategory.toLowerCase() && !knownSubValues.has(cs.subcategory.toLowerCase())) + .map(cs => ({ value: cs.subcategory, label: cs.subcategory })); + const subcategoryOptions = [...staticSubcategoryOptions, ...extraSubOptions]; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -262,10 +277,11 @@ export function PantryItemDialog({ item, locations, plants, onSuccess }: Props)
{ set("pantryCategory", v); set("pantrySubcategory", ""); }} - placeholder="Select category…" + placeholder="Select or add category…" + allowCustom />
@@ -274,9 +290,10 @@ export function PantryItemDialog({ item, locations, plants, onSuccess }: Props) options={subcategoryOptions} value={form.pantrySubcategory} onChange={v => set("pantrySubcategory", v)} - placeholder="Select subcategory…" + placeholder="Select or add…" disabled={!form.pantryCategory} - emptyText="No subcategories" + emptyText="Type to add a subcategory" + allowCustom />
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 07f824f..55e26c3 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import * as Popover from "@radix-ui/react-popover"; -import { Check, ChevronsUpDown, Search } from "lucide-react"; +import { Check, ChevronsUpDown, Plus, Search } from "lucide-react"; import { cn } from "@/lib/utils"; export type ComboOption = { value: string; label: string }; @@ -14,9 +14,10 @@ type Props = { placeholder?: string; disabled?: boolean; emptyText?: string; + allowCustom?: boolean; // show "Add '[query]'" when no exact match }; -export function Combobox({ options, value, onChange, placeholder = "Select…", disabled, emptyText = "No results" }: Props) { +export function Combobox({ options, value, onChange, placeholder = "Select…", disabled, emptyText = "No results", allowCustom = false }: Props) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const inputRef = useRef(null); @@ -25,7 +26,14 @@ export function Combobox({ options, value, onChange, placeholder = "Select…", ? options.filter(o => o.label.toLowerCase().includes(query.toLowerCase())) : options; + // Show "add" option when allowCustom and the query isn't already an exact match + const trimmed = query.trim(); + const exactMatch = options.some(o => o.label.toLowerCase() === trimmed.toLowerCase()); + const showAdd = allowCustom && trimmed.length > 0 && !exactMatch; + + // Display: if value matches a known option use its label, otherwise show value as-is (custom) const selected = options.find(o => o.value === value); + const displayValue = selected?.label ?? value; useEffect(() => { if (open) { @@ -34,6 +42,11 @@ export function Combobox({ options, value, onChange, placeholder = "Select…", } }, [open]); + function pick(val: string) { + onChange(val); + setOpen(false); + } + return ( @@ -43,10 +56,10 @@ export function Combobox({ options, value, onChange, placeholder = "Select…", className={cn( "w-full flex items-center justify-between rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring", disabled && "opacity-50 cursor-not-allowed", - !selected && "text-muted-foreground" + !displayValue && "text-muted-foreground" )} > - {selected?.label ?? placeholder} + {displayValue || placeholder} @@ -75,30 +88,42 @@ export function Combobox({ options, value, onChange, placeholder = "Select…", {value && ( )} - {filtered.length === 0 ? ( + {filtered.map(opt => ( + + ))} + + {filtered.length === 0 && !showAdd && (

{emptyText}

- ) : ( - filtered.map(opt => ( - - )) + )} + + {/* Add custom option */} + {showAdd && ( + )} diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index c814630..fda5e64 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -8,6 +8,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.18.1", + date: "2026-06-29", + changes: [ + "Pantry: you can now add a brand-new category or subcategory on the fly — just type the name into the Category or Subcategory dropdown and pick \"Add [name]\" at the bottom of the list. New categories are remembered in future add-item dialogs.", + ], + }, { version: "0.18.0", date: "2026-06-16", diff --git a/src/lib/version.ts b/src/lib/version.ts index 906b756..7349a50 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,2 +1,2 @@ // Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`. -export const APP_VERSION = "0.18.0"; +export const APP_VERSION = "0.18.1";