@zentauri-ui/zentauri-components 1.4.41 → 1.4.51
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 +1 -0
- package/cli/registry.json +1 -0
- package/dist/ui/search/filter-search-suggestions.d.ts +15 -0
- package/dist/ui/search/filter-search-suggestions.d.ts.map +1 -0
- package/dist/ui/search/index.d.ts +7 -0
- package/dist/ui/search/index.d.ts.map +1 -0
- package/dist/ui/search/search-bar.d.ts +6 -0
- package/dist/ui/search/search-bar.d.ts.map +1 -0
- package/dist/ui/search/search-suggestion-list.d.ts +6 -0
- package/dist/ui/search/search-suggestion-list.d.ts.map +1 -0
- package/dist/ui/search/search-suggestion-utils.d.ts +6 -0
- package/dist/ui/search/search-suggestion-utils.d.ts.map +1 -0
- package/dist/ui/search/types.d.ts +44 -0
- package/dist/ui/search/types.d.ts.map +1 -0
- package/dist/ui/search.js +195 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/search.mjs +190 -0
- package/dist/ui/search.mjs.map +1 -0
- package/package.json +3 -3
- package/src/ui/search/filter-search-suggestions.test.ts +48 -0
- package/src/ui/search/filter-search-suggestions.ts +43 -0
- package/src/ui/search/index.ts +11 -0
- package/src/ui/search/search-bar.tsx +83 -0
- package/src/ui/search/search-suggestion-list.tsx +95 -0
- package/src/ui/search/search-suggestion-utils.test.ts +9 -0
- package/src/ui/search/search-suggestion-utils.ts +8 -0
- package/src/ui/search/types.ts +52 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Import static primitives from `@zentauri-ui/zentauri-components/ui/<subpath>` wh
|
|
|
58
58
|
| Modal | `modal` | `modal/animated` |
|
|
59
59
|
| Pagination | `pagination` | — |
|
|
60
60
|
| Progress | `progress` | `progress/animated` |
|
|
61
|
+
| Search | `search` | - |
|
|
61
62
|
| Select | `select` | — |
|
|
62
63
|
| Skeleton | `skeleton` | `skeleton/animated` |
|
|
63
64
|
| Slider | `slider` | — |
|
package/cli/registry.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SearchFilterable } from "./types";
|
|
2
|
+
export type FilterSearchSuggestionsOptions = {
|
|
3
|
+
/** Maximum number of matches returned. */
|
|
4
|
+
maxResults?: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Returns items whose label, description, href, or keywords contain the query (case-insensitive).
|
|
8
|
+
* Whitespace-only query matches no items.
|
|
9
|
+
*/
|
|
10
|
+
export declare function filterSearchSuggestions<T extends SearchFilterable>({ query, items, options, }: {
|
|
11
|
+
query: string;
|
|
12
|
+
items: readonly T[];
|
|
13
|
+
options?: FilterSearchSuggestionsOptions;
|
|
14
|
+
}): T[];
|
|
15
|
+
//# sourceMappingURL=filter-search-suggestions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-search-suggestions.d.ts","sourceRoot":"","sources":["../../../src/ui/search/filter-search-suggestions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,MAAM,8BAA8B,GAAG;IAC3C,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAClE,KAAK,EACL,KAAK,EACL,OAA4B,GAC7B,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,8BAA8B,CAAC;CAC1C,GAAG,CAAC,EAAE,CAuBN"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SearchBar } from "./search-bar";
|
|
2
|
+
export { SearchSuggestionList } from "./search-suggestion-list";
|
|
3
|
+
export { searchSuggestionOptionDomId } from "./search-suggestion-utils";
|
|
4
|
+
export { filterSearchSuggestions } from "./filter-search-suggestions";
|
|
5
|
+
export type { SearchBarProps, SearchSuggestionItem, SearchSuggestionListProps, SearchFilterable, } from "./types";
|
|
6
|
+
export type { FilterSearchSuggestionsOptions } from "./filter-search-suggestions";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,yBAAyB,EACzB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SearchBarProps } from "./types";
|
|
2
|
+
export declare const SearchBar: {
|
|
3
|
+
({ value, onValueChange, leadingSlot, className, inputClassName, appearance, inputSize, ring, id, onChange, disabled, type, comboboxListboxId, comboboxActiveOptionId, comboboxExpanded, ref, ...rest }: SearchBarProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=search-bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-bar.d.ts","sourceRoot":"","sources":["../../../src/ui/search/search-bar.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,eAAO,MAAM,SAAS;6MAmBjB,cAAc;;CAoDlB,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SearchSuggestionListProps } from "./types";
|
|
2
|
+
export declare function SearchSuggestionList({ items, onSelect, activeId, onActiveIdChange, listboxId, className, listClassName, emptyLabel, }: SearchSuggestionListProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export declare namespace SearchSuggestionList {
|
|
4
|
+
var displayName: string;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=search-suggestion-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-suggestion-list.d.ts","sourceRoot":"","sources":["../../../src/ui/search/search-suggestion-list.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAKzD,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,aAAa,EACb,UAAU,GACX,EAAE,yBAAyB,2CAsE3B;yBA/Ee,oBAAoB"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a stable DOM id for a listbox option so `aria-activedescendant` on the combobox
|
|
3
|
+
* input can reference it. Safe for href-like `itemId` strings.
|
|
4
|
+
*/
|
|
5
|
+
export declare function searchSuggestionOptionDomId(listboxId: string, itemId: string): string;
|
|
6
|
+
//# sourceMappingURL=search-suggestion-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-suggestion-utils.d.ts","sourceRoot":"","sources":["../../../src/ui/search/search-suggestion-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode, Ref } from "react";
|
|
2
|
+
import type { VariantProps } from "class-variance-authority";
|
|
3
|
+
import type { inputVariants } from "../inputs/variants";
|
|
4
|
+
export type SearchBarProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "children" | "role"> & {
|
|
5
|
+
value: string;
|
|
6
|
+
onValueChange?: (value: string) => void;
|
|
7
|
+
leadingSlot?: ReactNode;
|
|
8
|
+
inputClassName?: string;
|
|
9
|
+
appearance?: VariantProps<typeof inputVariants>["appearance"];
|
|
10
|
+
inputSize?: VariantProps<typeof inputVariants>["size"];
|
|
11
|
+
ring?: VariantProps<typeof inputVariants>["ring"];
|
|
12
|
+
/** When set, the input exposes combobox semantics wired to a `role="listbox"` with this id. */
|
|
13
|
+
comboboxListboxId?: string;
|
|
14
|
+
/** Element id of the active option (from `searchSuggestionOptionDomId`) for `aria-activedescendant`. */
|
|
15
|
+
comboboxActiveOptionId?: string;
|
|
16
|
+
/** Whether the suggestion list is visibly expanded (controls `aria-expanded`). */
|
|
17
|
+
comboboxExpanded?: boolean;
|
|
18
|
+
ref?: Ref<HTMLInputElement>;
|
|
19
|
+
};
|
|
20
|
+
export type SearchSuggestionItem = {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
group?: string;
|
|
25
|
+
};
|
|
26
|
+
export type SearchSuggestionListProps = {
|
|
27
|
+
items: readonly SearchSuggestionItem[];
|
|
28
|
+
onSelect: (id: string) => void;
|
|
29
|
+
activeId?: string;
|
|
30
|
+
onActiveIdChange?: (id: string | undefined) => void;
|
|
31
|
+
/** Pass the same id as `comboboxListboxId` on `SearchBar` for ARIA wiring. */
|
|
32
|
+
listboxId?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
listClassName?: string;
|
|
35
|
+
emptyLabel?: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
export type SearchFilterable = {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
keywords?: readonly string[];
|
|
42
|
+
href?: string;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ui/search/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,MAAM,GAAG,UAAU,GAAG,MAAM,CAC7B,GAAG;IACF,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC,YAAY,CAAC,CAAC;IAC9D,SAAS,CAAC,EAAE,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;IAClD,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wGAAwG;IACxG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,GAAG,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACpD,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var chunkQZKMFSH5_js = require('../chunk-QZKMFSH5.js');
|
|
5
|
+
var chunkUOZYPWDZ_js = require('../chunk-UOZYPWDZ.js');
|
|
6
|
+
var react = require('react');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
var SearchBar = function SearchBar2({
|
|
10
|
+
value,
|
|
11
|
+
onValueChange,
|
|
12
|
+
leadingSlot,
|
|
13
|
+
className,
|
|
14
|
+
inputClassName,
|
|
15
|
+
appearance = "default",
|
|
16
|
+
inputSize = "md",
|
|
17
|
+
ring = true,
|
|
18
|
+
id,
|
|
19
|
+
onChange,
|
|
20
|
+
disabled,
|
|
21
|
+
type,
|
|
22
|
+
comboboxListboxId,
|
|
23
|
+
comboboxActiveOptionId,
|
|
24
|
+
comboboxExpanded,
|
|
25
|
+
ref,
|
|
26
|
+
...rest
|
|
27
|
+
}) {
|
|
28
|
+
const generatedId = react.useId();
|
|
29
|
+
const controlId = id ?? generatedId;
|
|
30
|
+
const combobox = Boolean(comboboxListboxId);
|
|
31
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
"data-slot": "search-bar",
|
|
35
|
+
className: chunkUOZYPWDZ_js.cn("relative flex w-full min-w-0 items-center", className),
|
|
36
|
+
children: [
|
|
37
|
+
leadingSlot ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
38
|
+
"span",
|
|
39
|
+
{
|
|
40
|
+
className: "pointer-events-none absolute left-3 top-1/2 z-1 flex -translate-y-1/2 text-slate-400 [&_svg]:size-4",
|
|
41
|
+
"aria-hidden": true,
|
|
42
|
+
children: leadingSlot
|
|
43
|
+
}
|
|
44
|
+
) : null,
|
|
45
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
46
|
+
"input",
|
|
47
|
+
{
|
|
48
|
+
ref,
|
|
49
|
+
id: controlId,
|
|
50
|
+
type: type ?? "search",
|
|
51
|
+
autoComplete: "off",
|
|
52
|
+
spellCheck: false,
|
|
53
|
+
disabled,
|
|
54
|
+
value,
|
|
55
|
+
"data-slot": "search-bar-input",
|
|
56
|
+
className: chunkUOZYPWDZ_js.cn(
|
|
57
|
+
chunkQZKMFSH5_js.inputVariants({ appearance, size: inputSize, ring, as: "input" }),
|
|
58
|
+
leadingSlot ? "pl-10" : null,
|
|
59
|
+
inputClassName
|
|
60
|
+
),
|
|
61
|
+
onChange: (event) => {
|
|
62
|
+
onChange?.(event);
|
|
63
|
+
onValueChange?.(event.target.value);
|
|
64
|
+
},
|
|
65
|
+
...combobox ? {
|
|
66
|
+
role: "combobox",
|
|
67
|
+
"aria-autocomplete": "list",
|
|
68
|
+
"aria-controls": comboboxListboxId,
|
|
69
|
+
"aria-expanded": comboboxExpanded ?? false,
|
|
70
|
+
...comboboxActiveOptionId ? { "aria-activedescendant": comboboxActiveOptionId } : {}
|
|
71
|
+
} : {},
|
|
72
|
+
...rest
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
SearchBar.displayName = "SearchBar";
|
|
80
|
+
|
|
81
|
+
// src/ui/search/search-suggestion-utils.ts
|
|
82
|
+
function searchSuggestionOptionDomId(listboxId, itemId) {
|
|
83
|
+
const safe = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
84
|
+
return `${listboxId}_opt_${safe}`;
|
|
85
|
+
}
|
|
86
|
+
var rowClassName = "flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50";
|
|
87
|
+
function SearchSuggestionList({
|
|
88
|
+
items,
|
|
89
|
+
onSelect,
|
|
90
|
+
activeId,
|
|
91
|
+
onActiveIdChange,
|
|
92
|
+
listboxId,
|
|
93
|
+
className,
|
|
94
|
+
listClassName,
|
|
95
|
+
emptyLabel
|
|
96
|
+
}) {
|
|
97
|
+
if (items.length === 0) {
|
|
98
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
99
|
+
"div",
|
|
100
|
+
{
|
|
101
|
+
"data-slot": "search-suggestion-list-empty",
|
|
102
|
+
className: chunkUOZYPWDZ_js.cn("px-1 py-6 text-center text-sm text-slate-500", className),
|
|
103
|
+
children: emptyLabel ?? "No matches."
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
let lastGroup;
|
|
108
|
+
const useListbox = Boolean(listboxId);
|
|
109
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
110
|
+
"nav",
|
|
111
|
+
{
|
|
112
|
+
"data-slot": "search-suggestion-list",
|
|
113
|
+
"aria-label": "Search results",
|
|
114
|
+
className: chunkUOZYPWDZ_js.cn("flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1", className),
|
|
115
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
...useListbox ? {
|
|
119
|
+
id: listboxId,
|
|
120
|
+
role: "listbox"
|
|
121
|
+
} : {},
|
|
122
|
+
className: chunkUOZYPWDZ_js.cn("flex flex-col gap-0.5", listClassName),
|
|
123
|
+
children: items.map((item) => {
|
|
124
|
+
const showGroup = item.group && item.group !== lastGroup;
|
|
125
|
+
if (item.group) {
|
|
126
|
+
lastGroup = item.group;
|
|
127
|
+
}
|
|
128
|
+
const isActive = activeId === item.id;
|
|
129
|
+
const optionDomId = useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : void 0;
|
|
130
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
131
|
+
showGroup ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
132
|
+
"div",
|
|
133
|
+
{
|
|
134
|
+
role: "presentation",
|
|
135
|
+
className: "sticky top-0 z-1 bg-slate-950/95 px-2 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 backdrop-blur-sm",
|
|
136
|
+
children: item.group
|
|
137
|
+
}
|
|
138
|
+
) : null,
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
140
|
+
"button",
|
|
141
|
+
{
|
|
142
|
+
type: "button",
|
|
143
|
+
id: optionDomId,
|
|
144
|
+
role: useListbox ? "option" : void 0,
|
|
145
|
+
"aria-selected": useListbox ? isActive : void 0,
|
|
146
|
+
"data-active": isActive ? "" : void 0,
|
|
147
|
+
className: chunkUOZYPWDZ_js.cn(rowClassName, isActive ? "bg-white/5" : null),
|
|
148
|
+
onMouseEnter: () => onActiveIdChange?.(item.id),
|
|
149
|
+
onFocus: () => onActiveIdChange?.(item.id),
|
|
150
|
+
onClick: () => onSelect(item.id),
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-slate-100", children: item.label }),
|
|
153
|
+
item.description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-slate-500", children: item.description }) : null
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
] }, item.id);
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
SearchSuggestionList.displayName = "SearchSuggestionList";
|
|
165
|
+
|
|
166
|
+
// src/ui/search/filter-search-suggestions.ts
|
|
167
|
+
function filterSearchSuggestions({
|
|
168
|
+
query,
|
|
169
|
+
items,
|
|
170
|
+
options = { maxResults: 20 }
|
|
171
|
+
}) {
|
|
172
|
+
const maxResults = options.maxResults ?? 20;
|
|
173
|
+
const normalized = query.trim().toLowerCase();
|
|
174
|
+
if (!normalized) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const matches = [];
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
const isMatch = item.label.toLowerCase().includes(normalized) || item.description?.toLowerCase().includes(normalized) || item.href?.toLowerCase().includes(normalized) || item.keywords?.some((k) => k.toLowerCase().includes(normalized));
|
|
180
|
+
if (isMatch) {
|
|
181
|
+
matches.push(item);
|
|
182
|
+
if (matches.length >= maxResults || maxResults <= 0) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return matches;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
exports.SearchBar = SearchBar;
|
|
191
|
+
exports.SearchSuggestionList = SearchSuggestionList;
|
|
192
|
+
exports.filterSearchSuggestions = filterSearchSuggestions;
|
|
193
|
+
exports.searchSuggestionOptionDomId = searchSuggestionOptionDomId;
|
|
194
|
+
//# sourceMappingURL=search.js.map
|
|
195
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/search/search-bar.tsx","../../src/ui/search/search-suggestion-utils.ts","../../src/ui/search/search-suggestion-list.tsx","../../src/ui/search/filter-search-suggestions.ts"],"names":["SearchBar","useId","jsxs","cn","jsx","inputVariants","Fragment"],"mappings":"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","file":"search.js","sourcesContent":["\"use client\";\n\nimport { forwardRef, useId } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\nimport { inputVariants } from \"../inputs/variants\";\n\nimport type { SearchBarProps } from \"./types\";\n\nexport const SearchBar = function SearchBar(\n {\n value,\n onValueChange,\n leadingSlot,\n className,\n inputClassName,\n appearance = \"default\",\n inputSize = \"md\",\n ring = true,\n id,\n onChange,\n disabled,\n type,\n comboboxListboxId,\n comboboxActiveOptionId,\n comboboxExpanded,\n ref,\n ...rest\n }: SearchBarProps,\n) {\n const generatedId = useId();\n const controlId = id ?? generatedId;\n const combobox = Boolean(comboboxListboxId);\n\n return (\n <div\n data-slot=\"search-bar\"\n className={cn(\"relative flex w-full min-w-0 items-center\", className)}\n >\n {leadingSlot ? (\n <span\n className=\"pointer-events-none absolute left-3 top-1/2 z-1 flex -translate-y-1/2 text-slate-400 [&_svg]:size-4\"\n aria-hidden\n >\n {leadingSlot}\n </span>\n ) : null}\n <input\n ref={ref}\n id={controlId}\n type={type ?? \"search\"}\n autoComplete=\"off\"\n spellCheck={false}\n disabled={disabled}\n value={value}\n data-slot=\"search-bar-input\"\n className={cn(\n inputVariants({ appearance, size: inputSize, ring, as: \"input\" }),\n leadingSlot ? \"pl-10\" : null,\n inputClassName,\n )}\n onChange={(event) => {\n onChange?.(event);\n onValueChange?.(event.target.value);\n }}\n {...(combobox\n ? {\n role: \"combobox\" as const,\n \"aria-autocomplete\": \"list\" as const,\n \"aria-controls\": comboboxListboxId,\n \"aria-expanded\": comboboxExpanded ?? false,\n ...(comboboxActiveOptionId\n ? { \"aria-activedescendant\": comboboxActiveOptionId }\n : {}),\n }\n : {})}\n {...rest}\n />\n </div>\n );\n}\n\nSearchBar.displayName = \"SearchBar\";\n","/**\n * Builds a stable DOM id for a listbox option so `aria-activedescendant` on the combobox\n * input can reference it. Safe for href-like `itemId` strings.\n */\nexport function searchSuggestionOptionDomId(listboxId: string, itemId: string): string {\n const safe = itemId.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return `${listboxId}_opt_${safe}`;\n}\n","\"use client\";\n\nimport { Fragment } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\nimport { searchSuggestionOptionDomId } from \"./search-suggestion-utils\";\n\nimport type { SearchSuggestionListProps } from \"./types\";\n\nconst rowClassName =\n \"flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50\";\n\nexport function SearchSuggestionList({\n items,\n onSelect,\n activeId,\n onActiveIdChange,\n listboxId,\n className,\n listClassName,\n emptyLabel,\n}: SearchSuggestionListProps) {\n if (items.length === 0) {\n return (\n <div\n data-slot=\"search-suggestion-list-empty\"\n className={cn(\"px-1 py-6 text-center text-sm text-slate-500\", className)}\n >\n {emptyLabel ?? \"No matches.\"}\n </div>\n );\n }\n\n let lastGroup: string | undefined;\n const useListbox = Boolean(listboxId);\n\n return (\n <nav\n data-slot=\"search-suggestion-list\"\n aria-label=\"Search results\"\n className={cn(\"flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1\", className)}\n >\n <div\n {...(useListbox\n ? {\n id: listboxId,\n role: \"listbox\" as const,\n }\n : {})}\n className={cn(\"flex flex-col gap-0.5\", listClassName)}\n >\n {items.map((item) => {\n const showGroup = item.group && item.group !== lastGroup;\n if (item.group) {\n lastGroup = item.group;\n }\n const isActive = activeId === item.id;\n const optionDomId =\n useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : undefined;\n return (\n <Fragment key={item.id}>\n {showGroup ? (\n <div\n role=\"presentation\"\n className=\"sticky top-0 z-1 bg-slate-950/95 px-2 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 backdrop-blur-sm\"\n >\n {item.group}\n </div>\n ) : null}\n <button\n type=\"button\"\n id={optionDomId}\n role={useListbox ? \"option\" : undefined}\n aria-selected={useListbox ? isActive : undefined}\n data-active={isActive ? \"\" : undefined}\n className={cn(rowClassName, isActive ? \"bg-white/5\" : null)}\n onMouseEnter={() => onActiveIdChange?.(item.id)}\n onFocus={() => onActiveIdChange?.(item.id)}\n onClick={() => onSelect(item.id)}\n >\n <span className=\"font-medium text-slate-100\">{item.label}</span>\n {item.description ? (\n <span className=\"truncate text-xs text-slate-500\">{item.description}</span>\n ) : null}\n </button>\n </Fragment>\n );\n })}\n </div>\n </nav>\n );\n}\n\nSearchSuggestionList.displayName = \"SearchSuggestionList\";\n\n","import type { SearchFilterable } from \"./types\";\n\nexport type FilterSearchSuggestionsOptions = {\n /** Maximum number of matches returned. */\n maxResults?: number;\n};\n\n/**\n * Returns items whose label, description, href, or keywords contain the query (case-insensitive).\n * Whitespace-only query matches no items.\n */\nexport function filterSearchSuggestions<T extends SearchFilterable>({\n query,\n items,\n options = { maxResults: 20 },\n}: {\n query: string;\n items: readonly T[];\n options?: FilterSearchSuggestionsOptions;\n}): T[] {\n const maxResults = options.maxResults ?? 20;\n const normalized = query.trim().toLowerCase();\n if (!normalized) {\n return [];\n }\n\n const matches: T[] = [];\n for (const item of items) {\n const isMatch =\n item.label.toLowerCase().includes(normalized) ||\n (item.description?.toLowerCase().includes(normalized)) ||\n (item.href?.toLowerCase().includes(normalized)) ||\n (item.keywords?.some((k) => k.toLowerCase().includes(normalized)));\n\n if (isMatch) {\n matches.push(item);\n if (matches.length >= maxResults || maxResults <= 0) {\n break;\n }\n }\n }\n return matches;\n}\n"]}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { inputVariants } from '../chunk-AOEI4V3W.mjs';
|
|
3
|
+
import { cn } from '../chunk-DFEZH7TC.mjs';
|
|
4
|
+
import { useId, Fragment } from 'react';
|
|
5
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var SearchBar = function SearchBar2({
|
|
8
|
+
value,
|
|
9
|
+
onValueChange,
|
|
10
|
+
leadingSlot,
|
|
11
|
+
className,
|
|
12
|
+
inputClassName,
|
|
13
|
+
appearance = "default",
|
|
14
|
+
inputSize = "md",
|
|
15
|
+
ring = true,
|
|
16
|
+
id,
|
|
17
|
+
onChange,
|
|
18
|
+
disabled,
|
|
19
|
+
type,
|
|
20
|
+
comboboxListboxId,
|
|
21
|
+
comboboxActiveOptionId,
|
|
22
|
+
comboboxExpanded,
|
|
23
|
+
ref,
|
|
24
|
+
...rest
|
|
25
|
+
}) {
|
|
26
|
+
const generatedId = useId();
|
|
27
|
+
const controlId = id ?? generatedId;
|
|
28
|
+
const combobox = Boolean(comboboxListboxId);
|
|
29
|
+
return /* @__PURE__ */ jsxs(
|
|
30
|
+
"div",
|
|
31
|
+
{
|
|
32
|
+
"data-slot": "search-bar",
|
|
33
|
+
className: cn("relative flex w-full min-w-0 items-center", className),
|
|
34
|
+
children: [
|
|
35
|
+
leadingSlot ? /* @__PURE__ */ jsx(
|
|
36
|
+
"span",
|
|
37
|
+
{
|
|
38
|
+
className: "pointer-events-none absolute left-3 top-1/2 z-1 flex -translate-y-1/2 text-slate-400 [&_svg]:size-4",
|
|
39
|
+
"aria-hidden": true,
|
|
40
|
+
children: leadingSlot
|
|
41
|
+
}
|
|
42
|
+
) : null,
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"input",
|
|
45
|
+
{
|
|
46
|
+
ref,
|
|
47
|
+
id: controlId,
|
|
48
|
+
type: type ?? "search",
|
|
49
|
+
autoComplete: "off",
|
|
50
|
+
spellCheck: false,
|
|
51
|
+
disabled,
|
|
52
|
+
value,
|
|
53
|
+
"data-slot": "search-bar-input",
|
|
54
|
+
className: cn(
|
|
55
|
+
inputVariants({ appearance, size: inputSize, ring, as: "input" }),
|
|
56
|
+
leadingSlot ? "pl-10" : null,
|
|
57
|
+
inputClassName
|
|
58
|
+
),
|
|
59
|
+
onChange: (event) => {
|
|
60
|
+
onChange?.(event);
|
|
61
|
+
onValueChange?.(event.target.value);
|
|
62
|
+
},
|
|
63
|
+
...combobox ? {
|
|
64
|
+
role: "combobox",
|
|
65
|
+
"aria-autocomplete": "list",
|
|
66
|
+
"aria-controls": comboboxListboxId,
|
|
67
|
+
"aria-expanded": comboboxExpanded ?? false,
|
|
68
|
+
...comboboxActiveOptionId ? { "aria-activedescendant": comboboxActiveOptionId } : {}
|
|
69
|
+
} : {},
|
|
70
|
+
...rest
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
SearchBar.displayName = "SearchBar";
|
|
78
|
+
|
|
79
|
+
// src/ui/search/search-suggestion-utils.ts
|
|
80
|
+
function searchSuggestionOptionDomId(listboxId, itemId) {
|
|
81
|
+
const safe = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
82
|
+
return `${listboxId}_opt_${safe}`;
|
|
83
|
+
}
|
|
84
|
+
var rowClassName = "flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50";
|
|
85
|
+
function SearchSuggestionList({
|
|
86
|
+
items,
|
|
87
|
+
onSelect,
|
|
88
|
+
activeId,
|
|
89
|
+
onActiveIdChange,
|
|
90
|
+
listboxId,
|
|
91
|
+
className,
|
|
92
|
+
listClassName,
|
|
93
|
+
emptyLabel
|
|
94
|
+
}) {
|
|
95
|
+
if (items.length === 0) {
|
|
96
|
+
return /* @__PURE__ */ jsx(
|
|
97
|
+
"div",
|
|
98
|
+
{
|
|
99
|
+
"data-slot": "search-suggestion-list-empty",
|
|
100
|
+
className: cn("px-1 py-6 text-center text-sm text-slate-500", className),
|
|
101
|
+
children: emptyLabel ?? "No matches."
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let lastGroup;
|
|
106
|
+
const useListbox = Boolean(listboxId);
|
|
107
|
+
return /* @__PURE__ */ jsx(
|
|
108
|
+
"nav",
|
|
109
|
+
{
|
|
110
|
+
"data-slot": "search-suggestion-list",
|
|
111
|
+
"aria-label": "Search results",
|
|
112
|
+
className: cn("flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1", className),
|
|
113
|
+
children: /* @__PURE__ */ jsx(
|
|
114
|
+
"div",
|
|
115
|
+
{
|
|
116
|
+
...useListbox ? {
|
|
117
|
+
id: listboxId,
|
|
118
|
+
role: "listbox"
|
|
119
|
+
} : {},
|
|
120
|
+
className: cn("flex flex-col gap-0.5", listClassName),
|
|
121
|
+
children: items.map((item) => {
|
|
122
|
+
const showGroup = item.group && item.group !== lastGroup;
|
|
123
|
+
if (item.group) {
|
|
124
|
+
lastGroup = item.group;
|
|
125
|
+
}
|
|
126
|
+
const isActive = activeId === item.id;
|
|
127
|
+
const optionDomId = useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : void 0;
|
|
128
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
129
|
+
showGroup ? /* @__PURE__ */ jsx(
|
|
130
|
+
"div",
|
|
131
|
+
{
|
|
132
|
+
role: "presentation",
|
|
133
|
+
className: "sticky top-0 z-1 bg-slate-950/95 px-2 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 backdrop-blur-sm",
|
|
134
|
+
children: item.group
|
|
135
|
+
}
|
|
136
|
+
) : null,
|
|
137
|
+
/* @__PURE__ */ jsxs(
|
|
138
|
+
"button",
|
|
139
|
+
{
|
|
140
|
+
type: "button",
|
|
141
|
+
id: optionDomId,
|
|
142
|
+
role: useListbox ? "option" : void 0,
|
|
143
|
+
"aria-selected": useListbox ? isActive : void 0,
|
|
144
|
+
"data-active": isActive ? "" : void 0,
|
|
145
|
+
className: cn(rowClassName, isActive ? "bg-white/5" : null),
|
|
146
|
+
onMouseEnter: () => onActiveIdChange?.(item.id),
|
|
147
|
+
onFocus: () => onActiveIdChange?.(item.id),
|
|
148
|
+
onClick: () => onSelect(item.id),
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-slate-100", children: item.label }),
|
|
151
|
+
item.description ? /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-slate-500", children: item.description }) : null
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
] }, item.id);
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
SearchSuggestionList.displayName = "SearchSuggestionList";
|
|
163
|
+
|
|
164
|
+
// src/ui/search/filter-search-suggestions.ts
|
|
165
|
+
function filterSearchSuggestions({
|
|
166
|
+
query,
|
|
167
|
+
items,
|
|
168
|
+
options = { maxResults: 20 }
|
|
169
|
+
}) {
|
|
170
|
+
const maxResults = options.maxResults ?? 20;
|
|
171
|
+
const normalized = query.trim().toLowerCase();
|
|
172
|
+
if (!normalized) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
const matches = [];
|
|
176
|
+
for (const item of items) {
|
|
177
|
+
const isMatch = item.label.toLowerCase().includes(normalized) || item.description?.toLowerCase().includes(normalized) || item.href?.toLowerCase().includes(normalized) || item.keywords?.some((k) => k.toLowerCase().includes(normalized));
|
|
178
|
+
if (isMatch) {
|
|
179
|
+
matches.push(item);
|
|
180
|
+
if (matches.length >= maxResults || maxResults <= 0) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return matches;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { SearchBar, SearchSuggestionList, filterSearchSuggestions, searchSuggestionOptionDomId };
|
|
189
|
+
//# sourceMappingURL=search.mjs.map
|
|
190
|
+
//# sourceMappingURL=search.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ui/search/search-bar.tsx","../../src/ui/search/search-suggestion-utils.ts","../../src/ui/search/search-suggestion-list.tsx","../../src/ui/search/filter-search-suggestions.ts"],"names":["SearchBar","jsx","jsxs"],"mappings":"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","file":"search.mjs","sourcesContent":["\"use client\";\n\nimport { forwardRef, useId } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\nimport { inputVariants } from \"../inputs/variants\";\n\nimport type { SearchBarProps } from \"./types\";\n\nexport const SearchBar = function SearchBar(\n {\n value,\n onValueChange,\n leadingSlot,\n className,\n inputClassName,\n appearance = \"default\",\n inputSize = \"md\",\n ring = true,\n id,\n onChange,\n disabled,\n type,\n comboboxListboxId,\n comboboxActiveOptionId,\n comboboxExpanded,\n ref,\n ...rest\n }: SearchBarProps,\n) {\n const generatedId = useId();\n const controlId = id ?? generatedId;\n const combobox = Boolean(comboboxListboxId);\n\n return (\n <div\n data-slot=\"search-bar\"\n className={cn(\"relative flex w-full min-w-0 items-center\", className)}\n >\n {leadingSlot ? (\n <span\n className=\"pointer-events-none absolute left-3 top-1/2 z-1 flex -translate-y-1/2 text-slate-400 [&_svg]:size-4\"\n aria-hidden\n >\n {leadingSlot}\n </span>\n ) : null}\n <input\n ref={ref}\n id={controlId}\n type={type ?? \"search\"}\n autoComplete=\"off\"\n spellCheck={false}\n disabled={disabled}\n value={value}\n data-slot=\"search-bar-input\"\n className={cn(\n inputVariants({ appearance, size: inputSize, ring, as: \"input\" }),\n leadingSlot ? \"pl-10\" : null,\n inputClassName,\n )}\n onChange={(event) => {\n onChange?.(event);\n onValueChange?.(event.target.value);\n }}\n {...(combobox\n ? {\n role: \"combobox\" as const,\n \"aria-autocomplete\": \"list\" as const,\n \"aria-controls\": comboboxListboxId,\n \"aria-expanded\": comboboxExpanded ?? false,\n ...(comboboxActiveOptionId\n ? { \"aria-activedescendant\": comboboxActiveOptionId }\n : {}),\n }\n : {})}\n {...rest}\n />\n </div>\n );\n}\n\nSearchBar.displayName = \"SearchBar\";\n","/**\n * Builds a stable DOM id for a listbox option so `aria-activedescendant` on the combobox\n * input can reference it. Safe for href-like `itemId` strings.\n */\nexport function searchSuggestionOptionDomId(listboxId: string, itemId: string): string {\n const safe = itemId.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return `${listboxId}_opt_${safe}`;\n}\n","\"use client\";\n\nimport { Fragment } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\nimport { searchSuggestionOptionDomId } from \"./search-suggestion-utils\";\n\nimport type { SearchSuggestionListProps } from \"./types\";\n\nconst rowClassName =\n \"flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50\";\n\nexport function SearchSuggestionList({\n items,\n onSelect,\n activeId,\n onActiveIdChange,\n listboxId,\n className,\n listClassName,\n emptyLabel,\n}: SearchSuggestionListProps) {\n if (items.length === 0) {\n return (\n <div\n data-slot=\"search-suggestion-list-empty\"\n className={cn(\"px-1 py-6 text-center text-sm text-slate-500\", className)}\n >\n {emptyLabel ?? \"No matches.\"}\n </div>\n );\n }\n\n let lastGroup: string | undefined;\n const useListbox = Boolean(listboxId);\n\n return (\n <nav\n data-slot=\"search-suggestion-list\"\n aria-label=\"Search results\"\n className={cn(\"flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1\", className)}\n >\n <div\n {...(useListbox\n ? {\n id: listboxId,\n role: \"listbox\" as const,\n }\n : {})}\n className={cn(\"flex flex-col gap-0.5\", listClassName)}\n >\n {items.map((item) => {\n const showGroup = item.group && item.group !== lastGroup;\n if (item.group) {\n lastGroup = item.group;\n }\n const isActive = activeId === item.id;\n const optionDomId =\n useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : undefined;\n return (\n <Fragment key={item.id}>\n {showGroup ? (\n <div\n role=\"presentation\"\n className=\"sticky top-0 z-1 bg-slate-950/95 px-2 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 backdrop-blur-sm\"\n >\n {item.group}\n </div>\n ) : null}\n <button\n type=\"button\"\n id={optionDomId}\n role={useListbox ? \"option\" : undefined}\n aria-selected={useListbox ? isActive : undefined}\n data-active={isActive ? \"\" : undefined}\n className={cn(rowClassName, isActive ? \"bg-white/5\" : null)}\n onMouseEnter={() => onActiveIdChange?.(item.id)}\n onFocus={() => onActiveIdChange?.(item.id)}\n onClick={() => onSelect(item.id)}\n >\n <span className=\"font-medium text-slate-100\">{item.label}</span>\n {item.description ? (\n <span className=\"truncate text-xs text-slate-500\">{item.description}</span>\n ) : null}\n </button>\n </Fragment>\n );\n })}\n </div>\n </nav>\n );\n}\n\nSearchSuggestionList.displayName = \"SearchSuggestionList\";\n\n","import type { SearchFilterable } from \"./types\";\n\nexport type FilterSearchSuggestionsOptions = {\n /** Maximum number of matches returned. */\n maxResults?: number;\n};\n\n/**\n * Returns items whose label, description, href, or keywords contain the query (case-insensitive).\n * Whitespace-only query matches no items.\n */\nexport function filterSearchSuggestions<T extends SearchFilterable>({\n query,\n items,\n options = { maxResults: 20 },\n}: {\n query: string;\n items: readonly T[];\n options?: FilterSearchSuggestionsOptions;\n}): T[] {\n const maxResults = options.maxResults ?? 20;\n const normalized = query.trim().toLowerCase();\n if (!normalized) {\n return [];\n }\n\n const matches: T[] = [];\n for (const item of items) {\n const isMatch =\n item.label.toLowerCase().includes(normalized) ||\n (item.description?.toLowerCase().includes(normalized)) ||\n (item.href?.toLowerCase().includes(normalized)) ||\n (item.keywords?.some((k) => k.toLowerCase().includes(normalized)));\n\n if (isMatch) {\n matches.push(item);\n if (matches.length >= maxResults || maxResults <= 0) {\n break;\n }\n }\n }\n return matches;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zentauri-ui/zentauri-components",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.51",
|
|
4
4
|
"description": "React + Tailwind UI kit with ESM/CJS builds, per-entry exports, and a zentauri-components / zentauri-ui CLI to vendor UI or hook source into your app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": ["dist", "src/ui", "src/lib", "src/hooks", "cli"],
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"test:watch": "vitest"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"react": "
|
|
49
|
-
"react-dom": "
|
|
48
|
+
"react": ">18",
|
|
49
|
+
"react-dom": ">18",
|
|
50
50
|
"class-variance-authority": "*",
|
|
51
51
|
"clsx": "*",
|
|
52
52
|
"tailwind-merge": "*",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { filterSearchSuggestions } from "./filter-search-suggestions";
|
|
4
|
+
import type { SearchFilterable } from "./types";
|
|
5
|
+
|
|
6
|
+
const sampleItems: SearchFilterable[] = [
|
|
7
|
+
{ id: "1", label: "Installation", href: "/preview/installation", keywords: ["npm", "setup"] },
|
|
8
|
+
{ id: "2", label: "Modal", href: "/preview/components/modal" },
|
|
9
|
+
{ id: "3", label: "GitHub", href: "https://example.com/repo", keywords: ["github", "source"] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe("filterSearchSuggestions", () => {
|
|
13
|
+
it("returns empty list for whitespace-only query", () => {
|
|
14
|
+
expect(filterSearchSuggestions({ query: " ", items: sampleItems })).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns empty list for empty query", () => {
|
|
18
|
+
expect(filterSearchSuggestions({ query: "", items: sampleItems })).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("matches label case-insensitively", () => {
|
|
22
|
+
expect(filterSearchSuggestions({ query: "modal", items: sampleItems })).toEqual([
|
|
23
|
+
{ id: "2", label: "Modal", href: "/preview/components/modal" },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("matches href substring", () => {
|
|
28
|
+
expect(filterSearchSuggestions({ query: "installation", items: sampleItems })).toEqual([
|
|
29
|
+
{ id: "1", label: "Installation", href: "/preview/installation", keywords: ["npm", "setup"] },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("matches keywords", () => {
|
|
34
|
+
expect(filterSearchSuggestions({ query: "github", items: sampleItems })).toEqual([
|
|
35
|
+
{ id: "3", label: "GitHub", href: "https://example.com/repo", keywords: ["github", "source"] },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("respects maxResults", () => {
|
|
40
|
+
const many: SearchFilterable[] = Array.from({ length: 30 }, (_, index) => ({
|
|
41
|
+
id: `x-${index}`,
|
|
42
|
+
label: `Item ${index}`,
|
|
43
|
+
href: `/x/${index}`,
|
|
44
|
+
keywords: ["alpha"],
|
|
45
|
+
}));
|
|
46
|
+
expect(filterSearchSuggestions({ query: "alpha", items: many, options: { maxResults: 5 } })).toHaveLength(5);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SearchFilterable } from "./types";
|
|
2
|
+
|
|
3
|
+
export type FilterSearchSuggestionsOptions = {
|
|
4
|
+
/** Maximum number of matches returned. */
|
|
5
|
+
maxResults?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns items whose label, description, href, or keywords contain the query (case-insensitive).
|
|
10
|
+
* Whitespace-only query matches no items.
|
|
11
|
+
*/
|
|
12
|
+
export function filterSearchSuggestions<T extends SearchFilterable>({
|
|
13
|
+
query,
|
|
14
|
+
items,
|
|
15
|
+
options = { maxResults: 20 },
|
|
16
|
+
}: {
|
|
17
|
+
query: string;
|
|
18
|
+
items: readonly T[];
|
|
19
|
+
options?: FilterSearchSuggestionsOptions;
|
|
20
|
+
}): T[] {
|
|
21
|
+
const maxResults = options.maxResults ?? 20;
|
|
22
|
+
const normalized = query.trim().toLowerCase();
|
|
23
|
+
if (!normalized) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const matches: T[] = [];
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
const isMatch =
|
|
30
|
+
item.label.toLowerCase().includes(normalized) ||
|
|
31
|
+
(item.description?.toLowerCase().includes(normalized)) ||
|
|
32
|
+
(item.href?.toLowerCase().includes(normalized)) ||
|
|
33
|
+
(item.keywords?.some((k) => k.toLowerCase().includes(normalized)));
|
|
34
|
+
|
|
35
|
+
if (isMatch) {
|
|
36
|
+
matches.push(item);
|
|
37
|
+
if (matches.length >= maxResults || maxResults <= 0) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { SearchBar } from "./search-bar";
|
|
2
|
+
export { SearchSuggestionList } from "./search-suggestion-list";
|
|
3
|
+
export { searchSuggestionOptionDomId } from "./search-suggestion-utils";
|
|
4
|
+
export { filterSearchSuggestions } from "./filter-search-suggestions";
|
|
5
|
+
export type {
|
|
6
|
+
SearchBarProps,
|
|
7
|
+
SearchSuggestionItem,
|
|
8
|
+
SearchSuggestionListProps,
|
|
9
|
+
SearchFilterable,
|
|
10
|
+
} from "./types";
|
|
11
|
+
export type { FilterSearchSuggestionsOptions } from "./filter-search-suggestions";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { inputVariants } from "../inputs/variants";
|
|
7
|
+
|
|
8
|
+
import type { SearchBarProps } from "./types";
|
|
9
|
+
|
|
10
|
+
export const SearchBar = function SearchBar(
|
|
11
|
+
{
|
|
12
|
+
value,
|
|
13
|
+
onValueChange,
|
|
14
|
+
leadingSlot,
|
|
15
|
+
className,
|
|
16
|
+
inputClassName,
|
|
17
|
+
appearance = "default",
|
|
18
|
+
inputSize = "md",
|
|
19
|
+
ring = true,
|
|
20
|
+
id,
|
|
21
|
+
onChange,
|
|
22
|
+
disabled,
|
|
23
|
+
type,
|
|
24
|
+
comboboxListboxId,
|
|
25
|
+
comboboxActiveOptionId,
|
|
26
|
+
comboboxExpanded,
|
|
27
|
+
ref,
|
|
28
|
+
...rest
|
|
29
|
+
}: SearchBarProps,
|
|
30
|
+
) {
|
|
31
|
+
const generatedId = useId();
|
|
32
|
+
const controlId = id ?? generatedId;
|
|
33
|
+
const combobox = Boolean(comboboxListboxId);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="search-bar"
|
|
38
|
+
className={cn("relative flex w-full min-w-0 items-center", className)}
|
|
39
|
+
>
|
|
40
|
+
{leadingSlot ? (
|
|
41
|
+
<span
|
|
42
|
+
className="pointer-events-none absolute left-3 top-1/2 z-1 flex -translate-y-1/2 text-slate-400 [&_svg]:size-4"
|
|
43
|
+
aria-hidden
|
|
44
|
+
>
|
|
45
|
+
{leadingSlot}
|
|
46
|
+
</span>
|
|
47
|
+
) : null}
|
|
48
|
+
<input
|
|
49
|
+
ref={ref}
|
|
50
|
+
id={controlId}
|
|
51
|
+
type={type ?? "search"}
|
|
52
|
+
autoComplete="off"
|
|
53
|
+
spellCheck={false}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
value={value}
|
|
56
|
+
data-slot="search-bar-input"
|
|
57
|
+
className={cn(
|
|
58
|
+
inputVariants({ appearance, size: inputSize, ring, as: "input" }),
|
|
59
|
+
leadingSlot ? "pl-10" : null,
|
|
60
|
+
inputClassName,
|
|
61
|
+
)}
|
|
62
|
+
onChange={(event) => {
|
|
63
|
+
onChange?.(event);
|
|
64
|
+
onValueChange?.(event.target.value);
|
|
65
|
+
}}
|
|
66
|
+
{...(combobox
|
|
67
|
+
? {
|
|
68
|
+
role: "combobox" as const,
|
|
69
|
+
"aria-autocomplete": "list" as const,
|
|
70
|
+
"aria-controls": comboboxListboxId,
|
|
71
|
+
"aria-expanded": comboboxExpanded ?? false,
|
|
72
|
+
...(comboboxActiveOptionId
|
|
73
|
+
? { "aria-activedescendant": comboboxActiveOptionId }
|
|
74
|
+
: {}),
|
|
75
|
+
}
|
|
76
|
+
: {})}
|
|
77
|
+
{...rest}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
SearchBar.displayName = "SearchBar";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { searchSuggestionOptionDomId } from "./search-suggestion-utils";
|
|
7
|
+
|
|
8
|
+
import type { SearchSuggestionListProps } from "./types";
|
|
9
|
+
|
|
10
|
+
const rowClassName =
|
|
11
|
+
"flex w-full flex-col gap-0.5 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50";
|
|
12
|
+
|
|
13
|
+
export function SearchSuggestionList({
|
|
14
|
+
items,
|
|
15
|
+
onSelect,
|
|
16
|
+
activeId,
|
|
17
|
+
onActiveIdChange,
|
|
18
|
+
listboxId,
|
|
19
|
+
className,
|
|
20
|
+
listClassName,
|
|
21
|
+
emptyLabel,
|
|
22
|
+
}: SearchSuggestionListProps) {
|
|
23
|
+
if (items.length === 0) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="search-suggestion-list-empty"
|
|
27
|
+
className={cn("px-1 py-6 text-center text-sm text-slate-500", className)}
|
|
28
|
+
>
|
|
29
|
+
{emptyLabel ?? "No matches."}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let lastGroup: string | undefined;
|
|
35
|
+
const useListbox = Boolean(listboxId);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<nav
|
|
39
|
+
data-slot="search-suggestion-list"
|
|
40
|
+
aria-label="Search results"
|
|
41
|
+
className={cn("flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1", className)}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
{...(useListbox
|
|
45
|
+
? {
|
|
46
|
+
id: listboxId,
|
|
47
|
+
role: "listbox" as const,
|
|
48
|
+
}
|
|
49
|
+
: {})}
|
|
50
|
+
className={cn("flex flex-col gap-0.5", listClassName)}
|
|
51
|
+
>
|
|
52
|
+
{items.map((item) => {
|
|
53
|
+
const showGroup = item.group && item.group !== lastGroup;
|
|
54
|
+
if (item.group) {
|
|
55
|
+
lastGroup = item.group;
|
|
56
|
+
}
|
|
57
|
+
const isActive = activeId === item.id;
|
|
58
|
+
const optionDomId =
|
|
59
|
+
useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : undefined;
|
|
60
|
+
return (
|
|
61
|
+
<Fragment key={item.id}>
|
|
62
|
+
{showGroup ? (
|
|
63
|
+
<div
|
|
64
|
+
role="presentation"
|
|
65
|
+
className="sticky top-0 z-1 bg-slate-950/95 px-2 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 backdrop-blur-sm"
|
|
66
|
+
>
|
|
67
|
+
{item.group}
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
id={optionDomId}
|
|
73
|
+
role={useListbox ? "option" : undefined}
|
|
74
|
+
aria-selected={useListbox ? isActive : undefined}
|
|
75
|
+
data-active={isActive ? "" : undefined}
|
|
76
|
+
className={cn(rowClassName, isActive ? "bg-white/5" : null)}
|
|
77
|
+
onMouseEnter={() => onActiveIdChange?.(item.id)}
|
|
78
|
+
onFocus={() => onActiveIdChange?.(item.id)}
|
|
79
|
+
onClick={() => onSelect(item.id)}
|
|
80
|
+
>
|
|
81
|
+
<span className="font-medium text-slate-100">{item.label}</span>
|
|
82
|
+
{item.description ? (
|
|
83
|
+
<span className="truncate text-xs text-slate-500">{item.description}</span>
|
|
84
|
+
) : null}
|
|
85
|
+
</button>
|
|
86
|
+
</Fragment>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
90
|
+
</nav>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
SearchSuggestionList.displayName = "SearchSuggestionList";
|
|
95
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { searchSuggestionOptionDomId } from "./search-suggestion-utils";
|
|
4
|
+
|
|
5
|
+
describe("searchSuggestionOptionDomId", () => {
|
|
6
|
+
it("prefixes listbox id and sanitizes item id", () => {
|
|
7
|
+
expect(searchSuggestionOptionDomId(":lb:", "/a/b")).toBe(":lb:_opt__a_b");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a stable DOM id for a listbox option so `aria-activedescendant` on the combobox
|
|
3
|
+
* input can reference it. Safe for href-like `itemId` strings.
|
|
4
|
+
*/
|
|
5
|
+
export function searchSuggestionOptionDomId(listboxId: string, itemId: string): string {
|
|
6
|
+
const safe = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7
|
+
return `${listboxId}_opt_${safe}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import type { VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import type { inputVariants } from "../inputs/variants";
|
|
6
|
+
|
|
7
|
+
export type SearchBarProps = Omit<
|
|
8
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
9
|
+
"size" | "children" | "role"
|
|
10
|
+
> & {
|
|
11
|
+
value: string;
|
|
12
|
+
onValueChange?: (value: string) => void;
|
|
13
|
+
leadingSlot?: ReactNode;
|
|
14
|
+
inputClassName?: string;
|
|
15
|
+
appearance?: VariantProps<typeof inputVariants>["appearance"];
|
|
16
|
+
inputSize?: VariantProps<typeof inputVariants>["size"];
|
|
17
|
+
ring?: VariantProps<typeof inputVariants>["ring"];
|
|
18
|
+
/** When set, the input exposes combobox semantics wired to a `role="listbox"` with this id. */
|
|
19
|
+
comboboxListboxId?: string;
|
|
20
|
+
/** Element id of the active option (from `searchSuggestionOptionDomId`) for `aria-activedescendant`. */
|
|
21
|
+
comboboxActiveOptionId?: string;
|
|
22
|
+
/** Whether the suggestion list is visibly expanded (controls `aria-expanded`). */
|
|
23
|
+
comboboxExpanded?: boolean;
|
|
24
|
+
ref?: Ref<HTMLInputElement>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SearchSuggestionItem = {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
group?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type SearchSuggestionListProps = {
|
|
35
|
+
items: readonly SearchSuggestionItem[];
|
|
36
|
+
onSelect: (id: string) => void;
|
|
37
|
+
activeId?: string;
|
|
38
|
+
onActiveIdChange?: (id: string | undefined) => void;
|
|
39
|
+
/** Pass the same id as `comboboxListboxId` on `SearchBar` for ARIA wiring. */
|
|
40
|
+
listboxId?: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
listClassName?: string;
|
|
43
|
+
emptyLabel?: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type SearchFilterable = {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
keywords?: readonly string[];
|
|
51
|
+
href?: string;
|
|
52
|
+
};
|