From 21ed9927277592704f07a3947ce03897fa16ce40 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 15 Feb 2026 15:21:47 +0100 Subject: [PATCH] feat: made category in the UI multi-select-capable --- src/renderer/components/Editor/Editor.css | 125 ++++++++++++++++++++++ src/renderer/components/Editor/Editor.tsx | 112 +++++++++++++++---- 2 files changed, 214 insertions(+), 23 deletions(-) diff --git a/src/renderer/components/Editor/Editor.css b/src/renderer/components/Editor/Editor.css index b0e3c08..d0b48e4 100644 --- a/src/renderer/components/Editor/Editor.css +++ b/src/renderer/components/Editor/Editor.css @@ -1082,3 +1082,128 @@ font-size: 11px; opacity: 0.7; } + +/* Multi-select dropdown for categories */ +.multi-select-dropdown { + position: relative; + width: 100%; +} + +.multi-select-trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px 10px; + border: 1px solid var(--vscode-input-border, #3c3c3c); + border-radius: 4px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 13px; + cursor: pointer; + text-align: left; +} + +.multi-select-trigger:hover { + border-color: var(--vscode-focusBorder, #007fd4); +} + +.multi-select-trigger:focus { + outline: none; + border-color: var(--vscode-focusBorder, #007fd4); +} + +.multi-select-value { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.multi-select-arrow { + font-size: 10px; + margin-left: 8px; + opacity: 0.7; +} + +.multi-select-menu { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 2px; + padding: 4px 0; + background: var(--vscode-dropdown-background, #3c3c3c); + border: 1px solid var(--vscode-dropdown-border, #3c3c3c); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + max-height: 200px; + overflow-y: auto; +} + +.multi-select-option { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + cursor: pointer; + font-size: 13px; + color: var(--vscode-dropdown-foreground); +} + +.multi-select-option:hover { + background: var(--vscode-list-hoverBackground, #2a2d2e); +} + +.multi-select-option input[type="checkbox"] { + margin: 0; + cursor: pointer; + accent-color: var(--vscode-focusBorder, #007fd4); +} + +.multi-select-option span { + flex: 1; +} + +/* Pills showing selected categories */ +.multi-select-pills { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.multi-select-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 6px 2px 8px; + background: var(--vscode-badge-background, #4d4d4d); + color: var(--vscode-badge-foreground, #ffffff); + border-radius: 12px; + font-size: 11px; +} + +.multi-select-pill-remove { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + padding: 0; + margin: 0; + border: none; + background: transparent; + color: var(--vscode-badge-foreground, #ffffff); + font-size: 12px; + line-height: 1; + cursor: pointer; + border-radius: 50%; + opacity: 0.7; +} + +.multi-select-pill-remove:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 60de147..f97c30c 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -39,9 +39,8 @@ const autoSaveManager = new AutoSaveManager({ const tagsStr = changes.tags as string; update.tags = tagsStr.split(',').map(t => t.trim()).filter(t => t.length > 0); } - if ('category' in changes) { - const cat = changes.category as string; - update.categories = cat ? [cat] : ['article']; + if ('categories' in changes) { + update.categories = changes.categories as string[]; } const updated = await window.electronAPI?.posts.update(id, update); @@ -645,8 +644,10 @@ const PostEditor: React.FC = ({ postId }) => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [tags, setTags] = useState([]); - const [category, setCategory] = useState('article'); + const [selectedCategories, setSelectedCategories] = useState(['article']); const [availableCategories, setAvailableCategories] = useState(['article', 'picture', 'aside', 'page']); + const [categoriesDropdownOpen, setCategoriesDropdownOpen] = useState(false); + const categoriesDropdownRef = useRef(null); const [isSaving, setIsSaving] = useState(false); const [hasPublishedVersion, setHasPublishedVersion] = useState(false); const hydrationOverlayRef = useRef(null); @@ -695,6 +696,19 @@ const PostEditor: React.FC = ({ postId }) => { loadCategories(); }, []); + // Close categories dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (categoriesDropdownRef.current && !categoriesDropdownRef.current.contains(event.target as Node)) { + setCategoriesDropdownOpen(false); + } + }; + if (categoriesDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [categoriesDropdownOpen]); + // Resolve media URLs in content for display const resolvedContent = useMemo(() => resolveMediaUrls(content, media), [content, media]); @@ -767,7 +781,7 @@ const PostEditor: React.FC = ({ postId }) => { title: string; content: string; tags: string[]; - category: string; + categories: string[]; postId: string; isDirty: boolean; } | null>(null); @@ -778,11 +792,11 @@ const PostEditor: React.FC = ({ postId }) => { title, content, tags, - category, + categories: selectedCategories, postId, isDirty, }; - }, [title, content, tags, category, postId, isDirty]); + }, [title, content, tags, selectedCategories, postId, isDirty]); // Auto-save when switching away from a post or unmounting useEffect(() => { @@ -798,7 +812,7 @@ const PostEditor: React.FC = ({ postId }) => { title: pending.title, content: pending.content, tags: pending.tags, - categories: pending.category ? [pending.category] : ['article'], + categories: pending.categories.length > 0 ? pending.categories : ['article'], }).then((updated) => { if (updated) { useAppStore.getState().updatePost(pending.postId, updated as Partial); @@ -818,7 +832,7 @@ const PostEditor: React.FC = ({ postId }) => { setTitle(post.title); setContent(post.content); setTags(post.tags); - setCategory(post.categories[0] || 'article'); + setSelectedCategories(post.categories.length > 0 ? post.categories : ['article']); markClean(postId); // Mark as initialized AFTER setting local state setIsInitialized(true); @@ -829,13 +843,13 @@ const PostEditor: React.FC = ({ postId }) => { // Only run after form has been initialized from post data useEffect(() => { if (!post || !isInitialized) return; - const currentCategory = post.categories[0] || 'article'; const tagsChanged = JSON.stringify(tags.slice().sort()) !== JSON.stringify(post.tags.slice().sort()); + const categoriesChanged = JSON.stringify(selectedCategories.slice().sort()) !== JSON.stringify(post.categories.slice().sort()); const hasChanges = title !== post.title || content !== post.content || tagsChanged || - category !== currentCategory; + categoriesChanged; if (hasChanges) { markDirty(postId); @@ -845,12 +859,12 @@ const PostEditor: React.FC = ({ postId }) => { title, content, tags: tags.join(', '), - category, + categories: selectedCategories, }); } else { markClean(postId); } - }, [title, content, tags, category, post, postId, isInitialized, markDirty, markClean]); + }, [title, content, tags, selectedCategories, post, postId, isInitialized, markDirty, markClean]); // Handle editor mode change and persist preference const handleEditorModeChange = (mode: EditorMode) => { @@ -930,7 +944,7 @@ const PostEditor: React.FC = ({ postId }) => { setTitle(reverted.title); setContent(reverted.content); setTags(reverted.tags); - setCategory(reverted.categories[0] || 'article'); + setSelectedCategories(reverted.categories.length > 0 ? reverted.categories : ['article']); // Update local post state so UI reflects the published status setPost(reverted as PostData); updatePost(postId, reverted as Partial); @@ -1264,15 +1278,67 @@ const PostEditor: React.FC = ({ postId }) => { />
- - + +
+ + {categoriesDropdownOpen && ( +
+ {availableCategories.map((cat) => ( + + ))} +
+ )} +
+ {selectedCategories.length > 1 && ( +
+ {selectedCategories.map((cat) => ( + + {cat} + + + ))} +
+ )}