feat(ui): the search palette, entry points and a trail that survives the URL (CG-45)
Search: `/` or ⌘K focuses the box; results arrive grouped by kind with their
glyph, signature and file:line, ↑/↓/Enter walk them, Esc dismisses. A group
appears where its best result did, so flattening the groups reproduces the
ranking the keyboard walks — the panel's flat item list IS that concatenation.
A flow question ("how does X reach Y", "X -> Y") is recognised and searches
both endpoints with a note, rather than offering a row that would land on the
phase-2 Flow view.
Entry points answer "where do I start" on the empty screen and in the resting
palette, all derived from the graph: routes, files that run something at module
level (the engine records a top-level statement as an edge out of the file node,
which is what makes src/bin/codegraph.ts the root of the CLI flow — ranked by
calls x the files they reach, so a registration table calling into itself does
not outrank the CLI), and the most depended-on symbols. Tests are excluded from
both derived lists.
Trail: hops record the direction they were walked (→ into a call, ← up to a
caller), clicking one truncates back to it, Clear keeps the place instead of
throwing it away, and the whole walk travels in the URL. A shared or reloaded
trail arrives as ids, so hops learn their names back through a new batch
endpoint and a session name cache — without it, walking back across a
truncation redrew earlier hops as raw hashes. "Read as flow" stays hidden until
there is a Flow view to send it to.
New endpoints: /api/entrypoints and /api/nodes. New engine reads:
getTopCallingFiles, getFileDependentCounts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e9596af1cf
commit
87afc50e76
@@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The rows of a palette — shared by the panel under the search box and the
|
||||
* empty screen's "where to start" list, because they are the same rows and a
|
||||
* second copy would drift.
|
||||
*
|
||||
* Selection is passed in rather than owned here: in the panel it belongs to
|
||||
* the keyboard, on the empty screen there is none.
|
||||
*/
|
||||
import KindGlyph from './KindGlyph.svelte';
|
||||
import type { Palette, PaletteItem } from '../lib/search-model';
|
||||
|
||||
interface Props {
|
||||
palette: Palette;
|
||||
/** Index into `palette.items`, or -1 for no keyboard selection. */
|
||||
selected?: number;
|
||||
/**
|
||||
* Set to 'option' when these rows sit inside a listbox (the search panel).
|
||||
* Left off on the empty screen, where they are just links: `role="option"`
|
||||
* outside a listbox is a lie a screen reader acts on.
|
||||
*/
|
||||
rowRole?: 'option' | undefined;
|
||||
/** Prefix for each row's DOM id, so a combobox can point at the selected one. */
|
||||
idPrefix?: string;
|
||||
onpick: (item: PaletteItem) => void;
|
||||
onhover?: (index: number) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
palette,
|
||||
selected = -1,
|
||||
rowRole = undefined,
|
||||
idPrefix = 'palette-row',
|
||||
onpick,
|
||||
onhover,
|
||||
}: Props = $props();
|
||||
|
||||
/** Running index into the flat item list, so a row knows its keyboard position. */
|
||||
function flatIndex(sectionIndex: number, rowIndex: number): number {
|
||||
let base = 0;
|
||||
for (let i = 0; i < sectionIndex; i += 1) base += palette.sections[i]?.items.length ?? 0;
|
||||
return base + rowIndex;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each palette.sections as section, s (section.title)}
|
||||
<div class="head">
|
||||
<span class="head-title">{section.title}</span>
|
||||
{#if section.note}<span class="head-note">{section.note}</span>{/if}
|
||||
</div>
|
||||
{#each section.items as item, r (item.id)}
|
||||
{@const index = flatIndex(s, r)}
|
||||
<button
|
||||
type="button"
|
||||
class="row"
|
||||
class:sel={index === selected}
|
||||
data-palette-row={index}
|
||||
id={`${idPrefix}-${index}`}
|
||||
role={rowRole}
|
||||
aria-selected={rowRole ? index === selected : undefined}
|
||||
onmousedown={(event) => {
|
||||
// mousedown, not click: the input's blur would close the panel first.
|
||||
event.preventDefault();
|
||||
onpick(item);
|
||||
}}
|
||||
onmouseenter={() => onhover?.(index)}
|
||||
>
|
||||
{#if item.type === 'route'}
|
||||
<KindGlyph kind="route" />
|
||||
<span class="mid">
|
||||
<span class="nm">{item.url}</span>
|
||||
<span class="sig">{item.handler}</span>
|
||||
</span>
|
||||
{:else}
|
||||
<KindGlyph kind={item.node.kind} />
|
||||
<span class="mid">
|
||||
<span class="nm">{item.name}</span>
|
||||
{#if item.meta}<span class="sig">{item.meta}</span>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="loc">{item.location}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 6px 10px 4px;
|
||||
border-bottom: 1px solid var(--rule-faint);
|
||||
color: var(--ink-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.head-note {
|
||||
overflow: hidden;
|
||||
color: var(--ink-4);
|
||||
font-size: 11.5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
align-items: baseline;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--rule-faint);
|
||||
color: var(--ink);
|
||||
gap: 10px;
|
||||
grid-template-columns: 18px 1fr auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.row:hover,
|
||||
.row.sel {
|
||||
background: var(--press);
|
||||
}
|
||||
|
||||
.mid {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nm {
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.sig {
|
||||
margin-left: 6px;
|
||||
color: var(--ink-3);
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.loc {
|
||||
color: var(--ink-3);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user