feat(ui): the viewer's screens as @colbymchenry/codegraph-ui, behind one adapter (CG-61)
`ui/src` now builds two ways from one tree: the static app `codegraph ui` serves, and — via `svelte-package` — a Svelte library the Pro app imports. A forked component would be a second answer to the same question about the same graph, so there is no fork. Everything a screen knows arrives through a `GraphAdapter`: eleven methods answering the wire shapes verbatim, with `createHttpAdapter()` (the loopback JSON API) as the default and a host's in-process engine reads as the point. `lib/api.ts` became a one-line-per-call facade over it, which is why no call site in the views changed. The payload types moved to `lib/wire.ts` — no imports, no runtime — so a host can depend on the vocabulary alone. Two more seams and one guard: - `lib/navigation.ts` holds the href builders behind a `NavigationDriver`, so a host addresses its own URL space. The app's half — the hash parser and the live route, which attach window listeners at module scope — stays in `router.svelte.ts` and is pruned out of the package: rendering a Symbol view must not install a hash router in somebody else's application. - `lib/theme.css` carries the design tokens and maps Svelte Flow's `--xy-*` variables onto them, so a host never sees library defaults. Dark now also answers to a bare `[data-theme]`, which is how `<CodegraphUi theme>` themes a container rather than the document. - `scripts/check-ui-package.mjs` prunes the app's shell, resolves the extensionless specifiers svelte-package leaves behind, and asserts that nothing but `lib/adapter.js` reaches the network. The search box, its keyboard and its panel are one component now (`SearchPalette`), because splitting them is what breaks a palette. `__tests__/ui-package.test.ts` mounts the three screens from the package entry against a mock adapter in jsdom; it runs as a second vitest project so the `browser` resolve condition it needs cannot reach the engine's suites. Versioned with the engine. Prepared, not published: `private: true` is the guard and `pack-npm.sh` only packs a tarball under CODEGRAPH_PACK_UI=1.
This commit is contained in:
@@ -1,18 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
router,
|
||||
mapHref,
|
||||
flowHref,
|
||||
entryHref,
|
||||
symbolHref,
|
||||
fileHref,
|
||||
navigate,
|
||||
} from '../lib/router.svelte';
|
||||
import { router, mapHref, flowHref, entryHref, symbolHref } from '../lib/router.svelte';
|
||||
import { trail } from '../lib/trail.svelte';
|
||||
import { palette } from '../lib/palette.svelte';
|
||||
import SearchPalette from './SearchPalette.svelte';
|
||||
import type { PaletteItem } from '../lib/search-model';
|
||||
import { openEntryTarget, walkTo } from '../lib/walk';
|
||||
import { live } from '../lib/live.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -24,7 +13,7 @@
|
||||
|
||||
let { project = null, stats = null }: Props = $props();
|
||||
|
||||
let input: HTMLInputElement | null = $state(null);
|
||||
let search: SearchPalette | null = $state(null);
|
||||
|
||||
let view = $derived(router.route.view);
|
||||
|
||||
@@ -37,99 +26,11 @@
|
||||
return current ? symbolHref(current.id) : '#/';
|
||||
});
|
||||
|
||||
/** What `/` and Cmd-K reach — the palette owns its own keyboard. */
|
||||
export function focusSearch(): void {
|
||||
input?.focus();
|
||||
input?.select();
|
||||
palette.show();
|
||||
search?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Following a result is a `start` hop, never `down` or `up`: nothing on
|
||||
* screen was stepped through to get there, and claiming a direction would
|
||||
* put a `→` in the trail that describes no call.
|
||||
*/
|
||||
export function pick(item: PaletteItem): void {
|
||||
// A flow is not a place in the graph, so it does not join the trail: it is
|
||||
// a question about two symbols, and the Flow view answers it.
|
||||
if (item.type === 'flow') {
|
||||
palette.reset();
|
||||
input?.blur();
|
||||
navigate(flowHref({ from: item.from, to: item.to }));
|
||||
return;
|
||||
}
|
||||
// An entry-point row already knows where it goes — a handler, a file, a
|
||||
// hub — and it is the one row type that can point at a FILE.
|
||||
if (item.type === 'entry') {
|
||||
if (!item.row.target) return;
|
||||
palette.reset();
|
||||
input?.blur();
|
||||
openEntryTarget(item.row.target);
|
||||
return;
|
||||
}
|
||||
const id = item.type === 'route' ? item.nodeId : item.id;
|
||||
// A route whose handler never resolved to a node has nowhere to go; the
|
||||
// row stays, because "this URL exists and we could not place it" is true.
|
||||
if (!id) return;
|
||||
palette.reset();
|
||||
input?.blur();
|
||||
// A file result opens the File view, not the file node's Symbol view: the
|
||||
// outline is there either way, and only the File view carries the import
|
||||
// rails. (CG-45 routed these at the Symbol view because #/file was a stub.)
|
||||
if (item.type === 'symbol' && item.node.kind === 'file') {
|
||||
navigate(fileHref(item.node.file));
|
||||
return;
|
||||
}
|
||||
walkTo(
|
||||
item.type === 'route'
|
||||
? { id, name: item.handler, kind: null }
|
||||
: { id, name: item.node.name, kind: item.node.kind },
|
||||
'start'
|
||||
);
|
||||
}
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
palette.hide();
|
||||
input?.blur();
|
||||
return;
|
||||
}
|
||||
if (!palette.open) {
|
||||
// Any other key means the box is being used again after a dismissal.
|
||||
if (event.key !== 'Tab') palette.show();
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
palette.move(1);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
palette.move(-1);
|
||||
break;
|
||||
case 'Enter': {
|
||||
event.preventDefault();
|
||||
const item = palette.selectedItem;
|
||||
if (item) pick(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A click anywhere else closes the panel. `mousedown` on a row calls
|
||||
* `preventDefault`, so picking a result never races this.
|
||||
*/
|
||||
function onpointerdown(event: PointerEvent) {
|
||||
if (!palette.open) return;
|
||||
const target = event.target;
|
||||
if (target instanceof Node && searchBox?.contains(target)) return;
|
||||
palette.hide();
|
||||
}
|
||||
|
||||
let searchBox: HTMLDivElement | null = $state(null);
|
||||
|
||||
/**
|
||||
* Why this page has stopped updating itself, when it has.
|
||||
*
|
||||
@@ -156,8 +57,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window {onpointerdown} />
|
||||
|
||||
<header class="topbar">
|
||||
<a class="brand" href="#/" aria-label="CodeGraph home">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
@@ -172,28 +71,7 @@
|
||||
<a href={flowHref()} class:active={view === 'flow'}>Flow</a>
|
||||
</nav>
|
||||
|
||||
<div class="search" role="search" bind:this={searchBox}>
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:value={palette.query}
|
||||
{onkeydown}
|
||||
onfocus={() => palette.show()}
|
||||
id="q"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder={'Search a symbol or file, or ask “how does execute reach getFile” — press / to focus'}
|
||||
aria-label="Search symbols and files"
|
||||
role="combobox"
|
||||
aria-expanded={palette.open}
|
||||
aria-controls="palette-panel"
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={palette.open ? `palette-row-${palette.selected}` : undefined}
|
||||
/>
|
||||
{#if palette.open}
|
||||
<SearchPalette onpick={pick} />
|
||||
{/if}
|
||||
</div>
|
||||
<SearchPalette bind:this={search} />
|
||||
|
||||
<div class="project" title="Indexed project">
|
||||
{#if liveNote}<span class="offline" title={liveNote.title}>{liveNote.text}</span>{/if}
|
||||
@@ -261,30 +139,6 @@
|
||||
border-bottom-color: var(--ink);
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
#q {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--rule-soft);
|
||||
background: var(--paper-2);
|
||||
color: var(--ink);
|
||||
font: 13px var(--sans);
|
||||
}
|
||||
|
||||
#q:focus {
|
||||
border-color: var(--ink);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#q::placeholder {
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.project {
|
||||
color: var(--ink-2);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user