Skip to content

Add <Search />: site-wide search as a command palette - #377

Open
NullVoxPopuli-ai-agent wants to merge 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:nvp/search-palette
Open

Add <Search />: site-wide search as a command palette#377
NullVoxPopuli-ai-agent wants to merge 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:nvp/search-palette

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

The K for a kolay site. Built on <CommandPalette>, which shipped in ember-primitives 0.62.0.

import { Search } from 'kolay/components';

<template>
  <Search />
</template>

Nothing to index and nothing to configure. The docs() plugin already writes every page's title, headings, and prose into the compiled docs, so this is searcher rendered, the utility that shipped in #372 and until now had only the search page to be used by. Same for stripFormatting and highlightSearch, which produce and mark the excerpts.

What it is

<CommandPalette> filled in with kolay's index, inside a <Modal> so the trigger button can sit outside the dialog:

  • the <dialog> handles the layer, the focus trap, and returning focus to whatever opened it
  • aria-activedescendant handles the keyboard, so / move the selection while the caret stays where the reader is typing
  • Enter clicks the active result, which is a real link, so the router navigates and -click still opens a new tab
  • Esc, and a click outside, close it

Async throughout: a plain .md page's text is fetched on demand, and the palette re-activates the best result whenever the list changes underneath.

Arguments and blocks

@hotkey (default "mod+k", "" to disable), @minLength (3), @limit (20), @placeholder.

Both blocks are optional and replace a default. :trigger is yielded open and the modifier that returns focus to the trigger when the palette closes. :result is yielded the result and the query.

Styling

Default styles ship in kolay.css, which consumers already import. Colours are --kolay-search-* variables, so a host site sets values rather than restating layout. The docs app maps them to pico's in six lines.

The active result is [data-active="true"], set by the pointer and the keyboard alike. There is no :hover rule, deliberately: one selector for both inputs means they can never disagree about what Enter will do.

The palette's stylesheet also answers frameworks that style a bare dialog as a full-screen box around an <article> card. Pico is one. min-width: 100% outranks any width the palette sets, and centring the children squashes the input and the results.

Two fixes this needed

The addon typecheck. {{on}} did not typecheck anywhere in src/, which is why there was no way to write a trigger button by hand. Two tsconfig settings disagreed with what Glint needs: moduleResolution: node16 stopped Glint's module augmentations from attaching, so on stayed the bare Opaque<"modifier:on"> that ember-source declares; and types named @glint/ember-tsc, the tool's own API, rather than @glint/ember-tsc/types, the file carrying those augmentations. types also read ember-source out of docs-app/node_modules and now reads the addon's own copy. With the augmentations attached, one real error surfaced in typedoc/signature/component.gts, fixed in the same commit.

stripFormatting and inline HTML. An excerpt rendered press <kbd>K</kbd> verbatim. It drops inline HTML now, keeping the text it wrapped. Lowercase tag names only, so <Search /> written in prose survives.

The docs site

The header's search form is replaced by the palette, which deletes ~75 lines of form and scoped CSS. The /search page stays: the palette is for looking something up and leaving, the page is for a search worth linking to.

Verified

Suites: markdown-only 14/14, docs-app 97 (93 pass, 4 pre-existing skips), test:node 250/250, pnpm lint 21/21 including lint:published-types across node10, node16, and bundler.

Driven in a real browser on the docs app, light and dark: the trigger opens the dialog, the input is a focused combobox, typing gives ranked results with role="option" and highlighted query marks, moves aria-activedescendant and [data-active] together, and Enter navigates and closes the dialog.

🤖 Generated with Claude Code

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

@NullVoxPopuli is attempting to deploy a commit to the universal-ember Team on Vercel.

A member of the Team first needs to authorize it.

NullVoxPopuli and others added 2 commits August 19, 2026 21:12
`{{on}}` did not typecheck anywhere in `src/`, because two tsconfig settings
disagreed with what Glint needs:

- `moduleResolution: node16` stopped Glint's module augmentations from
  attaching, so `on` stayed the bare `Opaque<"modifier:on">` that
  ember-source declares and no `resolve` overload accepted it.
- `types` named `@glint/ember-tsc`, which is the tool's own API, rather than
  `@glint/ember-tsc/types`, which is the file that carries those
  augmentations.

`types` also read ember-source out of `docs-app/node_modules`. It reads the
addon's own copy now.

With the augmentations attached, a real error appears: `getSignature` returns
`undefined` when a declaration carries no signature, and the union check does
not narrow that away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`<Search />` is `<CommandPalette>` from ember-primitives, filled in with what
`searcher` ranks. It renders a trigger button and a modal `<dialog>`, and it
needs nothing configured: the `docs()` plugin already wrote every page's
title, headings, and prose into the compiled docs.

The `:trigger` block replaces the button, and is yielded `open` and the
modifier that returns focus to it. The `:result` block replaces the group,
title, and excerpt.

`stripFormatting` now drops inline HTML, keeping the text it wrapped, so an
excerpt reads "press K" rather than "press <kbd>K</kbd>". Lowercase tag names
only, so a component written in prose survives.

The palette's own stylesheet answers frameworks that style a bare `dialog` as
a full-screen box around an `<article>`: `min-width: 100%` outranks any width
the palette sets, and centring the children squashes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent marked this pull request as ready for review August 20, 2026 01:13
@tracked query = '';

get hotkey() {
return this.args.hotkey ?? DEFAULT_HOTKEY;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all way too much code.

We don't want Hotkey customizable here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants