From deb0f3ae3b4b1601126ff89f21201c2050e00a8c Mon Sep 17 00:00:00 2001 From: hugo Date: Fri, 13 Feb 2026 10:34:15 +0100 Subject: [PATCH] feat: tag clouds use colors now --- src/renderer/components/Editor/Editor.css | 8 ++ src/renderer/components/Editor/Editor.tsx | 69 ++++++++-- src/renderer/components/Sidebar/Sidebar.css | 15 ++- src/renderer/components/Sidebar/Sidebar.tsx | 142 +++++++++++++++----- 4 files changed, 189 insertions(+), 45 deletions(-) diff --git a/src/renderer/components/Editor/Editor.css b/src/renderer/components/Editor/Editor.css index 3d22d19..166626c 100644 --- a/src/renderer/components/Editor/Editor.css +++ b/src/renderer/components/Editor/Editor.css @@ -554,6 +554,14 @@ opacity: 0.75; } +.dashboard-tag.has-color { + border-radius: 12px; +} + +.dashboard-tag.has-color:hover { + opacity: 0.85; +} + .tag-cloud-more { font-size: 11px; } diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 711f5bb..b29f48a 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -1278,26 +1278,60 @@ interface CategoryCount { count: number; } +interface TagDataWithColor { + id: string; + name: string; + color?: string; +} + +// Get contrasting text color for background +const getContrastColor = (hex: string): string => { + const color = hex.replace('#', ''); + let r: number, g: number, b: number; + if (color.length === 3) { + r = parseInt(color[0] + color[0], 16); + g = parseInt(color[1] + color[1], 16); + b = parseInt(color[2] + color[2], 16); + } else { + r = parseInt(color.substring(0, 2), 16); + g = parseInt(color.substring(2, 4), 16); + b = parseInt(color.substring(4, 6), 16); + } + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.5 ? '#000000' : '#ffffff'; +}; + const Dashboard: React.FC = () => { const { posts, media } = useAppStore(); const [stats, setStats] = useState(null); const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]); const [tagCounts, setTagCounts] = useState([]); + const [tagColors, setTagColors] = useState>(new Map()); const [categoryCounts, setCategoryCounts] = useState([]); useEffect(() => { const loadStats = async () => { try { - const [ds, ym, tc, cc] = await Promise.all([ + const [ds, ym, tc, cc, allTagsData] = await Promise.all([ window.electronAPI?.posts.getDashboardStats(), window.electronAPI?.posts.getByYearMonth(), window.electronAPI?.posts.getTagsWithCounts(), window.electronAPI?.posts.getCategoriesWithCounts(), + window.electronAPI?.tags.getAll(), ]); if (ds) setStats(ds); if (ym) setYearMonthData(ym); if (tc) setTagCounts(tc); if (cc) setCategoryCounts(cc); + if (allTagsData) { + const colorMap = new Map(); + for (const tag of allTagsData as TagDataWithColor[]) { + if (tag.color) { + colorMap.set(tag.name, tag.color); + } + } + setTagColors(colorMap); + } } catch (e) { console.error('Failed to load dashboard stats:', e); } @@ -1333,8 +1367,9 @@ const Dashboard: React.FC = () => { return items.map(t => ({ ...t, fontSize: 11 + ((t.count - minTagCount) / range) * 11, + color: tagColors.get(t.tag), })).sort((a, b) => a.tag.localeCompare(b.tag)); // alphabetical for cloud layout - }, [tagCounts]); + }, [tagCounts, tagColors]); const displayTotalPosts = stats?.totalPosts ?? posts.length; const displayDraftCount = stats?.draftCount ?? 0; @@ -1394,16 +1429,26 @@ const Dashboard: React.FC = () => {

Tags

- {tagCloudItems.map(item => ( - - {item.tag} - - ))} + {tagCloudItems.map(item => { + const hasColor = !!item.color; + const style: React.CSSProperties = hasColor + ? { + fontSize: `${item.fontSize}px`, + backgroundColor: item.color, + color: getContrastColor(item.color!), + } + : { fontSize: `${item.fontSize}px` }; + return ( + + {item.tag} + + ); + })} {tagCounts.length > 40 && +{tagCounts.length - 40} more}
diff --git a/src/renderer/components/Sidebar/Sidebar.css b/src/renderer/components/Sidebar/Sidebar.css index 8cddf4d..0e15c42 100644 --- a/src/renderer/components/Sidebar/Sidebar.css +++ b/src/renderer/components/Sidebar/Sidebar.css @@ -545,7 +545,7 @@ font-size: 11px; border-radius: 12px; cursor: pointer; - transition: background-color 0.15s; + transition: background-color 0.15s, opacity 0.15s; } .filter-chip:hover { @@ -557,6 +557,19 @@ color: var(--vscode-button-foreground); } +/* Colored filter chips (tags with assigned colors) */ +.filter-chip.has-color { + border: 1px solid transparent; +} + +.filter-chip.has-color:hover { + opacity: 0.85; +} + +.filter-chip.has-color.active { + box-shadow: 0 0 0 2px var(--vscode-focusBorder, #007fd4); +} + /* Filter Status */ .filter-status { display: flex; diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx index 7073d96..90043bd 100644 --- a/src/renderer/components/Sidebar/Sidebar.tsx +++ b/src/renderer/components/Sidebar/Sidebar.tsx @@ -4,6 +4,30 @@ import { showToast } from '../Toast'; import type { ChatConversation } from '../../types/electron'; import './Sidebar.css'; +// Tag data with color information +interface TagData { + id: string; + name: string; + color?: string; +} + +// Get contrasting text color for background +const getContrastColor = (hex: string): string => { + const color = hex.replace('#', ''); + let r: number, g: number, b: number; + if (color.length === 3) { + r = parseInt(color[0] + color[0], 16); + g = parseInt(color[1] + color[1], 16); + b = parseInt(color[2] + color[2], 16); + } else { + r = parseInt(color.substring(0, 2), 16); + g = parseInt(color.substring(2, 4), 16); + b = parseInt(color.substring(4, 6), 16); + } + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.5 ? '#000000' : '#ffffff'; +}; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); @@ -123,6 +147,7 @@ const CalendarView: React.FC = ({ onDateSelect, selectedYear, interface FilterPanelProps { tags: string[]; + tagColors: Map; categories: string[]; selectedTags: string[]; selectedCategories: string[]; @@ -132,6 +157,7 @@ interface FilterPanelProps { const FilterPanel: React.FC = ({ tags, + tagColors, categories, selectedTags, selectedCategories, @@ -144,21 +170,33 @@ const FilterPanel: React.FC = ({
TAGS
- {tags.map(tag => ( - - ))} + : {}; + return ( + + ); + })}
)} @@ -273,12 +311,14 @@ const MediaCalendarView: React.FC = ({ onDateSelect, sel // Media-specific filter panel interface MediaFilterPanelProps { tags: string[]; + tagColors: Map; selectedTags: string[]; onTagSelect: (tags: string[]) => void; } const MediaFilterPanel: React.FC = ({ tags, + tagColors, selectedTags, onTagSelect, }) => { @@ -288,21 +328,33 @@ const MediaFilterPanel: React.FC = ({
TAGS
- {tags.map(tag => ( - - ))} + : {}; + return ( + + ); + })}
)} @@ -355,20 +407,31 @@ const PostsList: React.FC = () => { const [selectedTags, setSelectedTags] = useState([]); const [selectedCategories, setSelectedCategories] = useState([]); const [availableTags, setAvailableTags] = useState([]); + const [tagColors, setTagColors] = useState>(new Map()); const [availableCategories, setAvailableCategories] = useState([]); const [showFilters, setShowFilters] = useState(false); const [filteredPosts, setFilteredPosts] = useState(null); const [isLoadingMore, setIsLoadingMore] = useState(false); - // Load available tags and categories + // Load available tags with colors and categories useEffect(() => { const loadFilters = async () => { - const [tags, categories] = await Promise.all([ + const [tags, categories, allTagsData] = await Promise.all([ window.electronAPI?.posts.getTags(), window.electronAPI?.posts.getCategories(), + window.electronAPI?.tags.getAll(), ]); if (tags) setAvailableTags(tags as string[]); if (categories) setAvailableCategories(categories as string[]); + if (allTagsData) { + const colorMap = new Map(); + for (const tag of allTagsData as TagData[]) { + if (tag.color) { + colorMap.set(tag.name, tag.color); + } + } + setTagColors(colorMap); + } }; loadFilters(); }, [posts]); @@ -553,6 +616,7 @@ const PostsList: React.FC = () => { /> { const [selectedMonth, setSelectedMonth] = useState(); const [selectedTags, setSelectedTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); + const [tagColors, setTagColors] = useState>(new Map()); const [showFilters, setShowFilters] = useState(false); const [filteredMedia, setFilteredMedia] = useState(null); - // Load available tags + // Load available tags with colors useEffect(() => { const loadTags = async () => { - const tags = await window.electronAPI?.media.getTags(); + const [tags, allTagsData] = await Promise.all([ + window.electronAPI?.media.getTags(), + window.electronAPI?.tags.getAll(), + ]); if (tags) setAvailableTags(tags as string[]); + if (allTagsData) { + const colorMap = new Map(); + for (const tag of allTagsData as TagData[]) { + if (tag.color) { + colorMap.set(tag.name, tag.color); + } + } + setTagColors(colorMap); + } }; loadTags(); }, [media]); @@ -843,6 +920,7 @@ const MediaList: React.FC = () => { />