@molecule/app-search-ui-react
v1.0.2
Published
React search UI for @molecule/app-client-search — a search box with filter chips and tips, a ranked results list with matched terms marked, and a quick-search dialog; styled through the ClassMap
Maintainers
Readme
@molecule/app-search-ui-react
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
React search UI for @molecule/app-client-search.
Three pieces that fit a useSearchSession from
@molecule/app-client-search-react: {@link SearchBox} (the field, its
clear button and shortcut cap, a help icon whose tooltip shows the search
syntax, an afterField slot for the consumer's own tabs, the active
filter chips and the hit count), {@link SearchResults} (ranked rows with the matched terms
marked, keyboard-active row, hover prefetch, an optional action per row),
and {@link QuickSearchDialog} (both in a mod+k dialog). Every class comes
from the ClassMap's search tokens (searchField, resultList, …), so a
UI bond restyles all of it at once.
Quick Start
import { useSearchSession } from '@molecule/app-client-search-react'
import { SearchBox, SearchResults } from '@molecule/app-search-ui-react'
import { useTranslation } from '@molecule/app-react'
function Catalog({ packages }: { packages: Pkg[] }) {
const navigate = useNavigate()
const { t } = useTranslation()
const session = useSearchSession({
docs: packages,
options: PACKAGE_INDEX_OPTIONS,
legacyHashField: 'category',
rememberKey: 'catalog.search',
onOpen: (p) => navigate(`/packages/${p.name}`),
})
return (
<>
<SearchBox
value={session.query}
onChange={session.setQuery}
onKeyDown={session.onKeyDown}
inputRef={session.inputRef}
placeholder={t('catalog.search', undefined, { defaultValue: 'Search packages' })}
count={session.hits.length}
filters={session.parsed.filters}
onRemoveFilter={session.removeFilter}
onClear={session.clear}
examples={['category:auth', 'type:bond']}
/>
<SearchResults
hits={session.hits}
view={(p) => ({
href: `/packages/${p.name}`,
title: `@molecule/${p.name}`,
description: p.description,
meta: [p.type, p.category],
mono: true,
})}
activeIndex={session.activeIndex}
onActivate={session.setActiveIndex}
onOpen={session.open}
onPrefetch={session.prefetch}
renderLink={(p) => (
<Link
to={p.href}
state={p.state}
className={p.className}
onClick={p.onClick}
data-mol-id={p['data-mol-id']}
>
{p.children}
</Link>
)}
emptyText={t('catalog.empty', undefined, { defaultValue: 'Nothing matches' })}
/>
</>
)
}(searchPlaceholder and nothingMatches are the app's own translated strings.)
Type
feature
Installation
npm install @molecule/app-search-ui-react @molecule/app-client-search @molecule/app-client-search-react @molecule/app-react @molecule/app-ui @molecule/app-ui-react react
npm install -D @types/reactAPI
Interfaces
HighlightProps
Props for {@link Highlight}.
interface HighlightProps {
/** The text to show. */
text: string
/** The matched terms to mark (a hit's `terms`). */
terms: string[]
}KbdProps
Props for {@link Kbd}.
interface KbdProps {
/** The key or keys shown, e.g. `/`, `↵`, `esc`. */
children: ReactNode
/** Extra classes composed onto the cap. */
className?: string
}QuickSearchDialogProps
Props for {@link QuickSearchDialog}.
interface QuickSearchDialogProps<T extends ClientSearchDocument> {
/** Whether the dialog is open. Rendered only while open. */
open: boolean
/** Close it (Escape with nothing typed, the overlay, or after opening a hit). */
onClose: () => void
/** The search session (from `useSearchSession`), created with `syncUrl: false` and `onEscape: onClose`. */
session: SearchSession<T>
/** How a hit renders. */
view: (doc: T) => SearchResultView
/** Placeholder for the box, already translated. */
placeholder: string
/** Example tokens for the tips line. */
examples?: string[]
/** Shown while the text matches nothing, already translated. */
emptyText: string
/** Shown before anything is typed, already translated. Defaults to the empty text. */
idleText?: string
/** Renders each row's link (a router link keeps navigation client-side). */
renderLink?: (props: SearchResultLinkProps) => ReactNode
/** `data-mol-id` prefix. Defaults to `quick-search`. */
molId?: string
}SearchBoxProps
Props for {@link SearchBox}.
interface SearchBoxProps {
/** The text. */
value: string
/** Called with the new text on every keystroke. */
onChange: (value: string) => void
/** Key handling for the input (arrows, Enter, Escape), usually a session's `onKeyDown`. */
onKeyDown?: (event: ReactKeyboardEvent<HTMLInputElement>) => void
/** A ref to the input, so a shortcut can focus it. */
inputRef?: RefObject<HTMLInputElement | null>
/** Placeholder, already translated. Also the accessible name unless `label` is given. */
placeholder: string
/** Accessible name for the input, already translated. Defaults to `placeholder`. */
label?: string
/** How many hits the current text has. Omit to show no count. */
count?: number
/** Active `field:value` filters, shown as removable chips. */
filters?: ClientSearchFilter[]
/** Removes the i-th filter. */
onRemoveFilter?: (index: number) => void
/** Clears the text. Shown as an × while there is text. */
onClear?: () => void
/**
* Example tokens for the search tips, e.g. `['category:auth', 'type:bond']`.
* Given, a help icon sits inside the field and shows the syntax on hover or
* focus. Omit for no help icon.
*/
examples?: string[]
/** The key that focuses the box, shown as a cap while it is empty. `null` hides it. Defaults to `/`. */
shortcut?: string | null
/** Focus the input on mount. */
autoFocus?: boolean
/**
* Rendered directly under the field, before the status row (chips and
* count), so a consumer can put its own controls — tabs, a scope switch —
* right below the input.
*/
afterField?: ReactNode
/** `data-mol-id` prefix: `<molId>-input`, `-clear`, `-help`, `-filter`, `-count`. */
molId?: string
/** Extra classes composed onto the field. */
className?: string
}SearchResultHit
What the list needs from a hit. A session's hits already have this shape.
interface SearchResultHit<T> {
id: string
doc: T
/** The matched terms, marked in the title and description. */
terms: string[]
/** Found by meaning rather than by the words typed. */
related?: boolean
}SearchResultLinkProps
Props the row's link gets; renderLink turns them into an element.
interface SearchResultLinkProps {
href: string
className: string
children: ReactNode
onClick: (event: ReactMouseEvent<HTMLElement>) => void
state?: unknown
'data-mol-id': string
}SearchResultsProps
Props for {@link SearchResults}.
interface SearchResultsProps<T> {
hits: SearchResultHit<T>[]
/** How a hit renders. */
view: (doc: T) => SearchResultView
/** The keyboard- or pointer-active row, or `-1`. */
activeIndex: number
/** The pointer moved onto a row. */
onActivate?: (index: number) => void
/** A row was chosen (a plain click, or Enter elsewhere). Navigation is the caller's. */
onOpen: (doc: T) => void
/** The person is about to open a row (hover, focus, touch): warm its page. */
onPrefetch?: (doc: T) => void
/**
* Renders the row's link. Defaults to a plain `<a>`; pass a router link to
* keep navigation client-side (spread `props` onto it; `state` is optional).
*/
renderLink?: (props: SearchResultLinkProps) => ReactNode
/** Something at the row's end (an action button). */
action?: (doc: T) => ReactNode
/** Shown when there are no hits. */
emptyText: string
/** `data-mol-id` prefix: `<molId>-results`, `-hit`, `-link`, `-related`, `-empty`. */
molId?: string
/** Extra classes composed onto the list. */
className?: string
}SearchResultView
How one hit renders.
interface SearchResultView {
/** Where the row goes. */
href: string
/** The row's title. */
title: string
/** Under the title, clamped to two lines. */
description?: string
/** Small facts after the title: type, category, source. */
meta?: string[]
/** Render the title in monospace (a package name). */
mono?: boolean
/** Opaque data handed to `renderLink` (router state). */
state?: unknown
}Functions
Highlight(props)
Text with the matched terms marked: each term is marked where it starts a
word, case-insensitively, through the ClassMap's resultMark.
function Highlight({ text, terms }: HighlightProps): JSX.Elementprops— The text and the terms.
Returns: The text with <mark> around each match.
Kbd(props)
A key cap, for shortcut hints (/ to focus, ↵ to open).
function Kbd({ children, className }: KbdProps): JSX.Elementprops— The key label.
Returns: A <kbd> styled by the ClassMap.
QuickSearchDialog(props)
A search dialog for mod+k: the box on top with focus, the hits below in
a scrolling pane, and the key hints in the footer. Before anything is
typed the pane shows idleText rather than a browse list. Enter opens the active
hit through the session (which closes the dialog by the caller's onOpen).
function QuickSearchDialog({
open,
onClose,
session,
view,
placeholder,
examples,
emptyText,
idleText,
renderLink,
molId = 'quick-search',
}: QuickSearchDialogProps<T>): JSX.Element | nullprops— See {@link QuickSearchDialogProps}.
Returns: The dialog, or nothing while closed.
SearchBox(props)
A search field: a leading icon, the input, a help icon that shows the
syntax on hover, an × to clear, and the shortcut cap; under it, whatever
afterField holds, then the active filters as removable chips and the
hit count. The field stays mounted, and its text with it, across searches.
function SearchBox({
value,
onChange,
onKeyDown,
inputRef,
placeholder,
label,
count,
filters = [],
onRemoveFilter,
onClear,
examples,
shortcut = '/',
autoFocus,
afterField,
molId = 'search',
className,
}: SearchBoxProps): JSX.Elementprops— See {@link SearchBoxProps}.
Returns: The field and its status row.
SearchResults(props)
The ranked hits: title and description with the matched terms marked, the meta line, the active row marked for keyboard users, hover making a row active and warming its page. The whole row is the link.
function SearchResults({
hits,
view,
activeIndex,
onActivate,
onOpen,
onPrefetch,
renderLink = defaultLink,
action,
emptyText,
molId = 'search',
className,
}: SearchResultsProps<T>): JSX.Elementprops— See {@link SearchResultsProps}.
Returns: The list, or the empty state.
Injection Notes
Requirements
Peer dependencies:
@molecule/app-client-search^1.0.0@molecule/app-client-search-react^1.0.0@molecule/app-react^1.0.1@molecule/app-ui^1.3.0@molecule/app-ui-react^1.2.0react^18.0.0 || ^19.0.0
Runtime Dependencies
@molecule/app-client-search@molecule/app-client-search-react@molecule/app-react@molecule/app-ui@molecule/app-ui-reactreactPass a router link through
renderLink, or every row is a full page load: the list renders a plain<a>by default because it does not know your router. A plain click callsonOpen(which the session turns into navigation); a modified click is left to the browser so "open in new tab" works.The box does not own the text: bind
value/onChangeto a session (or any state) and it stays mounted, with its text, across searches. Do not remount it per search.placeholder,emptyTextand the like are already-translated strings from the app; the package translates only its own chrome ("Clear search", "{{count}} results", "Search tips") through@molecule/app-locales-search-ui.The quick dialog needs a session created with
syncUrl: falseandonEscape: onClose; the page's own session keeps the URL.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- [ ] The field shows its placeholder and the shortcut cap; the cap goes away while typing and the × clears the text and keeps focus.
- [ ] A
field:valuein the text appears as a chip; its × removes only that part. - [ ] Results update on every keystroke with the matched words marked; the count agrees with the rows.
- [ ] Arrow keys move the highlight, Enter opens the highlighted row and the URL changes; a plain click on a row does the same; Cmd/Ctrl-click opens a new tab.
- [ ]
mod+kopens the dialog with focus in its field; Escape with an empty field closes it; opening a hit closes it and navigates.
Translations
Translation strings are provided by @molecule/app-locales-search-ui.
