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";