Compare commits

...

1 Commits

Author SHA1 Message Date
Test
42b1e18cc4 fix: align favorites sidebar rows 2026-04-10 12:32:22 +02:00
2 changed files with 106 additions and 16 deletions

View File

@@ -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(<Sidebar entries={[...mockEntries, favEntry]} selection={defaultSelection} onSelect={() => {}} />)
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(<Sidebar entries={[...mockEntries, customType, recipeFavorite]} selection={defaultSelection} onSelect={() => {}} />)
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', () => {

View File

@@ -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<string, string> = {
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<string, VaultEntry>) {
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<string, VaultEntry>
}) {
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 (
<div
@@ -353,17 +384,36 @@ function SortableFavoriteItem({
{...listeners}
>
<div
className={`flex cursor-pointer select-none items-center gap-1.5 rounded text-[13px] font-normal transition-colors ${isActive ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:bg-accent'}`}
style={{ padding: '4px 16px 4px 28px' }}
className={`group/section flex cursor-pointer select-none items-center justify-between rounded transition-colors ${isActive ? '' : 'hover:bg-accent'}`}
style={{ padding: '6px 8px 6px 16px', borderRadius: 4, gap: 4, ...(isActive ? { background: typeLightColor } : {}) }}
onClick={onSelect}
>
<NoteTitleIcon icon={entry.icon} size={14} />
<span className="truncate">{entry.title}</span>
<div className="flex min-w-0 flex-1 items-center" style={{ gap: 4 }}>
<NoteTitleIcon icon={icon} size={16} color={typeColor} />
<span className="truncate text-[13px] font-medium" style={{ marginLeft: 4, color: isActive ? typeColor : undefined }}>
{entry.title}
</span>
</div>
</div>
</div>
)
}
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)