diff --git a/design/autocomplete-type-color.pen b/design/autocomplete-type-color.pen new file mode 100644 index 00000000..a3af113b --- /dev/null +++ b/design/autocomplete-type-color.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"frame1","x":0,"y":0,"name":"Autocomplete β€” correct type colors","clip":true,"width":500,"height":400,"fill":"#FFFFFF","layout":"vertical","gap":0,"children":[{"type":"frame","id":"header","name":"Search Input","width":"fill_container","height":44,"fill":"#FFFFFF","stroke":{"align":"inside","thickness":{"bottom":1},"fill":"#E5E7EB"},"padding":[0,16],"alignItems":"center","children":[{"type":"text","id":"searchText","content":"[[ever","fontSize":15,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"results","name":"Results List","width":"fill_container","layout":"vertical","gap":0,"fill":"#FFFFFF","children":[{"type":"frame","id":"row1","name":"Result Row β€” Typed (Evergreen)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","fill":"#F3F4F6","children":[{"type":"frame","id":"row1Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon1","content":"🌿","fontSize":14},{"type":"text","id":"title1","content":"Evergreen: Writing Process","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge1","padding":[2,8],"cornerRadius":4,"fill":"#ECFDF5","children":[{"type":"text","id":"badgeText1","content":"Evergreen","fontSize":11,"fontFamily":"Inter","fill":"#10B981","fontWeight":"500"}]}]},{"type":"frame","id":"row2","name":"Result Row β€” Typed (Project)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row2Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon2","content":"πŸ”§","fontSize":14},{"type":"text","id":"title2","content":"Evergreen Garden Project","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge2","padding":[2,8],"cornerRadius":4,"fill":"#FEF2F2","children":[{"type":"text","id":"badgeText2","content":"Project","fontSize":11,"fontFamily":"Inter","fill":"#EF4444","fontWeight":"500"}]}]},{"type":"frame","id":"row3","name":"Result Row β€” Typed (Topic)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row3Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon3","content":"🏷️","fontSize":14},{"type":"text","id":"title3","content":"Evergreen Notes Philosophy","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge3","padding":[2,8],"cornerRadius":4,"fill":"#ECFDF5","children":[{"type":"text","id":"badgeText3","content":"Topic","fontSize":11,"fontFamily":"Inter","fill":"#10B981","fontWeight":"500"}]}]},{"type":"frame","id":"row4","name":"Result Row β€” Untyped (neutral grey)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row4Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"title4","content":"My evergreen garden notes","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]}]},{"type":"frame","id":"row5","name":"Result Row β€” Typed (Person)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row5Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon5","content":"πŸ‘€","fontSize":14},{"type":"text","id":"title5","content":"Evergreen Smith","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge5","padding":[2,8],"cornerRadius":4,"fill":"#FFFBEB","children":[{"type":"text","id":"badgeText5","content":"Person","fontSize":11,"fontFamily":"Inter","fill":"#F59E0B","fontWeight":"500"}]}]}]}]},{"type":"frame","id":"frame2","x":0,"y":440,"name":"Legend β€” Type Color Mapping","width":500,"height":200,"fill":"#FFFFFF","layout":"vertical","gap":8,"padding":16,"children":[{"type":"text","id":"legendTitle","content":"Type β†’ Color Mapping (Fixed)","fontSize":16,"fontFamily":"Inter","fill":"#1F2937","fontWeight":"700"},{"type":"text","id":"legend1","content":"β€’ Project/Experiment β†’ Red (--accent-red)","fontSize":12,"fontFamily":"Inter","fill":"#EF4444"},{"type":"text","id":"legend2","content":"β€’ Responsibility/Procedure β†’ Purple (--accent-purple)","fontSize":12,"fontFamily":"Inter","fill":"#8B5CF6"},{"type":"text","id":"legend3","content":"β€’ Person/Event β†’ Yellow (--accent-yellow)","fontSize":12,"fontFamily":"Inter","fill":"#F59E0B"},{"type":"text","id":"legend4","content":"β€’ Topic β†’ Green (--accent-green)","fontSize":12,"fontFamily":"Inter","fill":"#10B981"},{"type":"text","id":"legend5","content":"β€’ Type β†’ Blue (--accent-blue)","fontSize":12,"fontFamily":"Inter","fill":"#3B82F6"},{"type":"text","id":"legend6","content":"β€’ Custom types β†’ custom color from Type document","fontSize":12,"fontFamily":"Inter","fill":"#6B7280"},{"type":"text","id":"legend7","content":"β€’ Untyped (Note/null) β†’ Grey (--muted-foreground), no icon","fontSize":12,"fontFamily":"Inter","fill":"#9CA3AF"}]}],"variables":{}} \ No newline at end of file diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index 2c88521d..e756a9c8 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -390,11 +390,11 @@ describe('wikilink autocomplete', () => { mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items) }) - it('always includes noteType for every item, including default Note type (regression)', async () => { + it('shows correct noteType and color for typed entries, neutral for untyped', async () => { const mixedEntries: VaultEntry[] = [ - { ...mockEntry, title: 'My Project', filename: 'proj.md', path: '/vault/proj.md', isA: 'Project', aliases: [] }, - { ...mockEntry, title: 'Plain Note', filename: 'plain.md', path: '/vault/plain.md', isA: null, aliases: [] }, - { ...mockEntry, title: 'Explicit Note', filename: 'explicit.md', path: '/vault/explicit.md', isA: 'Note', aliases: [] }, + { ...mockEntry, title: 'Test Project', filename: 'proj.md', path: '/vault/proj.md', isA: 'Project', aliases: [] }, + { ...mockEntry, title: 'Test Plain', filename: 'plain.md', path: '/vault/plain.md', isA: null, aliases: [] }, + { ...mockEntry, title: 'Test Explicit', filename: 'explicit.md', path: '/vault/explicit.md', isA: 'Note', aliases: [] }, ] capturedGetItems = null mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items) @@ -406,16 +406,21 @@ describe('wikilink autocomplete', () => { entries={mixedEntries} /> ) - const items = await capturedGetItems!('Note') - // Every item must have a defined noteType β€” none should be blank - for (const item of items) { - expect(item.noteType).toBeTruthy() - expect(item.typeColor).toBeTruthy() - } - // Default notes (isA: null) should show 'Note' as their type - const plainNote = items.find((i: { title: string }) => i.title === 'Plain Note') + const items = await capturedGetItems!('Test') + // Typed entries should have noteType and color + const project = items.find((i: { title: string }) => i.title === 'Test Project') + expect(project).toBeDefined() + expect(project!.noteType).toBe('Project') + expect(project!.typeColor).toBeTruthy() + // Untyped entries (isA: null or 'Note') should have no noteType (grey/neutral) + const plainNote = items.find((i: { title: string }) => i.title === 'Test Plain') expect(plainNote).toBeDefined() - expect(plainNote!.noteType).toBe('Note') + expect(plainNote!.noteType).toBeUndefined() + expect(plainNote!.typeColor).toBeUndefined() + const explicitNote = items.find((i: { title: string }) => i.title === 'Test Explicit') + expect(explicitNote).toBeDefined() + expect(explicitNote!.noteType).toBeUndefined() + expect(explicitNote!.typeColor).toBeUndefined() mockFilterSuggestionItems.mockImplementation((items: unknown[]) => items) }) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index c1973376..4438f8c3 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -17,7 +17,8 @@ import { splitFrontmatter, preProcessWikilinks, injectWikilinks, restoreWikilink import { preFilterWikilinks, deduplicateByPath, disambiguateTitles, MAX_RESULTS, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions' import { filterPersonMentions, PERSON_MENTION_MIN_QUERY } from '../utils/personMentionSuggestions' import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors' -import { getTypeColor } from '../utils/typeColors' +import { getTypeColor, buildTypeEntryMap } from '../utils/typeColors' +import { getTypeIcon } from './NoteItem' import { WikilinkSuggestionMenu, type WikilinkSuggestionItem } from './WikilinkSuggestionMenu' import './Editor.css' import './EditorTheme.css' @@ -138,6 +139,8 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e return () => container.removeEventListener('click', handler as EventListener, true) }, [editor]) + const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) + const baseItems = useMemo( () => deduplicateByPath(entries.map(entry => ({ title: entry.title, @@ -167,12 +170,17 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e })) const filtered = filterSuggestionItems(items, query).slice(0, MAX_RESULTS) const final = disambiguateTitles(deduplicateByPath(filtered)) - return final.map(({ group, ...rest }) => ({ - ...rest, - noteType: group, - typeColor: getTypeColor(group), - })) - }, [baseItems, editor]) + return final.map(({ group, ...rest }) => { + const noteType = group !== 'Note' ? group : undefined + const te = typeEntryMap[group] + return { + ...rest, + noteType, + typeColor: noteType ? getTypeColor(group, te?.color) : undefined, + TypeIcon: noteType ? getTypeIcon(group, te?.icon) : undefined, + } + }) + }, [baseItems, editor, typeEntryMap]) const getPersonMentionItems = useCallback(async (query: string): Promise => { if (query.length < PERSON_MENTION_MIN_QUERY) return [] @@ -192,12 +200,17 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e })) const filtered = filterSuggestionItems(items, query).slice(0, MAX_RESULTS) const final = disambiguateTitles(deduplicateByPath(filtered)) - return final.map(({ group, ...rest }) => ({ - ...rest, - noteType: group, - typeColor: getTypeColor(group), - })) - }, [baseItems, editor]) + return final.map(({ group, ...rest }) => { + const noteType = group !== 'Note' ? group : undefined + const te = typeEntryMap[group] + return { + ...rest, + noteType, + typeColor: noteType ? getTypeColor(group, te?.color) : undefined, + TypeIcon: noteType ? getTypeIcon(group, te?.icon) : undefined, + } + }) + }, [baseItems, editor, typeEntryMap]) return (
diff --git a/src/components/NoteAutocomplete.tsx b/src/components/NoteAutocomplete.tsx index 8a08af76..f0987993 100644 --- a/src/components/NoteAutocomplete.tsx +++ b/src/components/NoteAutocomplete.tsx @@ -1,6 +1,7 @@ -import { useState, useRef, useCallback, useMemo, useEffect } from 'react' +import { useState, useRef, useCallback, useMemo, useEffect, type ComponentType, type SVGAttributes } from 'react' import type { VaultEntry } from '../types' import { getTypeColor } from '../utils/typeColors' +import { getTypeIcon } from './NoteItem' import './WikilinkSuggestionMenu.css' const MIN_QUERY_LENGTH = 2 @@ -22,6 +23,7 @@ interface MatchedEntry { title: string noteType?: string typeColor?: string + TypeIcon?: ComponentType> } function matchEntries(entries: VaultEntry[], typeEntryMap: Record, query: string): MatchedEntry[] { @@ -39,6 +41,7 @@ function matchEntries(entries: VaultEntry[], typeEntryMap: Record handleSelect(item.title)} onMouseEnter={() => setSelectedIndex(index)} > - {item.title} + + {item.TypeIcon && } + {item.title} + {item.noteType && ( {item.noteType} diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 8b5951ff..13d09840 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input' import { MagnifyingGlass, Plus, CaretDown, CaretRight, Warning, } from '@phosphor-icons/react' -import { getTypeColor, getTypeLightColor } from '../utils/typeColors' +import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors' import { NoteItem, getTypeIcon } from './NoteItem' import { SortDropdown } from './SortDropdown' import { @@ -106,13 +106,7 @@ function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntr } function useTypeEntryMap(entries: VaultEntry[]) { - return useMemo(() => { - const map: Record = {} - for (const e of entries) { - if (e.isA === 'Type') map[e.title] = e - } - return map - }, [entries]) + return useMemo(() => buildTypeEntryMap(entries), [entries]) } // --- View sub-components --- diff --git a/src/components/NoteSearchList.tsx b/src/components/NoteSearchList.tsx index 1e239142..98cb26e8 100644 --- a/src/components/NoteSearchList.tsx +++ b/src/components/NoteSearchList.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect } from 'react' +import { useRef, useEffect, type ComponentType, type SVGAttributes } from 'react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' @@ -6,6 +6,7 @@ export interface NoteSearchResultItem { title: string noteType?: string typeColor?: string + TypeIcon?: ComponentType> } interface NoteSearchListProps { @@ -57,8 +58,16 @@ export function NoteSearchList({ onClick={() => onItemClick(item, i)} onMouseEnter={() => onItemHover?.(i)} > - - {item.title} + + {item.TypeIcon && ( + + )} + {item.title} {item.noteType && ( window.removeEventListener('keydown', handleKey) }, [open, results, selectedIndex, handleSelect, setSelectedIndex, onClose]) - if (!open) return null + const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) + const entryLookup = useMemo(() => { + const map = new Map() + for (const e of entries) map.set(e.path, { isA: e.isA }) + return map + }, [entries]) - const entryTypeMap = new Map(entries.map(e => [e.path, e.isA])) + if (!open) return null return (
+ entryLookup: Map + typeEntryMap: Record listRef: React.RefObject onSelect: (result: SearchResult) => void onHover: (index: number) => void } function SearchContent({ - query, results, selectedIndex, loading, elapsedMs, entryTypeMap, listRef, onSelect, onHover, + query, results, selectedIndex, loading, elapsedMs, entryLookup, typeEntryMap, listRef, onSelect, onHover, }: SearchContentProps) { return (
@@ -181,7 +190,9 @@ function SearchContent({
{results.map((result, i) => { - const noteType = entryTypeMap.get(result.path) ?? result.noteType + const isA = entryLookup.get(result.path)?.isA ?? result.noteType + const noteType = isA && isA !== 'Note' ? isA : null + const typeColor = noteType ? getTypeColor(isA, typeEntryMap[isA ?? '']?.color) : undefined return (
{result.title} {noteType && ( - + {noteType} )} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f3a82793..b185cf38 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useRef, useEffect, useCallback, memo } from 'react' import type { VaultEntry, SidebarSelection } from '../types' import { resolveIcon } from '../utils/iconRegistry' +import { buildTypeEntryMap } from '../utils/typeColors' import { TypeCustomizePopover } from './TypeCustomizePopover' import { useSectionVisibility } from '../hooks/useSectionVisibility' import { @@ -62,12 +63,6 @@ function useOutsideClick(ref: React.RefObject, isOpen: boole }, [ref, isOpen, onClose]) } -function buildTypeEntryMap(entries: VaultEntry[]): Record { - const map: Record = {} - for (const e of entries) { if (e.isA === 'Type') map[e.title] = e } - return map -} - function applyOverrides(typeEntryMap: Record): SectionGroup[] { return BUILT_IN_SECTION_GROUPS.map((sg) => { const te = typeEntryMap[sg.type] diff --git a/src/components/WikilinkSuggestionMenu.tsx b/src/components/WikilinkSuggestionMenu.tsx index d6a5620d..e6198330 100644 --- a/src/components/WikilinkSuggestionMenu.tsx +++ b/src/components/WikilinkSuggestionMenu.tsx @@ -1,3 +1,4 @@ +import type { ComponentType, SVGAttributes } from 'react' import { NoteSearchList } from './NoteSearchList' import './WikilinkSuggestionMenu.css' @@ -6,6 +7,7 @@ export interface WikilinkSuggestionItem { onItemClick: () => void noteType?: string typeColor?: string + TypeIcon?: ComponentType> aliases?: string[] entryTitle?: string path?: string diff --git a/src/hooks/useNoteSearch.test.ts b/src/hooks/useNoteSearch.test.ts index 8b10efee..44da8007 100644 --- a/src/hooks/useNoteSearch.test.ts +++ b/src/hooks/useNoteSearch.test.ts @@ -171,4 +171,21 @@ describe('useNoteSearch', () => { }) expect(preventDefaultSpy).not.toHaveBeenCalled() }) + + it('resolves custom type color from Type entries', () => { + const withTypes: VaultEntry[] = [ + makeEntry({ path: '/vault/t/recipe.md', title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' }), + makeEntry({ path: '/vault/pasta.md', title: 'Pasta', isA: 'Recipe', modifiedAt: 1700000010 }), + makeEntry({ path: '/vault/proj.md', title: 'My Project', isA: 'Project', modifiedAt: 1700000009 }), + ] + const { result } = renderHook(() => useNoteSearch(withTypes, '')) + const pasta = result.current.results.find(r => r.title === 'Pasta') + expect(pasta?.noteType).toBe('Recipe') + expect(pasta?.typeColor).toBe('var(--accent-orange)') + expect(pasta?.TypeIcon).toBeDefined() + // Built-in type still works + const project = result.current.results.find(r => r.title === 'My Project') + expect(project?.typeColor).toBe('var(--accent-red)') + expect(project?.TypeIcon).toBeDefined() + }) }) diff --git a/src/hooks/useNoteSearch.ts b/src/hooks/useNoteSearch.ts index e31b0175..60b4350c 100644 --- a/src/hooks/useNoteSearch.ts +++ b/src/hooks/useNoteSearch.ts @@ -1,7 +1,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react' import type { VaultEntry } from '../types' import { fuzzyMatch } from '../utils/fuzzyMatch' -import { getTypeColor } from '../utils/typeColors' +import { getTypeColor, buildTypeEntryMap } from '../utils/typeColors' +import { getTypeIcon } from '../components/NoteItem' import type { NoteSearchResultItem } from '../components/NoteSearchList' const DEFAULT_MAX_RESULTS = 20 @@ -10,33 +11,37 @@ export interface NoteSearchResult extends NoteSearchResultItem { entry: VaultEntry } -function toResult(e: VaultEntry): NoteSearchResult { +function toResult(e: VaultEntry, typeEntryMap: Record): NoteSearchResult { const noteType = e.isA && e.isA !== 'Note' ? e.isA : undefined + const te = typeEntryMap[e.isA ?? ''] return { entry: e, title: e.title, noteType, - typeColor: noteType ? getTypeColor(e.isA) : undefined, + typeColor: noteType ? getTypeColor(e.isA, te?.color) : undefined, + TypeIcon: noteType ? getTypeIcon(e.isA, te?.icon) : undefined, } } export function useNoteSearch(entries: VaultEntry[], query: string, maxResults = DEFAULT_MAX_RESULTS) { const [selectedIndex, setSelectedIndex] = useState(0) + const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const results: NoteSearchResult[] = useMemo(() => { + const mapResult = (e: VaultEntry) => toResult(e, typeEntryMap) if (!query.trim()) { return [...entries] .sort((a, b) => (b.modifiedAt ?? 0) - (a.modifiedAt ?? 0)) .slice(0, maxResults) - .map(toResult) + .map(mapResult) } return entries .map((e) => ({ entry: e, ...fuzzyMatch(query, e.title) })) .filter((r) => r.match) .sort((a, b) => b.score - a.score) .slice(0, maxResults) - .map((r) => toResult(r.entry)) - }, [entries, query, maxResults]) + .map((r) => mapResult(r.entry)) + }, [entries, query, maxResults, typeEntryMap]) useEffect(() => { setSelectedIndex(0) // eslint-disable-line react-hooks/set-state-in-effect -- reset on query change diff --git a/src/utils/typeColors.test.ts b/src/utils/typeColors.test.ts index c4dd6a8d..4a7c7908 100644 --- a/src/utils/typeColors.test.ts +++ b/src/utils/typeColors.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' -import { getTypeColor, getTypeLightColor } from './typeColors' +import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from './typeColors' +import type { VaultEntry } from '../types' describe('getTypeColor', () => { it('returns hardcoded color for known types', () => { @@ -47,3 +48,31 @@ describe('getTypeLightColor', () => { expect(getTypeLightColor('Recipe', 'purple')).toBe('var(--accent-purple-light)') }) }) + +const baseEntry: VaultEntry = { + path: '', filename: '', title: '', isA: null, aliases: [], belongsTo: [], relatedTo: [], + status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, + modifiedAt: null, createdAt: null, fileSize: 0, snippet: '', relationships: {}, + icon: null, color: null, order: null, outgoingLinks: [], +} + +describe('buildTypeEntryMap', () => { + it('indexes Type entries by title', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' }, + { ...baseEntry, title: 'My Note', isA: 'Note' }, + { ...baseEntry, title: 'Evergreen', isA: 'Type', color: 'green', icon: 'leaf' }, + ] + const map = buildTypeEntryMap(entries) + expect(Object.keys(map)).toEqual(['Recipe', 'Evergreen']) + expect(map['Recipe'].color).toBe('orange') + expect(map['Evergreen'].icon).toBe('leaf') + }) + + it('returns empty map when no Type entries exist', () => { + const entries: VaultEntry[] = [ + { ...baseEntry, title: 'A Note', isA: 'Note' }, + ] + expect(buildTypeEntryMap(entries)).toEqual({}) + }) +}) diff --git a/src/utils/typeColors.ts b/src/utils/typeColors.ts index dbc5f0e9..fab58e3c 100644 --- a/src/utils/typeColors.ts +++ b/src/utils/typeColors.ts @@ -3,6 +3,15 @@ * Single source of truth for type→color mapping used across Sidebar, NoteList, and Inspector. */ +import type { VaultEntry } from '../types' + +/** Builds a map from type name → Type document entry (for custom color/icon lookup) */ +export function buildTypeEntryMap(entries: VaultEntry[]): Record { + const map: Record = {} + for (const e of entries) { if (e.isA === 'Type') map[e.title] = e } + return map +} + const TYPE_COLOR_MAP: Record = { Project: 'var(--accent-red)', Experiment: 'var(--accent-red)',