From 7736d20b9ff3ef42e317a497d29e57fb7f5968cd Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 24 Feb 2026 23:42:09 +0100 Subject: [PATCH] feat: replace grouped autocomplete with flat list and type badge Switch wiki-link autocomplete from BlockNote's default grouped suggestion menu to a custom flat list sorted by relevance. Each item shows the note type (with its accent color) discretely on the right side. Reduce MAX_RESULTS from 20 to 10 for a cleaner, more focused list. Co-Authored-By: Claude Opus 4.6 --- src/components/Editor.tsx | 13 ++++- src/components/WikilinkSuggestionMenu.css | 58 ++++++++++++++++++++++ src/components/WikilinkSuggestionMenu.tsx | 59 +++++++++++++++++++++++ src/utils/wikilinkSuggestions.test.ts | 4 +- src/utils/wikilinkSuggestions.ts | 2 +- 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/components/WikilinkSuggestionMenu.css create mode 100644 src/components/WikilinkSuggestionMenu.tsx diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index ede88396..1c70f872 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -16,6 +16,8 @@ import { useEditorTheme } from '../hooks/useTheme' import { splitFrontmatter, preProcessWikilinks, injectWikilinks, restoreWikilinksInBlocks, countWords } from '../utils/wikilinks' import { preFilterWikilinks, deduplicateByPath, disambiguateTitles, MAX_RESULTS, MIN_QUERY_LENGTH } from '../utils/wikilinkSuggestions' import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors' +import { getTypeColor } from '../utils/typeColors' +import { WikilinkSuggestionMenu, type WikilinkSuggestionItem } from './WikilinkSuggestionMenu' import './Editor.css' import './EditorTheme.css' @@ -142,7 +144,7 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e [entries] ) - const getWikilinkItems = useCallback(async (query: string) => { + const getWikilinkItems = useCallback(async (query: string): Promise => { if (query.length < MIN_QUERY_LENGTH) return [] const candidates = preFilterWikilinks(baseItems, query) @@ -159,7 +161,12 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e }, })) const filtered = filterSuggestionItems(items, query).slice(0, MAX_RESULTS) - return disambiguateTitles(deduplicateByPath(filtered)) + const final = disambiguateTitles(deduplicateByPath(filtered)) + return final.map(({ group, ...rest }) => ({ + ...rest, + noteType: group !== 'Note' ? group : undefined, + typeColor: group !== 'Note' ? getTypeColor(group) : undefined, + })) }, [baseItems, editor]) return ( @@ -177,6 +184,8 @@ function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }: { e item.onItemClick()} /> diff --git a/src/components/WikilinkSuggestionMenu.css b/src/components/WikilinkSuggestionMenu.css new file mode 100644 index 00000000..6b4913c3 --- /dev/null +++ b/src/components/WikilinkSuggestionMenu.css @@ -0,0 +1,58 @@ +/* Wikilink autocomplete — flat suggestion menu */ +.wikilink-menu { + background: var(--bg-primary, #fff); + border: 1px solid var(--border, rgba(0, 0, 0, 0.1)); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + max-height: 320px; + overflow-y: auto; + padding: 4px; + min-width: 260px; + max-width: 400px; + z-index: 9999; +} + +.wikilink-menu__empty { + padding: 8px 12px; + font-size: 13px; + color: var(--text-muted, #999); +} + +.wikilink-menu__item { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + line-height: 1.4; +} + +.wikilink-menu__item:hover { + background: var(--muted, rgba(0, 0, 0, 0.04)); +} + +.wikilink-menu__item--selected { + background: var(--accent, rgba(21, 93, 255, 0.08)); +} + +.wikilink-menu__title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-primary, #1a1a1a); +} + +.wikilink-menu__type { + flex-shrink: 0; + font-size: 11px; + font-weight: 500; + opacity: 0.8; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/components/WikilinkSuggestionMenu.tsx b/src/components/WikilinkSuggestionMenu.tsx new file mode 100644 index 00000000..1c6a257f --- /dev/null +++ b/src/components/WikilinkSuggestionMenu.tsx @@ -0,0 +1,59 @@ +import { useRef, useEffect } from 'react' +import './WikilinkSuggestionMenu.css' + +export interface WikilinkSuggestionItem { + title: string + onItemClick: () => void + noteType?: string + typeColor?: string + aliases?: string[] + entryTitle?: string + path?: string +} + +interface WikilinkSuggestionMenuProps { + items: WikilinkSuggestionItem[] + loadingState: 'loading-initial' | 'loading' | 'loaded' + selectedIndex: number | undefined + onItemClick?: (item: WikilinkSuggestionItem) => void +} + +export function WikilinkSuggestionMenu({ items, selectedIndex, onItemClick }: WikilinkSuggestionMenuProps) { + const menuRef = useRef(null) + + useEffect(() => { + if (selectedIndex === undefined || !menuRef.current) return + const el = menuRef.current.children[selectedIndex] as HTMLElement | undefined + el?.scrollIntoView({ block: 'nearest' }) + }, [selectedIndex]) + + if (items.length === 0) { + return ( +
+
No results
+
+ ) + } + + return ( +
+ {items.map((item, index) => ( +
{ + item.onItemClick() + onItemClick?.(item) + }} + > + {item.title} + {item.noteType && ( + + {item.noteType} + + )} +
+ ))} +
+ ) +} diff --git a/src/utils/wikilinkSuggestions.test.ts b/src/utils/wikilinkSuggestions.test.ts index f96263e9..e8b1249d 100644 --- a/src/utils/wikilinkSuggestions.test.ts +++ b/src/utils/wikilinkSuggestions.test.ts @@ -77,8 +77,8 @@ describe('constants', () => { expect(MIN_QUERY_LENGTH).toBe(2) }) - it('MAX_RESULTS is 20', () => { - expect(MAX_RESULTS).toBe(20) + it('MAX_RESULTS is 10', () => { + expect(MAX_RESULTS).toBe(10) }) }) diff --git a/src/utils/wikilinkSuggestions.ts b/src/utils/wikilinkSuggestions.ts index 3d8feeaa..5774eee3 100644 --- a/src/utils/wikilinkSuggestions.ts +++ b/src/utils/wikilinkSuggestions.ts @@ -1,5 +1,5 @@ export const MIN_QUERY_LENGTH = 2 -export const MAX_RESULTS = 20 +export const MAX_RESULTS = 10 export interface WikilinkBaseItem { title: string