diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 27de1a2f..49e839e3 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -910,6 +910,53 @@ describe('Sidebar', () => { fireEvent.click(screen.getByText('My Favorite Note')) expect(onSelect).toHaveBeenCalledWith({ kind: 'filter', filter: 'favorites' }) }) + + it('matches the Types row styling and type color for favorites', () => { + render( {}} />) + + const favoriteLabel = screen.getByText('My Favorite Note') + const favoriteRow = favoriteLabel.closest('.cursor-pointer') + const typeLabel = screen.getByText('Projects') + const typeRow = typeLabel.closest('.cursor-pointer') + const favoriteIcon = favoriteRow?.querySelector('svg') + + expect(favoriteRow?.className).toBe(typeRow?.className) + expect(favoriteRow?.style.padding).toBe(typeRow?.style.padding) + expect(favoriteRow?.style.gap).toBe(typeRow?.style.gap) + expect(favoriteLabel.className).toContain(typeLabel.className) + expect(favoriteLabel.className).toContain('truncate') + expect(favoriteIcon?.getAttribute('width')).toBe('16') + expect(favoriteIcon?.getAttribute('height')).toBe('16') + expect(favoriteIcon?.getAttribute('style')).toContain('var(--accent-red)') + }) + + it('falls back to a neutral icon color when the favorite type has no defined color', () => { + const customType: VaultEntry = { + path: '/vault/types/recipe.md', filename: 'recipe.md', title: 'Recipe', + isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, + cadence: null, archived: false, modifiedAt: 1700000000, + createdAt: null, fileSize: 120, snippet: '', wordCount: 0, relationships: {}, + icon: 'flask', color: null, order: null, sidebarLabel: null, template: null, + sort: null, view: null, visible: null, outgoingLinks: [], properties: {}, + } + const recipeFavorite: VaultEntry = { + path: '/vault/recipe/sourdough.md', filename: 'sourdough.md', title: 'Sourdough', + isA: 'Recipe', aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, + cadence: null, archived: false, modifiedAt: 1700000000, + createdAt: null, fileSize: 120, snippet: '', wordCount: 0, relationships: {}, + icon: null, color: null, order: null, sidebarLabel: null, template: null, + sort: null, view: null, visible: null, outgoingLinks: [], properties: {}, + favorite: true, favoriteIndex: 0, + } + + render( {}} />) + + const recipeRow = screen.getByText('Sourdough').closest('.cursor-pointer') + const recipeIcon = recipeRow?.querySelector('svg') + + expect(recipeIcon?.getAttribute('style')).toContain('var(--muted-foreground)') + expect(within(recipeRow as HTMLElement).getByText('Sourdough')).toBeInTheDocument() + }) }) describe('group separators', () => { diff --git a/src/components/sidebar/SidebarSections.tsx b/src/components/sidebar/SidebarSections.tsx index e0a31e2a..91d7403d 100644 --- a/src/components/sidebar/SidebarSections.tsx +++ b/src/components/sidebar/SidebarSections.tsx @@ -20,6 +20,7 @@ import { } from '../SidebarParts' import { TypeCustomizePopover } from '../TypeCustomizePopover' import { useDragRegion } from '../../hooks/useDragRegion' +import { buildTypeEntryMap, getTypeColor, getTypeLightColor } from '../../utils/typeColors' import { NoteTitleIcon } from '../NoteTitleIcon' export interface SidebarSectionProps { @@ -334,16 +335,46 @@ export function TypesSection({ ) } +const FAVORITE_TYPE_ICON_MAP: Record = { + Project: 'wrench', + project: 'wrench', + Experiment: 'flask', + experiment: 'flask', + Responsibility: 'target', + responsibility: 'target', + Procedure: 'arrows-clockwise', + procedure: 'arrows-clockwise', + Person: 'users', + person: 'users', + Event: 'calendar-blank', + event: 'calendar-blank', + Topic: 'tag', + topic: 'tag', + Type: 'stack-simple', + type: 'stack-simple', +} + +function getFavoriteIcon(entry: VaultEntry, typeEntryMap: Record) { + const typeEntry = entry.isA ? typeEntryMap[entry.isA] : undefined + return typeEntry?.icon ?? FAVORITE_TYPE_ICON_MAP[entry.isA ?? ''] ?? 'file-text' +} + function SortableFavoriteItem({ entry, isActive, onSelect, + typeEntryMap, }: { entry: VaultEntry isActive: boolean onSelect: () => void + typeEntryMap: Record }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: entry.path }) + const typeEntry = entry.isA ? typeEntryMap[entry.isA] : undefined + const icon = getFavoriteIcon(entry, typeEntryMap) + const typeColor = getTypeColor(entry.isA ?? null, typeEntry?.color) + const typeLightColor = getTypeLightColor(entry.isA ?? null, typeEntry?.color) return (
- - {entry.title} +
+ + + {entry.title} + +
) } +function sortFavorites(entries: VaultEntry[]) { + return entries + .filter((entry) => entry.favorite && !entry.archived) + .sort((a, b) => (a.favoriteIndex ?? Infinity) - (b.favoriteIndex ?? Infinity)) +} + +function reorderFavoriteIds(favoriteIds: string[], event: DragEndEvent) { + const { active, over } = event + if (!over || active.id === over.id) return null + const oldIndex = favoriteIds.indexOf(active.id as string) + const newIndex = favoriteIds.indexOf(over.id as string) + if (oldIndex === -1 || newIndex === -1) return null + return arrayMove(favoriteIds, oldIndex, newIndex) +} + export function FavoritesSection({ entries, selection, @@ -381,22 +431,14 @@ export function FavoritesSection({ collapsed: boolean onToggle: () => void }) { - const favorites = useMemo( - () => entries - .filter((entry) => entry.favorite && !entry.archived) - .sort((a, b) => (a.favoriteIndex ?? Infinity) - (b.favoriteIndex ?? Infinity)), - [entries], - ) + const favorites = useMemo(() => sortFavorites(entries), [entries]) const favoriteIds = useMemo(() => favorites.map((entry) => entry.path), [favorites]) + const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })) const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event - if (!over || active.id === over.id) return - const oldIndex = favoriteIds.indexOf(active.id as string) - const newIndex = favoriteIds.indexOf(over.id as string) - if (oldIndex === -1 || newIndex === -1) return - onReorder?.(arrayMove(favoriteIds, oldIndex, newIndex)) + const reordered = reorderFavoriteIds(favoriteIds, event) + if (reordered) onReorder?.(reordered) }, [favoriteIds, onReorder]) if (favorites.length === 0) return null @@ -413,6 +455,7 @@ export function FavoritesSection({ key={entry.path} entry={entry} isActive={isSelectionActive(selection, { kind: 'entity', entry })} + typeEntryMap={typeEntryMap} onSelect={() => { onSelect({ kind: 'filter', filter: 'favorites' }) onSelectNote?.(entry)