fix: clear codacy high severity batches
This commit is contained in:
@@ -316,8 +316,8 @@ function OpenCommandPalette({
|
||||
|
||||
useEffect(() => {
|
||||
if (aiMode || !listRef.current) return
|
||||
const selectedElement = listRef.current.querySelector('[data-selected="true"]') as HTMLElement | null
|
||||
selectedElement?.scrollIntoView({ block: 'nearest' })
|
||||
const selectedHTMLElement = listRef.current.querySelector('[data-selected="true"]') as HTMLElement | null
|
||||
selectedHTMLElement?.scrollIntoView({ block: 'nearest' })
|
||||
}, [aiMode, selectedIndex])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect, type ComponentType, type SVGAttributes } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
||||
import { scrollSelectedHTMLChildIntoView } from '../utils/domScroll'
|
||||
import { getTypeIcon } from './NoteItem'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
import './WikilinkSuggestionMenu.css'
|
||||
@@ -8,6 +9,8 @@ import './WikilinkSuggestionMenu.css'
|
||||
const MIN_QUERY_LENGTH = 2
|
||||
const MAX_RESULTS = 10
|
||||
|
||||
type AutocompleteKeyAction = 'next' | 'previous' | 'select' | 'close'
|
||||
|
||||
interface NoteAutocompleteProps {
|
||||
entries: VaultEntry[]
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
@@ -29,26 +32,106 @@ interface MatchedEntry {
|
||||
TypeIcon?: ComponentType<SVGAttributes<SVGSVGElement>>
|
||||
}
|
||||
|
||||
function entryMatchesQuery(entry: VaultEntry, lowerQuery: string): boolean {
|
||||
return entry.title.toLowerCase().includes(lowerQuery)
|
||||
|| entry.aliases.some(alias => alias.toLowerCase().includes(lowerQuery))
|
||||
}
|
||||
|
||||
function buildMatchedEntry(entry: VaultEntry, typeEntryMap: Record<string, VaultEntry>): MatchedEntry {
|
||||
const isA = entry.isA
|
||||
const typeEntry = typeEntryMap[isA ?? '']
|
||||
const noteType = isA || undefined
|
||||
return {
|
||||
title: entry.title,
|
||||
noteIcon: entry.icon,
|
||||
noteType,
|
||||
typeColor: noteType ? getTypeColor(isA, typeEntry?.color) : undefined,
|
||||
typeLightColor: noteType ? getTypeLightColor(isA, typeEntry?.color) : undefined,
|
||||
TypeIcon: noteType ? getTypeIcon(isA, typeEntry?.icon) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function matchEntries(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>, query: string): MatchedEntry[] {
|
||||
if (query.length < MIN_QUERY_LENGTH) return []
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const matches = entries.filter(e =>
|
||||
e.title.toLowerCase().includes(lowerQuery) ||
|
||||
e.aliases.some(a => a.toLowerCase().includes(lowerQuery)),
|
||||
return entries
|
||||
.filter(entry => entryMatchesQuery(entry, lowerQuery))
|
||||
.slice(0, MAX_RESULTS)
|
||||
.map(entry => buildMatchedEntry(entry, typeEntryMap))
|
||||
}
|
||||
|
||||
function resolveOpenAutocompleteKeyAction(key: string): AutocompleteKeyAction | null {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
return 'next'
|
||||
case 'ArrowUp':
|
||||
return 'previous'
|
||||
case 'Enter':
|
||||
return 'select'
|
||||
case 'Escape':
|
||||
return 'close'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function nextAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
|
||||
return (currentIndex + 1) % matchCount
|
||||
}
|
||||
|
||||
function previousAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
|
||||
return currentIndex <= 0 ? matchCount - 1 : currentIndex - 1
|
||||
}
|
||||
|
||||
function handleClosedAutocompleteKey(
|
||||
key: string,
|
||||
value: string,
|
||||
onSelect: (noteTitle: string) => void,
|
||||
onEscape: (() => void) | undefined,
|
||||
): void {
|
||||
if (key === 'Enter') {
|
||||
onSelect(value)
|
||||
return
|
||||
}
|
||||
if (key === 'Escape') onEscape?.()
|
||||
}
|
||||
|
||||
function preventAutocompleteMouseDown(event: React.MouseEvent): void {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
interface NoteAutocompleteMenuItemProps {
|
||||
item: MatchedEntry
|
||||
selected: boolean
|
||||
onSelect: (title: string) => void
|
||||
onHover: () => void
|
||||
}
|
||||
|
||||
function NoteAutocompleteMenuItem({
|
||||
item,
|
||||
selected,
|
||||
onSelect,
|
||||
onHover,
|
||||
}: NoteAutocompleteMenuItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={`wikilink-menu__item${selected ? ' wikilink-menu__item--selected' : ''}`}
|
||||
onMouseDown={preventAutocompleteMouseDown}
|
||||
onClick={() => onSelect(item.title)}
|
||||
onMouseEnter={onHover}
|
||||
>
|
||||
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} />
|
||||
{item.title}
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
|
||||
{item.noteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return matches.slice(0, MAX_RESULTS).map(e => {
|
||||
const isA = e.isA
|
||||
const te = typeEntryMap[isA ?? '']
|
||||
const noteType = isA || undefined
|
||||
return {
|
||||
title: e.title,
|
||||
noteIcon: e.icon,
|
||||
noteType,
|
||||
typeColor: noteType ? getTypeColor(isA, te?.color) : undefined,
|
||||
typeLightColor: noteType ? getTypeLightColor(isA, te?.color) : undefined,
|
||||
TypeIcon: noteType ? getTypeIcon(isA, te?.icon) : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSelect, onEscape, placeholder, autoFocus, testId }: NoteAutocompleteProps) {
|
||||
@@ -65,8 +148,7 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0 || !menuRef.current) return
|
||||
const el = menuRef.current.children[selectedIndex] as HTMLElement | undefined
|
||||
el?.scrollIntoView?.({ block: 'nearest' })
|
||||
scrollSelectedHTMLChildIntoView(menuRef.current, selectedIndex)
|
||||
}, [selectedIndex])
|
||||
|
||||
// Close on outside click
|
||||
@@ -95,28 +177,31 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (!open || matches.length === 0) {
|
||||
if (e.key === 'Enter') { onSelect(value); return }
|
||||
if (e.key === 'Escape') { onEscape?.(); return }
|
||||
handleClosedAutocompleteKey(e.key, value, onSelect, onEscape)
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => (i + 1) % matches.length)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => (i <= 0 ? matches.length - 1 : i - 1))
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < matches.length) {
|
||||
handleSelect(matches[selectedIndex].title)
|
||||
} else {
|
||||
onSelect(value)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
|
||||
const action = resolveOpenAutocompleteKeyAction(e.key)
|
||||
if (!action) return
|
||||
|
||||
e.preventDefault()
|
||||
if (action === 'next') {
|
||||
setSelectedIndex(i => nextAutocompleteSelectionIndex(i, matches.length))
|
||||
return
|
||||
}
|
||||
if (action === 'previous') {
|
||||
setSelectedIndex(i => previousAutocompleteSelectionIndex(i, matches.length))
|
||||
return
|
||||
}
|
||||
if (action === 'close') {
|
||||
setOpen(false)
|
||||
onEscape?.()
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[selectedIndex]
|
||||
if (match) handleSelect(match.title)
|
||||
else onSelect(value)
|
||||
}, [open, matches, selectedIndex, value, handleSelect, onSelect, onEscape])
|
||||
|
||||
return (
|
||||
@@ -136,24 +221,13 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
{open && matches.length > 0 && (
|
||||
<div className="wikilink-menu" ref={menuRef} style={{ position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 2, minWidth: 'auto' }}>
|
||||
{matches.map((item, index) => (
|
||||
<div
|
||||
<NoteAutocompleteMenuItem
|
||||
key={item.title}
|
||||
className={`wikilink-menu__item${index === selectedIndex ? ' wikilink-menu__item--selected' : ''}`}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
onClick={() => handleSelect(item.title)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
>
|
||||
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} />
|
||||
{item.title}
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
|
||||
{item.noteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
item={item}
|
||||
selected={index === selectedIndex}
|
||||
onSelect={handleSelect}
|
||||
onHover={() => setSelectedIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRef, useEffect, type ComponentType, type SVGAttributes } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { scrollSelectedHTMLChildIntoView } from '../utils/domScroll'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
|
||||
export interface NoteSearchResultItem {
|
||||
@@ -22,6 +23,55 @@ interface NoteSearchListProps<T extends NoteSearchResultItem> {
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface NoteSearchListItemProps<T extends NoteSearchResultItem> {
|
||||
item: T
|
||||
index: number
|
||||
selected: boolean
|
||||
onItemClick: (item: T, index: number) => void
|
||||
onItemHover?: (index: number) => void
|
||||
}
|
||||
|
||||
function NoteSearchListItem<T extends NoteSearchResultItem>({
|
||||
item,
|
||||
index,
|
||||
selected,
|
||||
onItemClick,
|
||||
onItemHover,
|
||||
}: NoteSearchListItemProps<T>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center justify-between gap-2 px-3 py-1.5 transition-colors',
|
||||
selected ? 'bg-accent' : 'hover:bg-secondary',
|
||||
)}
|
||||
onClick={() => onItemClick(item, index)}
|
||||
onMouseEnter={() => onItemHover?.(index)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 truncate text-sm text-foreground">
|
||||
{item.TypeIcon && (
|
||||
<item.TypeIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className="shrink-0"
|
||||
style={item.typeColor ? { color: item.typeColor } : undefined}
|
||||
/>
|
||||
)}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} testId="note-search-item-icon" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 text-[11px]"
|
||||
style={item.typeColor ? { color: item.typeColor, backgroundColor: item.typeLightColor } : undefined}
|
||||
>
|
||||
{item.noteType}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteSearchList<T extends NoteSearchResultItem>({
|
||||
items,
|
||||
selectedIndex,
|
||||
@@ -34,9 +84,7 @@ export function NoteSearchList<T extends NoteSearchResultItem>({
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!listRef.current) return
|
||||
const el = listRef.current.children[selectedIndex] as HTMLElement | undefined
|
||||
el?.scrollIntoView({ block: 'nearest' })
|
||||
scrollSelectedHTMLChildIntoView(listRef.current, selectedIndex)
|
||||
}, [selectedIndex])
|
||||
|
||||
if (items.length === 0) {
|
||||
@@ -52,37 +100,14 @@ export function NoteSearchList<T extends NoteSearchResultItem>({
|
||||
return (
|
||||
<div ref={listRef} className={cn('py-1', className)}>
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
<NoteSearchListItem
|
||||
key={getItemKey(item, i)}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center justify-between gap-2 px-3 py-1.5 transition-colors',
|
||||
i === selectedIndex ? 'bg-accent' : 'hover:bg-secondary',
|
||||
)}
|
||||
onClick={() => onItemClick(item, i)}
|
||||
onMouseEnter={() => onItemHover?.(i)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 truncate text-sm text-foreground">
|
||||
{item.TypeIcon && (
|
||||
<item.TypeIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className="shrink-0"
|
||||
style={item.typeColor ? { color: item.typeColor } : undefined}
|
||||
/>
|
||||
)}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} testId="note-search-item-icon" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 text-[11px]"
|
||||
style={item.typeColor ? { color: item.typeColor, backgroundColor: item.typeLightColor } : undefined}
|
||||
>
|
||||
{item.noteType}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
item={item}
|
||||
index={i}
|
||||
selected={i === selectedIndex}
|
||||
onItemClick={onItemClick}
|
||||
onItemHover={onItemHover}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useLayoutEffect, useRef, type HTMLAttributes } from 'react'
|
||||
import { RUNTIME_STYLE_NONCE } from '@/lib/runtimeStyleNonce'
|
||||
|
||||
interface SafeHtmlSpanProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
html: string
|
||||
markup: string
|
||||
}
|
||||
|
||||
interface SafeSvgDivProps extends HTMLAttributes<HTMLDivElement> {
|
||||
@@ -17,8 +17,8 @@ const MERMAID_SVG_SANITIZE_CONFIG = {
|
||||
HTML_INTEGRATION_POINTS: { foreignobject: true },
|
||||
}
|
||||
|
||||
function importSanitizedHtmlNodes(html: string): Node[] {
|
||||
const sanitized = DOMPurify.sanitize(html, {
|
||||
function importSanitizedMarkupNodes(markup: string): Node[] {
|
||||
const sanitized = DOMPurify.sanitize(markup, {
|
||||
USE_PROFILES: { html: true, mathMl: true },
|
||||
})
|
||||
const parsed = new DOMParser().parseFromString(sanitized, 'text/html')
|
||||
@@ -38,12 +38,12 @@ function importSanitizedSvgNode(svg: string): Node | null {
|
||||
return svgNode
|
||||
}
|
||||
|
||||
export function SafeHtmlSpan({ html, ...props }: SafeHtmlSpanProps) {
|
||||
export function SafeHtmlSpan({ markup, ...props }: SafeHtmlSpanProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
ref.current?.replaceChildren(...importSanitizedHtmlNodes(html))
|
||||
}, [html])
|
||||
ref.current?.replaceChildren(...importSanitizedMarkupNodes(markup))
|
||||
}, [markup])
|
||||
|
||||
return <span {...props} ref={ref} />
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRef, useEffect, useCallback, useLayoutEffect } from 'react'
|
||||
import { createElement, useRef, useEffect, useCallback, useLayoutEffect } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SearchResult, VaultEntry } from '../types'
|
||||
import { useUnifiedSearch } from '../hooks/useUnifiedSearch'
|
||||
import { getTypeColor, buildTypeEntryMap } from '../utils/typeColors'
|
||||
import { formatSearchSubtitle } from '../utils/noteListHelpers'
|
||||
import { scrollSelectedHTMLChildIntoView } from '../utils/domScroll'
|
||||
import { getTypeIcon } from './NoteItem'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
|
||||
@@ -16,6 +17,32 @@ interface SearchPanelProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type SearchKeyboardAction = 'close' | 'next' | 'previous' | 'select'
|
||||
|
||||
function resolveSearchKeyboardAction(key: string): SearchKeyboardAction | null {
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
return 'close'
|
||||
case 'ArrowDown':
|
||||
return 'next'
|
||||
case 'ArrowUp':
|
||||
return 'previous'
|
||||
case 'Enter':
|
||||
return 'select'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function nextSearchSelectionIndex(
|
||||
action: Extract<SearchKeyboardAction, 'next' | 'previous'>,
|
||||
currentIndex: number,
|
||||
resultCount: number,
|
||||
): number {
|
||||
if (action === 'next') return Math.min(currentIndex + 1, resultCount - 1)
|
||||
return Math.max(currentIndex - 1, 0)
|
||||
}
|
||||
|
||||
export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }: SearchPanelProps) {
|
||||
const {
|
||||
query, setQuery, results, selectedIndex, setSelectedIndex, loading, elapsedMs,
|
||||
@@ -27,9 +54,7 @@ export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }:
|
||||
const selectedIndexRef = useRef(selectedIndex)
|
||||
|
||||
useEffect(() => {
|
||||
if (!listRef.current) return
|
||||
const selected = listRef.current.children[selectedIndex] as HTMLElement | undefined
|
||||
selected?.scrollIntoView({ block: 'nearest' })
|
||||
scrollSelectedHTMLChildIntoView(listRef.current, selectedIndex)
|
||||
}, [selectedIndex])
|
||||
|
||||
const handleSelect = useCallback((result: SearchResult) => {
|
||||
@@ -50,20 +75,22 @@ export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }:
|
||||
}, [open])
|
||||
|
||||
const handleKeyDown = useCallback((e: { key: string; preventDefault: () => void }) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
const action = resolveSearchKeyboardAction(e.key)
|
||||
if (!action) return
|
||||
|
||||
e.preventDefault()
|
||||
if (action === 'close') {
|
||||
onClose()
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => Math.min(i + 1, resultsRef.current.length - 1))
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => Math.max(i - 1, 0))
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'select') {
|
||||
const result = resultsRef.current[selectedIndexRef.current]
|
||||
if (result) handleSelect(result)
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedIndex(i => nextSearchSelectionIndex(action, i, resultsRef.current.length))
|
||||
}, [handleSelect, onClose, setSelectedIndex])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -170,6 +197,59 @@ interface SearchContentProps {
|
||||
onHover: (index: number) => void
|
||||
}
|
||||
|
||||
interface SearchResultRowProps {
|
||||
result: SearchResult
|
||||
entry: VaultEntry | undefined
|
||||
selected: boolean
|
||||
index: number
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
onSelect: (result: SearchResult) => void
|
||||
onHover: (index: number) => void
|
||||
}
|
||||
|
||||
function SearchResultRow({
|
||||
result, entry, selected, index, typeEntryMap, onSelect, onHover,
|
||||
}: SearchResultRowProps) {
|
||||
const isA = entry?.isA ?? result.noteType
|
||||
const noteType = isA || null
|
||||
const te = typeEntryMap[isA ?? '']
|
||||
const typeColor = noteType ? getTypeColor(isA, te?.color) : undefined
|
||||
const TypeIcon = getTypeIcon(isA ?? null, te?.icon)
|
||||
const subtitle = entry ? formatSearchSubtitle(entry) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"cursor-pointer px-4 py-2.5 transition-colors",
|
||||
selected ? "bg-accent" : "hover:bg-secondary",
|
||||
)}
|
||||
onClick={() => onSelect(result)}
|
||||
onMouseEnter={() => onHover(index)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{createElement(TypeIcon, {
|
||||
width: 14,
|
||||
height: 14,
|
||||
className: 'shrink-0',
|
||||
style: { color: typeColor ?? 'var(--muted-foreground)' },
|
||||
})}
|
||||
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">
|
||||
<NoteTitleIcon icon={entry?.icon} size={14} className="mr-1" />
|
||||
{entry?.title ?? result.title}
|
||||
</span>
|
||||
{noteType && (
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground/70">{noteType}</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="mt-0.5 pl-[22px] text-[11px] text-muted-foreground">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchContent({
|
||||
query, results, selectedIndex, loading, elapsedMs, entryLookup, typeEntryMap, listRef, onSelect, onHover,
|
||||
}: SearchContentProps) {
|
||||
@@ -204,42 +284,18 @@ function SearchContent({
|
||||
</span>
|
||||
</div>
|
||||
<div ref={listRef}>
|
||||
{results.map((result, i) => {
|
||||
const entry = entryLookup.get(result.path)
|
||||
const isA = entry?.isA ?? result.noteType
|
||||
const noteType = isA || null
|
||||
const te = typeEntryMap[isA ?? '']
|
||||
const typeColor = noteType ? getTypeColor(isA, te?.color) : undefined
|
||||
const TypeIcon = getTypeIcon(isA ?? null, te?.icon)
|
||||
const subtitle = entry ? formatSearchSubtitle(entry) : null
|
||||
return (
|
||||
<div
|
||||
key={result.path}
|
||||
className={cn(
|
||||
"cursor-pointer px-4 py-2.5 transition-colors",
|
||||
i === selectedIndex ? "bg-accent" : "hover:bg-secondary",
|
||||
)}
|
||||
onClick={() => onSelect(result)}
|
||||
onMouseEnter={() => onHover(i)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<TypeIcon width={14} height={14} className="shrink-0" style={{ color: typeColor ?? 'var(--muted-foreground)' }} />
|
||||
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">
|
||||
<NoteTitleIcon icon={entry?.icon} size={14} className="mr-1" />
|
||||
{entry?.title ?? result.title}
|
||||
</span>
|
||||
{noteType && (
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground/70">{noteType}</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="mt-0.5 pl-[22px] text-[11px] text-muted-foreground">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{results.map((result, i) => (
|
||||
<SearchResultRow
|
||||
key={result.path}
|
||||
result={result}
|
||||
entry={entryLookup.get(result.path)}
|
||||
selected={i === selectedIndex}
|
||||
index={i}
|
||||
typeEntryMap={typeEntryMap}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -35,9 +35,12 @@ function buildSortItems(locale: AppLocale, customProperties?: string[]): SortIte
|
||||
}
|
||||
|
||||
function resolveFocusedIndex(groupLabel: string, current: SortOption, sortItems: SortItem[]) {
|
||||
const activeElement = document.activeElement as HTMLElement | null
|
||||
const activeIndex = Number(activeElement?.dataset.sortItemIndex ?? -1)
|
||||
if (activeElement?.dataset.sortGroupLabel === groupLabel && activeIndex >= 0) return activeIndex
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
const activeIndexText = activeElement.dataset.sortItemIndex
|
||||
const activeIndex = activeIndexText === undefined ? -1 : Number(activeIndexText)
|
||||
if (activeElement.dataset.sortGroupLabel === groupLabel && activeIndex >= 0) return activeIndex
|
||||
}
|
||||
|
||||
const currentIndex = sortItems.findIndex((item) => item.value === current)
|
||||
return currentIndex >= 0 ? currentIndex : 0
|
||||
|
||||
@@ -79,7 +79,7 @@ function MathRender({ latex, displayMode }: { latex: string; displayMode: boolea
|
||||
aria-label={`Math: ${latex}`}
|
||||
className={displayMode ? 'math math--block' : 'math math--inline'}
|
||||
data-latex={latex}
|
||||
html={renderMathToHtml({ latex, displayMode })}
|
||||
markup={renderMathToHtml({ latex, displayMode })}
|
||||
role="img"
|
||||
title={source}
|
||||
/>
|
||||
|
||||
@@ -401,8 +401,12 @@ export function useNoteListSort({
|
||||
// --- useMultiSelectKeyboard ---
|
||||
|
||||
function isInputFocused(): boolean {
|
||||
const el = document.activeElement
|
||||
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || !!(el as HTMLElement)?.isContentEditable
|
||||
const activeElement = document.activeElement
|
||||
if (!(activeElement instanceof HTMLElement)) return false
|
||||
|
||||
return activeElement.tagName === 'INPUT'
|
||||
|| activeElement.tagName === 'TEXTAREA'
|
||||
|| activeElement.isContentEditable
|
||||
}
|
||||
|
||||
function handleEscapeKey(e: KeyboardEvent, multiSelect: MultiSelectState) {
|
||||
|
||||
@@ -10,8 +10,12 @@ interface UseMultiSelectKeyboardOptions {
|
||||
}
|
||||
|
||||
function isInputFocused(): boolean {
|
||||
const el = document.activeElement
|
||||
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || !!(el as HTMLElement)?.isContentEditable
|
||||
const activeElement = document.activeElement
|
||||
if (!(activeElement instanceof HTMLElement)) return false
|
||||
|
||||
return activeElement.tagName === 'INPUT'
|
||||
|| activeElement.tagName === 'TEXTAREA'
|
||||
|| activeElement.isContentEditable
|
||||
}
|
||||
|
||||
function usesCommandModifier(event: KeyboardEvent): boolean {
|
||||
|
||||
Reference in New Issue
Block a user