From 2dd6e04b44d17c63218c5ec2cf8d66cf63d004fb Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 21 Apr 2026 21:50:56 +0200 Subject: [PATCH] fix: treat new-category placeholder name '...' as a UI placeholder (#811) When a user adds a new packing category, the first item is seeded with name '...' because the server rejects empty names. That string was rendered as a real value in the input, forcing users to delete the dots before typing. Now we detect the sentinel, show it as a faint placeholder in the display span, and start the edit input empty (with '...' as the HTML placeholder). --- client/src/components/Packing/PackingListPanel.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx index d1bdb03d..9311cbc1 100644 --- a/client/src/components/Packing/PackingListPanel.tsx +++ b/client/src/components/Packing/PackingListPanel.tsx @@ -208,9 +208,14 @@ interface ArtikelZeileProps { canEdit?: boolean } +// A category's first item is seeded with this sentinel because the server +// rejects empty names. Treat it as a placeholder in the UI. +const PACKING_PLACEHOLDER_NAME = '...' + function ArtikelZeile({ item, tripId, categories, onCategoryChange, bagTrackingEnabled, bags = [], onCreateBag, canEdit = true }: ArtikelZeileProps) { + const isPlaceholder = item.name === PACKING_PLACEHOLDER_NAME const [editing, setEditing] = useState(false) - const [editName, setEditName] = useState(item.name) + const [editName, setEditName] = useState(isPlaceholder ? '' : item.name) const [hovered, setHovered] = useState(false) const [showCatPicker, setShowCatPicker] = useState(false) const [showBagPicker, setShowBagPicker] = useState(false) @@ -223,7 +228,7 @@ function ArtikelZeile({ item, tripId, categories, onCategoryChange, bagTrackingE const handleToggle = () => togglePackingItem(tripId, item.id, !item.checked) const handleSaveName = async () => { - if (!editName.trim()) { setEditing(false); setEditName(item.name); return } + if (!editName.trim()) { setEditing(false); setEditName(isPlaceholder ? '' : item.name); return } try { await updatePackingItem(tripId, item.id, { name: editName.trim() }); setEditing(false) } catch { toast.error(t('packing.toast.saveError')) } } @@ -275,9 +280,10 @@ function ArtikelZeile({ item, tripId, categories, onCategoryChange, bagTrackingE {editing && canEdit ? ( setEditName(e.target.value)} onBlur={handleSaveName} - onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditing(false); setEditName(item.name) } }} + onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditing(false); setEditName(isPlaceholder ? '' : item.name) } }} style={{ flex: 1, fontSize: 13.5, padding: '2px 8px', borderRadius: 6, border: '1px solid var(--border-primary)', outline: 'none', fontFamily: 'inherit' }} /> ) : ( @@ -286,7 +292,7 @@ function ArtikelZeile({ item, tripId, categories, onCategoryChange, bagTrackingE style={{ flex: 1, fontSize: 13.5, cursor: !canEdit || item.checked ? 'default' : 'text', - color: item.checked ? 'var(--text-faint)' : 'var(--text-primary)', + color: isPlaceholder ? 'var(--text-faint)' : (item.checked ? 'var(--text-faint)' : 'var(--text-primary)'), transition: 'color 200ms cubic-bezier(0.23,1,0.32,1)', textDecoration: item.checked ? 'line-through' : 'none', }}