Parent design: #420
Depends on: #421
Purpose
Add the user-facing documentation search experience to the existing persistent Reference drawer, using the SchemaCatalogService.docSearch() API from #421.
This issue owns drawer chrome, search submission, result rendering, focus and keyboard behavior, stale-response suppression, Back-stack snapshots, responsive layout, accessibility, and regression coverage. It must not execute SQL or duplicate catalog ranking/normalization logic.
Header design
Add a persistent search form to the Reference drawer header:
Reference [ Search functions, engines, types, settings… ] [Search] ×
Requirements:
- Keep “Reference” visible as the drawer title.
- Keep the close button as the rightmost control.
- The search form survives every body-state replacement.
- The input grows to use available width.
- At narrow drawer widths, allow the title/close row and search form to wrap onto two rows.
- Never shrink the close target or make the search input unusably narrow.
Use semantic controls:
<form role="search" aria-label="Search ClickHouse documentation">
<input
type="search"
aria-label="Search documentation"
autocomplete="off"
spellcheck="false"
>
<button type="submit" aria-label="Search documentation">Search</button>
</form>
Suggested placeholder:
Search functions, engines, types, settings…
Submission behavior
- Submit explicitly with Enter or the Search button.
- Do not issue a request for empty, whitespace-only, or one-character normalized input.
- Do not search on every keystroke.
- Keep focus in the input after results, no-results, or unavailable settles.
- Disable or mark the button busy while the identical query is in flight.
- A different submitted query supersedes the earlier one.
- The pane token must suppress any late result from an older search.
The UI delegates to:
app.catalog.docSearch(query)
No SQL or source-table knowledge belongs in doc-pane.ts.
Keyboard behavior
Enter in the search input submits.
ArrowDown in the input focuses the first result when one exists.
/ while focus is inside the drawer but not in a text-editing control focuses the search input.
Escape with non-empty search text clears the input and current search state first.
Escape with an empty search input preserves the drawer’s existing close behavior.
- Escape handled inside the drawer must continue to prevent the global shortcut handler from also cancelling a running query.
- Browser-reserved shortcuts remain browser-owned.
Search result state
Add a new body state alongside loading, found entry, disambiguation, missing, and unavailable.
Example:
23 results for “array”
[aggregate function] groupArray
Creates an array of argument values…
[function] arrayJoin
Unfolds an array into rows…
[data type] Array
An array of values of type T…
Render with semantic list markup:
<div role="status" aria-live="polite">23 results for “array”</div>
<ul aria-label="Documentation search results">
<li><button type="button">…</button></li>
</ul>
Each result button contains:
- logical
DocKind badge;
- canonical title/name;
- one or two lines of plain-text summary;
- optional safe visual highlighting of matched name fragments.
Do not render server result text as HTML.
Physical source_table provenance remains in the model for diagnostics/testing but does not replace the logical kind badge in the primary UI.
Result selection
Selecting a result must:
- capture the current search snapshot;
- push that snapshot onto the existing bounded Back stack;
- call the existing target-aware entry lookup path with the result’s
DocTarget;
- render the existing structured/Markdown documentation entry;
- preserve all existing alias, related-entry, and disambiguation behavior.
Search results are not full documentation entries. Do not add another entry renderer.
Back-stack search snapshots
Extend the pane’s existing BackEntry union:
type BackEntry =
| { kind: 'target'; target: DocTarget }
| { kind: 'disambiguation'; name: string }
| {
kind: 'search';
query: string;
response: DocSearchResponse;
selectedIndex: number;
scrollTop: number;
connectionGeneration: number;
};
When navigating from results to an entry, store:
- normalized query;
- bounded search response;
- selected result index;
- result-body scroll position;
- active connection generation.
Back from the entry restores:
- input text;
- the same result list without another SQL request;
- result count;
- scroll position;
- focus to the previously selected result when still connected.
If the connection generation changed, do not restore stale results; clear the invalid snapshot and require a new search.
The existing Back-stack cap remains authoritative.
Loading state
During a search:
- retain the query in the input;
- set
aria-busy="true" on the result/body region;
- show a button spinner or disabled busy state;
- prevent duplicate manual submission of the identical in-flight query.
Using the existing full-body loading state is acceptable initially. Retaining the previous body until the replacement settles is preferred but not required.
No-results state
When docSearch() returns missing, render:
No documentation found for “xyz”.
Keep the query and input focus. This is distinct from a target-specific missing documentation entry.
Unavailable state
When docSearch() returns unavailable, reuse the drawer’s quiet unavailable/Retry pattern:
Reference search isn't available on this server or connection.
Retry reruns the current normalized query through docSearch().
Do not show a toast for capability or permission failures.
Pane state and lifecycle
Likely additions:
interface PaneState {
// existing fields
searchInput: HTMLInputElement;
searchButton: HTMLButtonElement;
currentSearch: {
query: string;
response: DocSearchResponse;
} | null;
}
Required stale-response behavior:
- submitting a new search invalidates the previous search paint;
- selecting a result invalidates an in-flight search paint;
- external
openDocEntry() or openDocDisambiguation() invalidates in-flight search paint and begins its existing fresh browsing session;
- closing the drawer invalidates pending search responses;
- sign-out/reconnect invalidates pending search responses;
- no late response may replace newer body content.
Focus behavior
- Opening an external entry continues to record the initiating focus target.
- Search submission does not unexpectedly move focus.
- Result count is announced through
aria-live.
- ArrowDown explicitly moves to the first result.
- Selecting a result moves focus according to the existing entry-rendering convention.
- Back restores result focus when possible.
- Closing the drawer restores focus through the existing initiator logic.
- Search controls remain usable after entry, alias, related, disambiguation, missing, unavailable, and Back transitions.
Styling
Add styles for:
- flexible/wrapping Reference header;
- search input and button sizing;
- busy/disabled search state;
- result count/status;
- semantic result list reset;
- full-width result buttons;
- kind badge/title/summary layout;
- one/two-line summary truncation;
- hover, keyboard focus, and restored-selection states;
- no-results and unavailable states;
- narrow drawer widths.
Do not alter the drawer’s persisted-width behavior or make the pane modal.
Suggested implementation boundary
src/ui/doc-pane.ts
Own:
- persistent search form;
- submission and keyboard handling;
- pane-token stale checks;
- result/loading/missing/unavailable rendering;
- result selection through existing lookup;
- search Back snapshots;
- focus and scroll restoration;
- live-region announcements.
src/styles.css
Own only layout and visual states.
src/ui/app.types.ts or narrow pane contract
Expose catalog.docSearch() without broadening dependencies unnecessarily.
Tests
Header
- Reference title remains present.
- Search form has
role="search" and an accessible name.
- Input and button are labelled.
- Close remains rightmost.
- Header safely wraps at constrained width.
- Search form survives body replacement.
Submission
- Enter submits.
- Button submits.
- empty/whitespace/one-character input does not call the service.
- normalized query is passed to the service.
- duplicate identical submission while in flight does not cause another request.
- a different second query supersedes the first.
Stale responses
- late first search cannot replace second search results.
- late search cannot replace a selected entry.
- late search cannot repaint a closed pane.
- reconnect/sign-out prevents old search results from rendering.
- external F1/hover/completion open suppresses an in-flight search.
Results
- count and query render correctly.
- count is announced with
aria-live.
- results use semantic list/buttons.
- kind, title, and summary render.
- summaries are text, not HTML.
- physical source remains available in the model but logical kind is primary.
missing renders the search no-results state.
unavailable renders Retry.
- Retry repeats the current query.
Selection and Back
- selecting a result calls the existing target-aware lookup.
- search snapshot is pushed before entry navigation.
- Back restores query, result rows, selected index, focus, and scroll.
- Back does not rerun SQL while the snapshot remains generation-valid.
- changed generation rejects the snapshot.
- existing Back-stack cap still applies.
Keyboard/focus
- ArrowDown focuses first result.
/ focuses search only from appropriate non-text drawer targets.
- Escape clears non-empty search before closing.
- Escape with empty search closes the pane.
- Escape does not also cancel a running query.
- drawer close restores the valid initiator.
Regressions
- editor hover opening remains unchanged.
- completion info opening remains unchanged.
- F1 entry/disambiguation remains unchanged.
- aliases and related entries remain unchanged.
- existing loading, missing, unavailable, Retry, and Markdown rendering remain unchanged.
- CodeViewer teardown remains correct.
- drawer resizing and persisted width remain unchanged.
Acceptance criteria
- The persistent Reference header contains a responsive, accessible search form.
- Search is explicitly submitted and delegates only to
catalog.docSearch().
- Results render as a safe semantic list with kind, name, and summary.
- Selecting a result uses the existing full-entry lookup/rendering path.
- Search Back snapshots restore query, results, selection, focus, and scroll without rerunning SQL.
- Pane tokens and connection generation prevent stale search repaint.
- Empty, loading, no-results, and unavailable states behave distinctly.
- Escape, ArrowDown,
/, close, and focus restoration are accessible and do not conflict with global shortcuts.
- Existing Reference entry/disambiguation/navigation behavior remains intact.
Non-goals
- SQL construction, capability probing, ranking, merge, or caching logic.
- Search-as-you-type.
- Kind filter UI.
- Position/proximity-aware ranking.
- Remote documentation search.
- New full-entry rendering.
Parent design: #420
Depends on: #421
Purpose
Add the user-facing documentation search experience to the existing persistent Reference drawer, using the
SchemaCatalogService.docSearch()API from #421.This issue owns drawer chrome, search submission, result rendering, focus and keyboard behavior, stale-response suppression, Back-stack snapshots, responsive layout, accessibility, and regression coverage. It must not execute SQL or duplicate catalog ranking/normalization logic.
Header design
Add a persistent search form to the Reference drawer header:
Requirements:
Use semantic controls:
Suggested placeholder:
Submission behavior
The UI delegates to:
No SQL or source-table knowledge belongs in
doc-pane.ts.Keyboard behavior
Enterin the search input submits.ArrowDownin the input focuses the first result when one exists./while focus is inside the drawer but not in a text-editing control focuses the search input.Escapewith non-empty search text clears the input and current search state first.Escapewith an empty search input preserves the drawer’s existing close behavior.Search result state
Add a new body state alongside loading, found entry, disambiguation, missing, and unavailable.
Example:
Render with semantic list markup:
Each result button contains:
DocKindbadge;Do not render server result text as HTML.
Physical
source_tableprovenance remains in the model for diagnostics/testing but does not replace the logical kind badge in the primary UI.Result selection
Selecting a result must:
DocTarget;Search results are not full documentation entries. Do not add another entry renderer.
Back-stack search snapshots
Extend the pane’s existing
BackEntryunion:When navigating from results to an entry, store:
Back from the entry restores:
If the connection generation changed, do not restore stale results; clear the invalid snapshot and require a new search.
The existing Back-stack cap remains authoritative.
Loading state
During a search:
aria-busy="true"on the result/body region;Using the existing full-body loading state is acceptable initially. Retaining the previous body until the replacement settles is preferred but not required.
No-results state
When
docSearch()returnsmissing, render:Keep the query and input focus. This is distinct from a target-specific missing documentation entry.
Unavailable state
When
docSearch()returnsunavailable, reuse the drawer’s quiet unavailable/Retry pattern:Retry reruns the current normalized query through
docSearch().Do not show a toast for capability or permission failures.
Pane state and lifecycle
Likely additions:
Required stale-response behavior:
openDocEntry()oropenDocDisambiguation()invalidates in-flight search paint and begins its existing fresh browsing session;Focus behavior
aria-live.Styling
Add styles for:
Do not alter the drawer’s persisted-width behavior or make the pane modal.
Suggested implementation boundary
src/ui/doc-pane.tsOwn:
src/styles.cssOwn only layout and visual states.
src/ui/app.types.tsor narrow pane contractExpose
catalog.docSearch()without broadening dependencies unnecessarily.Tests
Header
role="search"and an accessible name.Submission
Stale responses
Results
aria-live.missingrenders the search no-results state.unavailablerenders Retry.Selection and Back
Keyboard/focus
/focuses search only from appropriate non-text drawer targets.Regressions
Acceptance criteria
catalog.docSearch()./, close, and focus restoration are accessible and do not conflict with global shortcuts.Non-goals