/
githubmirror
/
marktext
Обзор
Документация
Войти
/
githubmirror
/
marktext
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/website/src/components/docs/CommandPalette.tsx
234 строки
6 KB
Ran Luo
refactor(website): simplify docs subtree (palette / sidebar / markdown / nav) (#4349)
02 июн 2026, 19:36
Не верифицирован
02 июн 2026, 19:36
8544b10
Код
Авторство
О чём код?
'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { loadIndex, search, type IndexedPage, type SearchHit } from '@/lib/docs-search' import { SearchIcon } from '@/components/Icons' type Props = { open: boolean onClose: () => void } type GroupedHit = { hit: SearchHit; index: number } type HitGroup = { key: string; label: string; hits: GroupedHit[] } export default function CommandPalette({ open, onClose }: Props) { const router = useRouter() const inputRef = useRef<HTMLInputElement | null>(null) const listRef = useRef<HTMLDivElement | null>(null) const [index, setIndex] = useState<IndexedPage[] | null>(null) const [query, setQuery] = useState('') const [selected, setSelected] = useState(0) const [error, setError] = useState<string | null>(null) useEffect(() => { if (!open) return loadIndex().then(setIndex).catch((err) => setError((err as Error).message)) setQuery('') setSelected(0) queueMicrotask(() => inputRef.current?.focus()) }, [open]) const hits: SearchHit[] = useMemo(() => { if (!index) return [] if (!query.trim()) { return index.map((page) => ({ page, score: 0, snippet: page.hint ?? page.body.slice(0, 120) })) } return search(query, index) }, [index, query]) const groups = useMemo(() => groupHits(hits), [hits]) useEffect(() => { setSelected(0) }, [query]) const navigateTo = useCallback( (hit: SearchHit) => { const href = hit.headingId ? hit.page.href + '#' + hit.headingId : hit.page.href router.push(href) onClose() }, [router, onClose] ) useEffect(() => { if (!open) return function onKey(ev: KeyboardEvent) { if (ev.isComposing) return if (ev.key === 'Escape') { ev.preventDefault() onClose() return } if (ev.key === 'ArrowDown') { ev.preventDefault() setSelected((s) => Math.min(s + 1, hits.length - 1)) return } if (ev.key === 'ArrowUp') { ev.preventDefault() setSelected((s) => Math.max(s - 1, 0)) return } if (ev.key === 'Enter') { ev.preventDefault() setSelected((s) => { const hit = hits[s] if (hit) navigateTo(hit) return s }) } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [open, onClose, navigateTo, hits]) useEffect(() => { if (!open) return const el = listRef.current?.querySelector('.kbar-item.sel') as HTMLElement | null el?.scrollIntoView({ block: 'nearest' }) }, [selected, open]) return ( <div className={'kbar-scrim' + (open ? ' open' : '')} onClick={onClose} aria-hidden={!open}> <div className="kbar" role="dialog" aria-modal="true" aria-label="Search documentation" onClick={(e) => e.stopPropagation()} > <div className="kbar-input"> <SearchIcon aria-hidden /> <input ref={inputRef} type="text" placeholder="Search documentation…" value={query} onChange={(e) => setQuery(e.target.value)} /> <span className="esc">esc</span> </div> <div className="kbar-results" ref={listRef}> {error && <div className="kbar-empty">Could not load search index: {error}</div>} {!error && hits.length === 0 && index && ( <div className="kbar-empty">No results for “{query}”</div> )} {!error && !index && <div className="kbar-empty">Loading…</div>} {groups.map((group) => ( <div key={group.key}> <div className="kbar-sec">{group.label}</div> {group.hits.map(({ hit, index: idx }) => ( <ResultItem key={hit.page.slug} hit={hit} selected={idx === selected} onHover={() => setSelected(idx)} onClick={() => navigateTo(hit)} /> ))} </div> ))} </div> <div className="kbar-foot"> <span className="kk"> <kbd>↑</kbd> <kbd>↓</kbd> navigate </span> <span className="kk"> <kbd>↵</kbd> open </span> <span className="kk"> <kbd>esc</kbd> close </span> </div> </div> </div> ) } function groupHits(hits: SearchHit[]): HitGroup[] { const order: Record<string, number> = { user: 0, dev: 1 } const map = new Map<string, HitGroup>() hits.forEach((hit, index) => { const key = `${hit.page.tab}::${hit.page.group}` let bucket = map.get(key) if (!bucket) { bucket = { key, label: `${hit.page.tabLabel} · ${hit.page.group}`, hits: [] } map.set(key, bucket) } bucket.hits.push({ hit, index }) }) return Array.from(map.values()).sort((a, b) => { const ta = a.key.split('::')[0] const tb = b.key.split('::')[0] return (order[ta] ?? 9) - (order[tb] ?? 9) }) } function ResultItem({ hit, selected, onHover, onClick }: { hit: SearchHit selected: boolean onHover: () => void onClick: () => void }) { return ( <div className={'kbar-item' + (selected ? ' sel' : '')} onMouseEnter={onHover} onClick={onClick} role="option" aria-selected={selected} > <span className="ki"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /> <path d="M14 2v6h6" /> </svg> </span> <span className="kt"> <b>{hit.page.title}</b> {hit.snippetHtml ? ( <span dangerouslySetInnerHTML={{ __html: hit.snippetHtml }} /> ) : ( <span>{hit.snippet}</span> )} </span> <span className="kgo"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <path d="M5 12h14M12 5l7 7-7 7" /> </svg> </span> </div> ) }