fix: sidebar section header reflects type icon, color, and label

- Add GearSix icon ('gear-six') to icon registry — was missing, causing
  Config type to show FileText fallback instead of its configured icon
- Add 'gray' to ACCENT_COLORS palette with CSS variables — was missing,
  causing Config type color to fall back to muted foreground
- Extract sidebar section logic to utils/sidebarSections.ts for testability
- Add Config type + instance to mock entries for browser dev mode
- Add tests: icon resolution, gray color, sidebar section builder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-11 20:44:19 +01:00
parent 5107cd13f4
commit aaa5bc2e81
9 changed files with 238 additions and 56 deletions

View File

@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest'
import { resolveIcon, ICON_OPTIONS } from './iconRegistry'
import { FileText, GearSix, CookingPot } from '@phosphor-icons/react'
describe('resolveIcon', () => {
it('returns FileText for null', () => {
expect(resolveIcon(null)).toBe(FileText)
})
it('returns FileText for unknown icon name', () => {
expect(resolveIcon('nonexistent-icon')).toBe(FileText)
})
it('resolves gear-six to GearSix', () => {
expect(resolveIcon('gear-six')).toBe(GearSix)
})
it('resolves cooking-pot to CookingPot', () => {
expect(resolveIcon('cooking-pot')).toBe(CookingPot)
})
})
describe('ICON_OPTIONS', () => {
it('includes gear-six', () => {
expect(ICON_OPTIONS.some((o) => o.name === 'gear-six')).toBe(true)
})
})

View File

@@ -13,7 +13,7 @@ import {
Door, Drop, Envelope, Eye, Eyeglasses, Factory, Fan, Farm, Feather, File,
FileCode, FileText, FilmReel, Fingerprint, Fire, FirstAid, Fish, Flag, Flame, Flashlight,
Flask, Flower, FlowerLotus, Folder, FolderOpen, Football, ForkKnife, Function, Funnel, GameController,
Gauge, Gavel, Gear, Ghost, Gift, GitBranch, Globe, GraduationCap, Guitar, Hammer,
Gauge, Gavel, Gear, GearSix, Ghost, Gift, GitBranch, Globe, GraduationCap, Guitar, Hammer,
Hand, Handshake, Headphones, Headset, Heart, Heartbeat, Horse, Hospital, Hourglass, House,
IceCream, IdentificationBadge, Image, Infinity as InfinityIcon, Island, Joystick, Kanban, Key, Keyboard, Knife,
Ladder, Lamp, Laptop, Leaf, Lifebuoy, Lightbulb, Lighthouse, Lightning, Link, List,
@@ -174,6 +174,7 @@ export const ICON_OPTIONS: IconEntry[] = [
{ name: 'gauge', Icon: Gauge },
{ name: 'gavel', Icon: Gavel },
{ name: 'gear', Icon: Gear },
{ name: 'gear-six', Icon: GearSix },
{ name: 'ghost', Icon: Ghost },
{ name: 'gift', Icon: Gift },
{ name: 'git-branch', Icon: GitBranch },

View File

@@ -0,0 +1,65 @@
/**
* Pure functions for building sidebar section groups from vault entries.
* Extracted from Sidebar.tsx for testability.
*/
import type { VaultEntry } from '../types'
import type { SectionGroup } from '../components/SidebarParts'
import { resolveIcon } from './iconRegistry'
import { pluralizeType } from '../hooks/useCommandRegistry'
import {
Wrench, Flask, Target, ArrowsClockwise,
Users, CalendarBlank, Tag, StackSimple,
} from '@phosphor-icons/react'
const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [
{ label: 'Projects', type: 'Project', Icon: Wrench },
{ label: 'Experiments', type: 'Experiment', Icon: Flask },
{ label: 'Responsibilities', type: 'Responsibility', Icon: Target },
{ label: 'Procedures', type: 'Procedure', Icon: ArrowsClockwise },
{ label: 'People', type: 'Person', Icon: Users },
{ label: 'Events', type: 'Event', Icon: CalendarBlank },
{ label: 'Topics', type: 'Topic', Icon: Tag },
{ label: 'Types', type: 'Type', Icon: StackSimple },
]
/** Metadata lookup for well-known types (icon/label only — NOT used to determine which sections to show) */
const BUILT_IN_TYPE_MAP = new Map(BUILT_IN_SECTION_GROUPS.map((sg) => [sg.type, sg]))
/** Collect unique isA values from active (non-trashed, non-archived) entries, excluding generic Note */
export function collectActiveTypes(entries: VaultEntry[]): Set<string> {
const types = new Set<string>()
for (const e of entries) {
if (e.isA && e.isA !== 'Note' && !e.trashed && !e.archived) types.add(e.isA)
}
return types
}
/** Build a single SectionGroup for a type, using built-in metadata or Type entry for icon/label */
export function buildSectionGroup(type: string, typeEntryMap: Record<string, VaultEntry>): SectionGroup {
const builtIn = BUILT_IN_TYPE_MAP.get(type)
const typeEntry = typeEntryMap[type]
const customColor = typeEntry?.color ?? null
const label = typeEntry?.sidebarLabel || (builtIn?.label ?? pluralizeType(type))
if (builtIn) {
const Icon = typeEntry?.icon ? resolveIcon(typeEntry.icon) : builtIn.Icon
return { ...builtIn, label, Icon, customColor }
}
return { label, type, Icon: resolveIcon(typeEntry?.icon ?? null), customColor }
}
/** Build sections dynamically from actual vault entries — only types with ≥1 active note appear */
export function buildDynamicSections(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>): SectionGroup[] {
const activeTypes = collectActiveTypes(entries)
return Array.from(activeTypes, (type) => buildSectionGroup(type, typeEntryMap))
}
export function sortSections(groups: SectionGroup[], typeEntryMap: Record<string, VaultEntry>): SectionGroup[] {
return [...groups].sort((a, b) => {
const orderA = typeEntryMap[a.type]?.order ?? Infinity
const orderB = typeEntryMap[b.type]?.order ?? Infinity
return orderA !== orderB ? orderA - orderB : a.label.localeCompare(b.label)
})
}
export { BUILT_IN_SECTION_GROUPS }

View File

@@ -28,6 +28,10 @@ describe('getTypeColor', () => {
it('ignores invalid custom color key', () => {
expect(getTypeColor('Project', 'invalid')).toBe('var(--accent-red)')
})
it('uses gray custom color key', () => {
expect(getTypeColor('Config', 'gray')).toBe('var(--accent-gray)')
})
})
describe('getTypeLightColor', () => {
@@ -47,6 +51,10 @@ describe('getTypeLightColor', () => {
it('uses custom color key for light variant', () => {
expect(getTypeLightColor('Recipe', 'purple')).toBe('var(--accent-purple-light)')
})
it('uses gray custom color key for light variant', () => {
expect(getTypeLightColor('Config', 'gray')).toBe('var(--accent-gray-light)')
})
})
const baseEntry: VaultEntry = {
@@ -54,7 +62,8 @@ const baseEntry: VaultEntry = {
status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null,
modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', relationships: {},
wordCount: 0,
icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [],
icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null,
view: null, visible: null, outgoingLinks: [], properties: {},
}
describe('buildTypeEntryMap', () => {
@@ -76,4 +85,14 @@ describe('buildTypeEntryMap', () => {
]
expect(buildTypeEntryMap(entries)).toEqual({})
})
it('preserves sidebarLabel in type entry', () => {
const entries: VaultEntry[] = [
{ ...baseEntry, title: 'Config', isA: 'Type', icon: 'gear-six', color: 'gray', sidebarLabel: 'Config' },
]
const map = buildTypeEntryMap(entries)
expect(map['Config'].sidebarLabel).toBe('Config')
expect(map['Config'].icon).toBe('gear-six')
expect(map['Config'].color).toBe('gray')
})
})

View File

@@ -47,6 +47,7 @@ export const ACCENT_COLORS: { key: string; label: string; css: string; cssLight:
{ key: 'purple', label: 'Purple', css: 'var(--accent-purple)', cssLight: 'var(--accent-purple-light)' },
{ key: 'teal', label: 'Teal', css: 'var(--accent-teal)', cssLight: 'var(--accent-teal-light)' },
{ key: 'pink', label: 'Pink', css: 'var(--accent-pink)', cssLight: 'var(--accent-pink-light)' },
{ key: 'gray', label: 'Gray', css: 'var(--accent-gray)', cssLight: 'var(--accent-gray-light)' },
]
const COLOR_KEY_TO_CSS: Record<string, string> = Object.fromEntries(