From 057fab8383c38008e441342ef973efef93e78d6e Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Fri, 29 May 2026 21:06:51 -0700 Subject: [PATCH 1/4] fix(catalog): whole card opens the editor, New form captures primary content in one step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related UX gaps in the freshly-landed Catalog page (PR #542): 1. Only the small name text in each card was a Link — clicking the card body, badge, tags, or empty space did nothing, leaving users without an obvious path to the editor. The whole card is now the link; the delete button overlays the top-right corner and stops propagation so it still works independently. 2. The inline 'New' form only had Type + Name, so users had to create, then click in, then fill, then save just to capture a one-line idea. The form now includes an optional content textarea that lands in the per-type primary field (physicalDescription for character, description for place/object, summary for idea/scene/concept) — matching the field labels in CatalogIngredient.jsx. --- client/src/pages/Catalog.jsx | 137 +++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 48 deletions(-) diff --git a/client/src/pages/Catalog.jsx b/client/src/pages/Catalog.jsx index a2e758049e..806106a368 100644 --- a/client/src/pages/Catalog.jsx +++ b/client/src/pages/Catalog.jsx @@ -30,6 +30,19 @@ const TYPES = [ const TYPE_BY_ID = Object.fromEntries(TYPES.map((t) => [t.id, t])); +// Per-type primary content key used by the inline "New" form, so users can +// capture the body in one step instead of bouncing into the editor. Mirrors +// the labels in CatalogIngredient.jsx — characters land in `physicalDescription` +// (canon shape), place/object in `description`, light types in `summary`. +const PRIMARY_CONTENT_KEY = { + character: 'physicalDescription', + place: 'description', + object: 'description', + idea: 'summary', + scene: 'summary', + concept: 'summary', +}; + // Pull a short snippet from the type-specific payload — first hit wins, // trimmed and ellipsised to ~120 chars. Characters use `physicalDescription` // (canon shape), so check it first to avoid rendering empty rows for @@ -61,9 +74,12 @@ export default function Catalog() { // debounced value that actually drives the list fetch. 300ms gap. const [searchInput, setSearchInput] = useState(''); const [q, setQ] = useState(''); - // Inline create form + // Inline create form. `content` is a single freeform textarea that lands in + // the per-type primary content field on submit (physicalDescription for + // character, description for place/object, summary for idea/scene/concept) + // so users don't have to navigate to the editor just to capture the body. const [showForm, setShowForm] = useState(false); - const [form, setForm] = useState({ type: 'character', name: '' }); + const [form, setForm] = useState({ type: 'character', name: '', content: '' }); const [creating, setCreating] = useState(false); // Armed-row id for two-click delete (no window.confirm). const [armedId, setArmedId] = useState(null); @@ -111,11 +127,16 @@ export default function Catalog() { e.preventDefault(); const name = form.name.trim(); if (!name) return; + const content = form.content.trim(); + const payload = {}; + if (content) { + payload[PRIMARY_CONTENT_KEY[form.type] || 'description'] = content; + } setCreating(true); const created = await createCatalogIngredient({ type: form.type, name, - payload: {}, + payload, tags: [], }, { silent: true }).catch((err) => { toast.error(err?.message || 'Failed to create ingredient'); @@ -124,7 +145,7 @@ export default function Catalog() { setCreating(false); if (!created) return; toast.success(`Created ${form.type} "${name}"`); - setForm({ type: form.type, name: '' }); + setForm({ type: form.type, name: '', content: '' }); setShowForm(false); // Update list locally (CLAUDE.md: prefer state update over refetch) but // still refresh stats so the type-chip counts move. @@ -221,8 +242,8 @@ export default function Catalog() { {showForm && ( -
-
+ +
-
- - -
+
+
+ +