@dbx-tools/ui-search 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @dbx-tools/ui-search
2
+
3
+ React search box and results for Databricks AI Search.
4
+
5
+ Import this package when an AppKit UI wants a drop-in, search-as-you-type box
6
+ over the [`@dbx-tools/search`](../../node/search) plugin. The components
7
+ read the plugin's boot config (indexes, default index, page size, route path)
8
+ through AppKit's `usePluginClientConfig`, so search is one component and zero
9
+ props.
10
+
11
+ **Key features:**
12
+
13
+ - `SearchBox` - a debounced, cancellable search-as-you-type input with a results
14
+ dropdown, styled with AppKit tokens. Meilisearch-style instant search.
15
+ - `SearchResults` - a presentational hit list for a full-page results layout.
16
+ - `useSearch` - the hook the components share: `{ query, setQuery, hits,
17
+ loading, error, config, submit, clear }`, debounced and abortable, targeting
18
+ the single-index or universal (federated) route.
19
+ - Universal search with one flag (`universal`) to search every configured index.
20
+ - `renderHit` overrides on both components for full control of a row; a sensible
21
+ default shows a title, id, and score.
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import { SearchBox } from "@dbx-tools/ui-search/react";
27
+ import "@dbx-tools/ui-search/styles.css";
28
+
29
+ export function DocsSearch() {
30
+ return <SearchBox placeholder="Search docs…" onSelect={(hit) => open(hit.id)} />;
31
+ }
32
+ ```
33
+
34
+ Universal search across every index the plugin knows about:
35
+
36
+ ```tsx
37
+ <SearchBox universal showIndex placeholder="Search everything…" />
38
+ ```
39
+
40
+ ## Use The Hook
41
+
42
+ ```tsx
43
+ import { useSearch, SearchResults } from "@dbx-tools/ui-search/react";
44
+
45
+ function Results() {
46
+ const { query, setQuery, hits, loading } = useSearch({ limit: 20 });
47
+ return (
48
+ <>
49
+ <input value={query} onChange={(e) => setQuery(e.target.value)} />
50
+ {loading ? "…" : <SearchResults hits={hits} />}
51
+ </>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Subpaths
57
+
58
+ - `@dbx-tools/ui-search/react` - `SearchBox`, `SearchResults`, `useSearch`,
59
+ and the re-exported `SearchHit` / `SearchResult` / `SearchMode` /
60
+ `SearchClientConfig` types.
61
+ - `@dbx-tools/ui-search/styles.css` - AppKit-token styling for the box and
62
+ results; import once after Tailwind and your AppKit-UI theme.
63
+
64
+ Runtime search, routes, and the plugin live in
65
+ [`@dbx-tools/search`](../../node/search); the wire contract lives in
66
+ [`@dbx-tools/shared-search`](../../shared/search).
package/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as reactSearchBox from "./src/react/search-box.tsx";
6
+ export * as reactSearchResults from "./src/react/search-results.tsx";
7
+ export * as reactUseSearch from "./src/react/use-search.ts";
8
+ export { SearchBox } from "./src/react/search-box.tsx";
9
+ export type { SearchBoxProps } from "./src/react/search-box.tsx";
10
+ export { SearchResults } from "./src/react/search-results.tsx";
11
+ export type { SearchResultsProps } from "./src/react/search-results.tsx";
12
+ export { useSearch } from "./src/react/use-search.ts";
13
+ export type { UseSearchOptions, UseSearchState } from "./src/react/use-search.ts";
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@dbx-tools/ui-search",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/reggie-db/dbx-tools.git",
6
+ "directory": "packages/ui/search"
7
+ },
8
+ "devDependencies": {
9
+ "@types/node": "^24.6.0",
10
+ "@types/react": "^19.2.2",
11
+ "@types/react-dom": "^19.2.2",
12
+ "tsx": "^4.23.0",
13
+ "typescript": "^5.9.3"
14
+ },
15
+ "dependencies": {
16
+ "lucide-react": "^0.554.0",
17
+ "react": "^19.2.4",
18
+ "react-dom": "^19.2.4",
19
+ "@dbx-tools/shared-core": "0.6.9",
20
+ "@dbx-tools/shared-search": "0.6.9",
21
+ "@dbx-tools/ui-appkit": "0.6.9"
22
+ },
23
+ "license": "UNLICENSED",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "version": "0.6.9",
28
+ "type": "module",
29
+ "exports": {
30
+ "./react": "./src/react/index.ts",
31
+ "./styles.css": "./src/styles.css",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "index.ts",
36
+ "src"
37
+ ],
38
+ "dbxToolsConfig": {
39
+ "tags": [
40
+ "ui"
41
+ ]
42
+ },
43
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
44
+ "scripts": {
45
+ "build": "projen build",
46
+ "compile": "projen compile",
47
+ "default": "projen default",
48
+ "package": "projen package",
49
+ "post-compile": "projen post-compile",
50
+ "pre-compile": "projen pre-compile",
51
+ "test": "projen test",
52
+ "watch": "projen watch",
53
+ "projen": "projen"
54
+ }
55
+ }
@@ -0,0 +1,16 @@
1
+ // React surface for `@dbx-tools/ui-search`: a drop-in `SearchBox`
2
+ // (search-as-you-type over Databricks AI Search), a `SearchResults` list for a
3
+ // full-page layout, and the `useSearch` hook they share. All three talk to the
4
+ // `@dbx-tools/search` plugin's routes and read its boot config through
5
+ // AppKit's `usePluginClientConfig`, so search is one component and zero props.
6
+ // Styled with AppKit tokens (import `@dbx-tools/ui-search/styles.css`).
7
+
8
+ export type {
9
+ SearchHit,
10
+ SearchResult,
11
+ SearchMode,
12
+ SearchClientConfig,
13
+ } from "@dbx-tools/shared-search";
14
+ export { SearchBox, type SearchBoxProps } from "./search-box.tsx";
15
+ export { SearchResults, type SearchResultsProps } from "./search-results.tsx";
16
+ export { useSearch, type UseSearchOptions, type UseSearchState } from "./use-search.ts";
@@ -0,0 +1,146 @@
1
+ // `SearchBox` — a drop-in, Meilisearch-style search-as-you-type input for
2
+ // Databricks AI Search. It wires an AppKit `Input` to the {@link useSearch}
3
+ // hook and renders the hits in a dropdown as the user types, so adding search
4
+ // to an app is one component and zero configuration: the plugin's client config
5
+ // supplies the index, page size, and route.
6
+ //
7
+ // It is presentational and unopinionated about what a hit looks like: pass a
8
+ // `renderHit` to control each row, or rely on the default which shows the first
9
+ // string-ish field as a title and the primary-key `id` as a subtitle. Styled
10
+ // with AppKit tokens (see `../styles.css`).
11
+
12
+ import type { SearchHit } from "@dbx-tools/shared-search";
13
+ import { Badge, Button, Input, ScrollArea, Spinner, cn } from "@dbx-tools/ui-appkit/react";
14
+ import { SearchIcon, XIcon } from "lucide-react";
15
+ import { useCallback, useState, type ReactNode } from "react";
16
+ import { useSearch, type UseSearchOptions } from "./use-search.ts";
17
+
18
+ /** Props for {@link SearchBox}. */
19
+ export interface SearchBoxProps extends UseSearchOptions {
20
+ /** Placeholder text for the input. */
21
+ placeholder?: string;
22
+ /** Called when the user picks a hit (click or Enter on a focused row). */
23
+ onSelect?: (hit: SearchHit) => void;
24
+ /** Render one hit row. Defaults to a title + id + score badge. */
25
+ renderHit?: (hit: SearchHit) => ReactNode;
26
+ /** Show the source index name on each hit (useful for universal search). */
27
+ showIndex?: boolean;
28
+ /** Extra class names for the outer container. */
29
+ className?: string;
30
+ }
31
+
32
+ /** Pick the most title-like string field from a hit for the default row. */
33
+ function hitTitle(hit: SearchHit): string {
34
+ for (const key of ["title", "name", "text", "content", "body"]) {
35
+ const value = hit.fields[key];
36
+ if (typeof value === "string" && value.trim()) return value;
37
+ }
38
+ const firstString = Object.values(hit.fields).find(
39
+ (value) => typeof value === "string" && value.trim(),
40
+ );
41
+ return typeof firstString === "string" ? firstString : hit.id;
42
+ }
43
+
44
+ /**
45
+ * A search-as-you-type box over AI Search. Renders an input and a results
46
+ * dropdown, debouncing keystrokes and cancelling stale requests through
47
+ * {@link useSearch}.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * import { SearchBox } from "@dbx-tools/ui-search/react";
52
+ * import "@dbx-tools/ui-search/styles.css";
53
+ *
54
+ * <SearchBox placeholder="Search docs…" onSelect={(hit) => open(hit.id)} />
55
+ * ```
56
+ */
57
+ export function SearchBox({
58
+ placeholder = "Search…",
59
+ onSelect,
60
+ renderHit,
61
+ showIndex,
62
+ className,
63
+ ...searchOptions
64
+ }: SearchBoxProps): ReactNode {
65
+ const { query, setQuery, hits, loading, error, clear, submit } = useSearch(searchOptions);
66
+ const [open, setOpen] = useState(false);
67
+
68
+ const handleSelect = useCallback(
69
+ (hit: SearchHit) => {
70
+ onSelect?.(hit);
71
+ setOpen(false);
72
+ },
73
+ [onSelect],
74
+ );
75
+
76
+ return (
77
+ <div className={cn("dbx-search-box", className)}>
78
+ <div className="dbx-search-box__field">
79
+ <SearchIcon className="dbx-search-box__icon" aria-hidden />
80
+ <Input
81
+ value={query}
82
+ placeholder={placeholder}
83
+ aria-label={placeholder}
84
+ onChange={(event) => {
85
+ setQuery(event.target.value);
86
+ setOpen(true);
87
+ }}
88
+ onFocus={() => setOpen(true)}
89
+ onKeyDown={(event) => {
90
+ if (event.key === "Enter") submit();
91
+ if (event.key === "Escape") setOpen(false);
92
+ }}
93
+ />
94
+ {loading ? <Spinner className="dbx-search-box__spinner" /> : null}
95
+ {query ? (
96
+ <Button
97
+ variant="ghost"
98
+ size="icon"
99
+ aria-label="Clear search"
100
+ onClick={() => {
101
+ clear();
102
+ setOpen(false);
103
+ }}
104
+ >
105
+ <XIcon />
106
+ </Button>
107
+ ) : null}
108
+ </div>
109
+
110
+ {open && (query.trim() || error) ? (
111
+ <div className="dbx-search-box__panel" role="listbox">
112
+ {error ? <div className="dbx-search-box__error">{error}</div> : null}
113
+ {!error && hits.length === 0 && !loading ? (
114
+ <div className="dbx-search-box__empty">No results</div>
115
+ ) : null}
116
+ <ScrollArea className="dbx-search-box__results">
117
+ {hits.map((hit) => (
118
+ <button
119
+ key={`${hit.index ?? ""}:${hit.id}`}
120
+ type="button"
121
+ role="option"
122
+ aria-selected={false}
123
+ className="dbx-search-box__hit"
124
+ onClick={() => handleSelect(hit)}
125
+ >
126
+ {renderHit ? (
127
+ renderHit(hit)
128
+ ) : (
129
+ <div className="dbx-search-box__hit-default">
130
+ <span className="dbx-search-box__hit-title">{hitTitle(hit)}</span>
131
+ <span className="dbx-search-box__hit-meta">
132
+ {showIndex && hit.index ? (
133
+ <Badge variant="secondary">{hit.index}</Badge>
134
+ ) : null}
135
+ <span className="dbx-search-box__hit-id">{hit.id}</span>
136
+ </span>
137
+ </div>
138
+ )}
139
+ </button>
140
+ ))}
141
+ </ScrollArea>
142
+ </div>
143
+ ) : null}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,75 @@
1
+ // `SearchResults` — a presentational list of AI Search hits for a full-page
2
+ // results layout (as opposed to the `SearchBox` dropdown). Give it `hits` from
3
+ // `useSearch` (or any `SearchResult`) and it renders a scored, index-tagged
4
+ // list; pass `renderHit` to control a row. Styled with AppKit tokens.
5
+
6
+ import type { SearchHit } from "@dbx-tools/shared-search";
7
+ import { Badge, cn } from "@dbx-tools/ui-appkit/react";
8
+ import type { ReactNode } from "react";
9
+
10
+ /** Props for {@link SearchResults}. */
11
+ export interface SearchResultsProps {
12
+ /** The hits to render, most relevant first. */
13
+ hits: SearchHit[];
14
+ /** Called when a hit is clicked. */
15
+ onSelect?: (hit: SearchHit) => void;
16
+ /** Render one hit row. Defaults to a title, fields, and a score badge. */
17
+ renderHit?: (hit: SearchHit) => ReactNode;
18
+ /** Show each hit's source index (useful for universal search). */
19
+ showIndex?: boolean;
20
+ /** Message shown when there are no hits. */
21
+ emptyMessage?: ReactNode;
22
+ /** Extra class names for the container. */
23
+ className?: string;
24
+ }
25
+
26
+ function hitTitle(hit: SearchHit): string {
27
+ for (const key of ["title", "name", "text", "content", "body"]) {
28
+ const value = hit.fields[key];
29
+ if (typeof value === "string" && value.trim()) return value;
30
+ }
31
+ return hit.id;
32
+ }
33
+
34
+ /** A full-page list of search hits. */
35
+ export function SearchResults({
36
+ hits,
37
+ onSelect,
38
+ renderHit,
39
+ showIndex,
40
+ emptyMessage = "No results",
41
+ className,
42
+ }: SearchResultsProps): ReactNode {
43
+ if (hits.length === 0) {
44
+ return <div className={cn("dbx-search-results__empty", className)}>{emptyMessage}</div>;
45
+ }
46
+ return (
47
+ <ul className={cn("dbx-search-results", className)}>
48
+ {hits.map((hit) => (
49
+ <li key={`${hit.index ?? ""}:${hit.id}`} className="dbx-search-results__item">
50
+ <button
51
+ type="button"
52
+ className="dbx-search-results__button"
53
+ onClick={() => onSelect?.(hit)}
54
+ >
55
+ {renderHit ? (
56
+ renderHit(hit)
57
+ ) : (
58
+ <>
59
+ <div className="dbx-search-results__header">
60
+ <span className="dbx-search-results__title">{hitTitle(hit)}</span>
61
+ <Badge variant="outline">{hit.score.toFixed(3)}</Badge>
62
+ </div>
63
+ {showIndex && hit.index ? (
64
+ <Badge variant="secondary" className="dbx-search-results__index">
65
+ {hit.index}
66
+ </Badge>
67
+ ) : null}
68
+ </>
69
+ )}
70
+ </button>
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ );
75
+ }
@@ -0,0 +1,161 @@
1
+ // `useSearch` — a small React hook that turns the AI Search plugin's HTTP
2
+ // routes into a debounced, cancellable search-as-you-type state machine. It
3
+ // reads the plugin's boot config (indexes, default index, page size, base path)
4
+ // via AppKit's `usePluginClientConfig`, so a search box needs no props to know
5
+ // where to POST. Point it at `@dbx-tools/search`'s `POST /api/search`
6
+ // (single index) or `POST /api/search/universal` (federated) route.
7
+
8
+ import type {
9
+ SearchClientConfig,
10
+ SearchHit,
11
+ SearchMode,
12
+ SearchResult,
13
+ } from "@dbx-tools/shared-search";
14
+ import { usePluginClientConfig } from "@dbx-tools/ui-appkit/react";
15
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
16
+
17
+ /** Options for {@link useSearch}. */
18
+ export interface UseSearchOptions {
19
+ /** Which index to search (name or alias). Defaults to the plugin's default index. */
20
+ index?: string;
21
+ /** Search every configured index and merge results (universal search). */
22
+ universal?: boolean;
23
+ /** Match mode. Defaults to the plugin's configured mode. */
24
+ mode?: SearchMode;
25
+ /** Max hits to request. Defaults to the plugin's configured page size. */
26
+ limit?: number;
27
+ /** Debounce in ms before a keystroke triggers a request. Defaults to 200. */
28
+ debounceMs?: number;
29
+ /** Minimum query length before searching. Defaults to 1. */
30
+ minLength?: number;
31
+ /** The plugin name to read config from / route under. Defaults to "search". */
32
+ pluginName?: string;
33
+ }
34
+
35
+ /** The state {@link useSearch} returns. */
36
+ export interface UseSearchState {
37
+ /** The current query text. */
38
+ query: string;
39
+ /** Set the query (triggers a debounced search). */
40
+ setQuery: (query: string) => void;
41
+ /** The latest hits, most relevant first. */
42
+ hits: SearchHit[];
43
+ /** True while a request is in flight. */
44
+ loading: boolean;
45
+ /** The last error, if any. */
46
+ error: string | null;
47
+ /** The resolved plugin config (indexes, default, page size). */
48
+ config: SearchClientConfig | undefined;
49
+ /** Run the search immediately (bypassing the debounce). */
50
+ submit: () => void;
51
+ /** Clear the query and hits. */
52
+ clear: () => void;
53
+ }
54
+
55
+ const DEFAULT_BASE_PATH = "/api/search";
56
+
57
+ /**
58
+ * Search-as-you-type against the AI Search plugin. Debounces input, cancels the
59
+ * previous request when a new one starts, and exposes `{ query, setQuery, hits,
60
+ * loading, error }` for a search box to render. Reads the plugin's client config
61
+ * for the base path, default index, and page size.
62
+ */
63
+ export function useSearch(options: UseSearchOptions = {}): UseSearchState {
64
+ const pluginName = options.pluginName ?? "search";
65
+ const config = usePluginClientConfig<SearchClientConfig>(pluginName);
66
+ const basePath = config?.basePath ?? DEFAULT_BASE_PATH;
67
+ const debounceMs = options.debounceMs ?? 200;
68
+ const minLength = options.minLength ?? 1;
69
+
70
+ const [query, setQueryState] = useState("");
71
+ const [hits, setHits] = useState<SearchHit[]>([]);
72
+ const [loading, setLoading] = useState(false);
73
+ const [error, setError] = useState<string | null>(null);
74
+
75
+ const abortRef = useRef<AbortController | null>(null);
76
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
77
+
78
+ const run = useCallback(
79
+ async (text: string) => {
80
+ abortRef.current?.abort();
81
+ if (text.trim().length < minLength) {
82
+ setHits([]);
83
+ setLoading(false);
84
+ return;
85
+ }
86
+ const controller = new AbortController();
87
+ abortRef.current = controller;
88
+ setLoading(true);
89
+ setError(null);
90
+ try {
91
+ const path = options.universal ? `${basePath}/universal` : basePath;
92
+ const body = options.universal
93
+ ? {
94
+ query: text,
95
+ ...(options.limit ? { limit: options.limit } : {}),
96
+ ...(options.mode ? { mode: options.mode } : {}),
97
+ }
98
+ : {
99
+ query: text,
100
+ ...(options.index ? { index: options.index } : {}),
101
+ ...(options.limit ? { limit: options.limit } : {}),
102
+ ...(options.mode ? { mode: options.mode } : {}),
103
+ };
104
+ const response = await fetch(path, {
105
+ method: "POST",
106
+ headers: { "content-type": "application/json" },
107
+ body: JSON.stringify(body),
108
+ signal: controller.signal,
109
+ });
110
+ if (!response.ok) {
111
+ throw new Error(`search failed (${response.status})`);
112
+ }
113
+ const result = (await response.json()) as SearchResult;
114
+ setHits(result.hits ?? []);
115
+ } catch (err) {
116
+ if ((err as Error).name === "AbortError") return;
117
+ setError((err as Error).message);
118
+ setHits([]);
119
+ } finally {
120
+ if (abortRef.current === controller) setLoading(false);
121
+ }
122
+ },
123
+ [basePath, minLength, options.index, options.limit, options.mode, options.universal],
124
+ );
125
+
126
+ const setQuery = useCallback(
127
+ (text: string) => {
128
+ setQueryState(text);
129
+ if (timerRef.current) clearTimeout(timerRef.current);
130
+ timerRef.current = setTimeout(() => void run(text), debounceMs);
131
+ },
132
+ [debounceMs, run],
133
+ );
134
+
135
+ const submit = useCallback(() => {
136
+ if (timerRef.current) clearTimeout(timerRef.current);
137
+ void run(query);
138
+ }, [query, run]);
139
+
140
+ const clear = useCallback(() => {
141
+ if (timerRef.current) clearTimeout(timerRef.current);
142
+ abortRef.current?.abort();
143
+ setQueryState("");
144
+ setHits([]);
145
+ setError(null);
146
+ setLoading(false);
147
+ }, []);
148
+
149
+ useEffect(
150
+ () => () => {
151
+ if (timerRef.current) clearTimeout(timerRef.current);
152
+ abortRef.current?.abort();
153
+ },
154
+ [],
155
+ );
156
+
157
+ return useMemo(
158
+ () => ({ query, setQuery, hits, loading, error, config, submit, clear }),
159
+ [query, setQuery, hits, loading, error, config, submit, clear],
160
+ );
161
+ }
package/src/styles.css ADDED
@@ -0,0 +1,156 @@
1
+ /*
2
+ * @dbx-tools/ui-search stylesheet.
3
+ *
4
+ * Import once from the host app's Tailwind entry, after Tailwind and
5
+ * your AppKit-UI theme:
6
+ *
7
+ * @import "tailwindcss";
8
+ * @import "@databricks/appkit-ui/styles.css";
9
+ * @import "@dbx-tools/ui-search/styles.css";
10
+ *
11
+ * Defines NO design tokens - the search box / results style exclusively with
12
+ * AppKit semantic tokens and inherit the host app's theme. Registers this
13
+ * package's React components with Tailwind via `@source`.
14
+ */
15
+
16
+ @import "@dbx-tools/ui-appkit/styles.css";
17
+
18
+ @source "./react";
19
+
20
+ .dbx-search-box {
21
+ position: relative;
22
+ width: 100%;
23
+ }
24
+
25
+ .dbx-search-box__field {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 0.5rem;
29
+ }
30
+
31
+ .dbx-search-box__icon {
32
+ position: absolute;
33
+ left: 0.625rem;
34
+ width: 1rem;
35
+ height: 1rem;
36
+ color: var(--muted-foreground, currentColor);
37
+ pointer-events: none;
38
+ }
39
+
40
+ .dbx-search-box__field :where(input) {
41
+ padding-left: 2rem;
42
+ }
43
+
44
+ .dbx-search-box__spinner {
45
+ position: absolute;
46
+ right: 2.25rem;
47
+ }
48
+
49
+ .dbx-search-box__panel {
50
+ position: absolute;
51
+ z-index: 50;
52
+ margin-top: 0.25rem;
53
+ width: 100%;
54
+ overflow: hidden;
55
+ border-radius: var(--radius, 0.5rem);
56
+ border: 1px solid var(--border, currentColor);
57
+ background: var(--popover, var(--background));
58
+ color: var(--popover-foreground, var(--foreground));
59
+ box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.35);
60
+ }
61
+
62
+ .dbx-search-box__results {
63
+ max-height: 20rem;
64
+ }
65
+
66
+ .dbx-search-box__hit {
67
+ display: block;
68
+ width: 100%;
69
+ text-align: left;
70
+ padding: 0.5rem 0.75rem;
71
+ cursor: pointer;
72
+ background: transparent;
73
+ border: 0;
74
+ color: inherit;
75
+ }
76
+
77
+ .dbx-search-box__hit:hover,
78
+ .dbx-search-box__hit:focus-visible {
79
+ background: var(--accent, rgb(0 0 0 / 0.05));
80
+ outline: none;
81
+ }
82
+
83
+ .dbx-search-box__hit-default {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 0.125rem;
87
+ }
88
+
89
+ .dbx-search-box__hit-title {
90
+ font-weight: 500;
91
+ overflow: hidden;
92
+ text-overflow: ellipsis;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .dbx-search-box__hit-meta {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 0.5rem;
100
+ font-size: 0.75rem;
101
+ color: var(--muted-foreground, currentColor);
102
+ }
103
+
104
+ .dbx-search-box__empty,
105
+ .dbx-search-box__error {
106
+ padding: 0.75rem;
107
+ font-size: 0.875rem;
108
+ color: var(--muted-foreground, currentColor);
109
+ }
110
+
111
+ .dbx-search-box__error {
112
+ color: var(--destructive, currentColor);
113
+ }
114
+
115
+ .dbx-search-results {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 0.5rem;
119
+ list-style: none;
120
+ margin: 0;
121
+ padding: 0;
122
+ }
123
+
124
+ .dbx-search-results__button {
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 0.25rem;
128
+ width: 100%;
129
+ text-align: left;
130
+ padding: 0.75rem 1rem;
131
+ border-radius: var(--radius, 0.5rem);
132
+ border: 1px solid var(--border, currentColor);
133
+ background: var(--card, transparent);
134
+ color: inherit;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .dbx-search-results__button:hover {
139
+ background: var(--accent, rgb(0 0 0 / 0.05));
140
+ }
141
+
142
+ .dbx-search-results__header {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ gap: 0.75rem;
147
+ }
148
+
149
+ .dbx-search-results__title {
150
+ font-weight: 500;
151
+ }
152
+
153
+ .dbx-search-results__empty {
154
+ padding: 1rem;
155
+ color: var(--muted-foreground, currentColor);
156
+ }