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