@zentauri-ui/zentauri-components 1.4.41 → 1.4.61

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.
Files changed (77) hide show
  1. package/README.md +64 -1
  2. package/cli/registry.json +3 -1
  3. package/dist/ui/search/filter-search-suggestions.d.ts +15 -0
  4. package/dist/ui/search/filter-search-suggestions.d.ts.map +1 -0
  5. package/dist/ui/search/index.d.ts +7 -0
  6. package/dist/ui/search/index.d.ts.map +1 -0
  7. package/dist/ui/search/search-bar.d.ts +6 -0
  8. package/dist/ui/search/search-bar.d.ts.map +1 -0
  9. package/dist/ui/search/search-suggestion-list.d.ts +6 -0
  10. package/dist/ui/search/search-suggestion-list.d.ts.map +1 -0
  11. package/dist/ui/search/search-suggestion-utils.d.ts +6 -0
  12. package/dist/ui/search/search-suggestion-utils.d.ts.map +1 -0
  13. package/dist/ui/search/types.d.ts +44 -0
  14. package/dist/ui/search/types.d.ts.map +1 -0
  15. package/dist/ui/search.js +199 -0
  16. package/dist/ui/search.js.map +1 -0
  17. package/dist/ui/search.mjs +194 -0
  18. package/dist/ui/search.mjs.map +1 -0
  19. package/dist/ui/typography/blockquote-base.d.ts +6 -0
  20. package/dist/ui/typography/blockquote-base.d.ts.map +1 -0
  21. package/dist/ui/typography/blockquote.d.ts +6 -0
  22. package/dist/ui/typography/blockquote.d.ts.map +1 -0
  23. package/dist/ui/typography/code-block-base.d.ts +6 -0
  24. package/dist/ui/typography/code-block-base.d.ts.map +1 -0
  25. package/dist/ui/typography/code-block.d.ts +6 -0
  26. package/dist/ui/typography/code-block.d.ts.map +1 -0
  27. package/dist/ui/typography/heading-base.d.ts +6 -0
  28. package/dist/ui/typography/heading-base.d.ts.map +1 -0
  29. package/dist/ui/typography/heading.d.ts +6 -0
  30. package/dist/ui/typography/heading.d.ts.map +1 -0
  31. package/dist/ui/typography/index.d.ts +9 -0
  32. package/dist/ui/typography/index.d.ts.map +1 -0
  33. package/dist/ui/typography/inline-code-base.d.ts +6 -0
  34. package/dist/ui/typography/inline-code-base.d.ts.map +1 -0
  35. package/dist/ui/typography/inline-code.d.ts +6 -0
  36. package/dist/ui/typography/inline-code.d.ts.map +1 -0
  37. package/dist/ui/typography/list-base.d.ts +10 -0
  38. package/dist/ui/typography/list-base.d.ts.map +1 -0
  39. package/dist/ui/typography/list.d.ts +12 -0
  40. package/dist/ui/typography/list.d.ts.map +1 -0
  41. package/dist/ui/typography/text-base.d.ts +6 -0
  42. package/dist/ui/typography/text-base.d.ts.map +1 -0
  43. package/dist/ui/typography/text.d.ts +6 -0
  44. package/dist/ui/typography/text.d.ts.map +1 -0
  45. package/dist/ui/typography/types.d.ts +56 -0
  46. package/dist/ui/typography/types.d.ts.map +1 -0
  47. package/dist/ui/typography/variants.d.ts +16 -0
  48. package/dist/ui/typography/variants.d.ts.map +1 -0
  49. package/dist/ui/typography.js +334 -0
  50. package/dist/ui/typography.js.map +1 -0
  51. package/dist/ui/typography.mjs +321 -0
  52. package/dist/ui/typography.mjs.map +1 -0
  53. package/package.json +3 -3
  54. package/src/ui/search/filter-search-suggestions.test.ts +48 -0
  55. package/src/ui/search/filter-search-suggestions.ts +43 -0
  56. package/src/ui/search/index.ts +11 -0
  57. package/src/ui/search/search-bar.tsx +83 -0
  58. package/src/ui/search/search-suggestion-list.tsx +103 -0
  59. package/src/ui/search/search-suggestion-utils.test.ts +9 -0
  60. package/src/ui/search/search-suggestion-utils.ts +8 -0
  61. package/src/ui/search/types.ts +52 -0
  62. package/src/ui/typography/blockquote-base.tsx +39 -0
  63. package/src/ui/typography/blockquote.tsx +8 -0
  64. package/src/ui/typography/code-block-base.tsx +37 -0
  65. package/src/ui/typography/code-block.tsx +8 -0
  66. package/src/ui/typography/heading-base.tsx +59 -0
  67. package/src/ui/typography/heading.tsx +8 -0
  68. package/src/ui/typography/index.ts +28 -0
  69. package/src/ui/typography/inline-code-base.tsx +27 -0
  70. package/src/ui/typography/inline-code.tsx +8 -0
  71. package/src/ui/typography/list-base.tsx +88 -0
  72. package/src/ui/typography/list.tsx +15 -0
  73. package/src/ui/typography/text-base.tsx +43 -0
  74. package/src/ui/typography/text.tsx +8 -0
  75. package/src/ui/typography/types.ts +90 -0
  76. package/src/ui/typography/typography.test.tsx +80 -0
  77. package/src/ui/typography/variants.ts +72 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@ Published artifacts live under `dist/`. Imports use **per-entry subpaths**: `@ze
16
16
  | `@zentauri-ui/zentauri-components/ui/<name>/animated` | Motion entry for that area when published: animated components, motion presets, and related types (depends on **framer-motion**). |
17
17
  | `@zentauri-ui/zentauri-components/hooks/<entry>` | One hook module or `utils` (`cn`, `clampPage`, `range` from `src/lib/utils.ts`). Entries match files under `src/hooks/` (see **React hooks**). |
18
18
 
19
- The UI `<name>` segment matches the folder under `src/ui/` (for example `accordion`, `select`, `empty-state`, `buttons` for `Button`, `inputs` for `Input`). The hooks `<entry>` is the file stem (for example `useMediaQuery`, `usePagination`) or `utils`.
19
+ The UI `<name>` segment matches the folder under `src/ui/` (for example `accordion`, `select`, `empty-state`, `buttons` for `Button`, `inputs` for `Input`, `typography` for `Heading` / `Text` and related primitives). The hooks `<entry>` is the file stem (for example `useMediaQuery`, `usePagination`) or `utils`.
20
20
 
21
21
  Only a subset of UI areas publish a `/animated` entry (see **Components**). Some motion entries also re-export non-motion pieces from the same feature so you can import one motion subpath for a whole flow; others pair a base `ui/<name>` import with a small set of `*Animated` exports from `ui/<name>/animated`—use the `animated/index.ts` for that area as the source of truth.
22
22
 
@@ -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` | — |
@@ -68,6 +69,68 @@ Import static primitives from `@zentauri-ui/zentauri-components/ui/<subpath>` wh
68
69
  | Toast | `toast` | `toast/animated` |
69
70
  | Toggle | `toggle` | `toggle/animated` |
70
71
  | Tooltip | `tooltip` | `tooltip/animated` |
72
+ | Typography | `typography` | — |
73
+
74
+ ## Typography
75
+
76
+ Import from `@zentauri-ui/zentauri-components/ui/typography`. This entry is **static only** (no `/animated` subpath).
77
+
78
+ **Components:** `Heading`, `Text`, `List` (with `List.Item`, also exported as `ListItem`), `Blockquote`, `InlineCode`, `CodeBlock`.
79
+
80
+ **Types:** `HeadingProps`, `TextProps`, `ListProps`, `ListItemProps`, `BlockquoteProps`, `InlineCodeProps`, `CodeBlockProps`, `HeadingLevel`, `TextElement`, `TypographyTone`, `UnorderedMarker`.
81
+
82
+ **Tone (`tone` prop):** `default`, `muted`, `primary`, `secondary`, `accent`, `destructive`, `info`, `success`, `warning`, `error`, and gradient presets: `gradient-pink-violet`, `gradient-cyan-violet`, `gradient-cyan-blue`, `gradient-cyan-green`, `gradient-cyan-orange`, `gradient-cyan-red`, `gradient-cyan-purple`, `gradient-cyan-pink`. Tones align with kit accent colors (slate / cyan / violet baseline).
83
+
84
+ **Heading:** `level` is required (`1`–`6`) and picks the semantic tag (`h1`–`h6`). Optional `displayLevel` overrides only the visual scale (still the same tag). Optional `bold`, `italic`, `underline`, `strikethrough`.
85
+
86
+ **Text:** Optional `as` (`p`, `span`, `div`, `label`; default `p`). `size` is `sm`, `base`, or `lg` (default `base`). Optional `highlight` plus the same emphasis flags as headings where applicable.
87
+
88
+ **List:** `ordered` renders an `<ol>` (decimal markers); omit or `false` for `<ul>`. Unordered lists accept `marker`: `disc`, `circle`, or `none`.
89
+
90
+ **Blockquote:** Optional `attribution` renders a footer label (separate from the HTML `cite` attribute).
91
+
92
+ **Code samples:** `InlineCode` styles inline `code`; `CodeBlock` renders a `pre` with an inner `code` (pass string or fragment children, not another `code` element) and optional `language` for `aria-label`.
93
+
94
+ **CVA helpers (composition):** `headingLevelVariants`, `textSizeVariants`, `typographyToneVariants`, `unorderedListMarkerVariants`, `orderedListVariants`.
95
+
96
+ ```tsx
97
+ import {
98
+ Blockquote,
99
+ CodeBlock,
100
+ Heading,
101
+ InlineCode,
102
+ List,
103
+ Text,
104
+ } from "@zentauri-ui/zentauri-components/ui/typography";
105
+
106
+ export function ArticleIntro() {
107
+ return (
108
+ <>
109
+ <Heading level={2} displayLevel={1} tone="gradient-cyan-violet">
110
+ Feature title
111
+ </Heading>
112
+ <Text as="p" size="sm" tone="muted">
113
+ Supporting copy with{" "}
114
+ <InlineCode tone="accent">inline code</InlineCode>.
115
+ </Text>
116
+ <List marker="disc" tone="default">
117
+ <List.Item>First item</List.Item>
118
+ <List.Item>Second item</List.Item>
119
+ </List>
120
+ <List ordered tone="muted">
121
+ <List.Item>Step one</List.Item>
122
+ <List.Item>Step two</List.Item>
123
+ </List>
124
+ <Blockquote tone="secondary" attribution="Docs">
125
+ Quoted guidance for the reader.
126
+ </Blockquote>
127
+ <CodeBlock language="tsx" tone="muted">
128
+ {`export const app = () => null;`}
129
+ </CodeBlock>
130
+ </>
131
+ );
132
+ }
133
+ ```
71
134
 
72
135
  ## React hooks
73
136
 
package/cli/registry.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "modal",
19
19
  "pagination",
20
20
  "progress",
21
+ "search",
21
22
  "select",
22
23
  "skeleton",
23
24
  "slider",
@@ -27,7 +28,8 @@
27
28
  "tabs",
28
29
  "toast",
29
30
  "toggle",
30
- "tooltip"
31
+ "tooltip",
32
+ "typography"
31
33
  ],
32
34
  "hooks": [
33
35
  "useBodyScrollLock",
@@ -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,2CA8E3B;yBAvFe,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,199 @@
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
+ const useListbox = Boolean(listboxId);
108
+ const rows = [];
109
+ let lastGroupSeen;
110
+ for (const item of items) {
111
+ const showGroup = Boolean(item.group && item.group !== lastGroupSeen);
112
+ if (item.group) {
113
+ lastGroupSeen = item.group;
114
+ }
115
+ rows.push({ item, showGroup });
116
+ }
117
+ return /* @__PURE__ */ jsxRuntime.jsx(
118
+ "nav",
119
+ {
120
+ "data-slot": "search-suggestion-list",
121
+ "aria-label": "Search results",
122
+ className: chunkUOZYPWDZ_js.cn("flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1", className),
123
+ children: /* @__PURE__ */ jsxRuntime.jsx(
124
+ "div",
125
+ {
126
+ ...useListbox ? {
127
+ id: listboxId,
128
+ role: "listbox"
129
+ } : {},
130
+ className: chunkUOZYPWDZ_js.cn("flex flex-col gap-0.5", listClassName),
131
+ children: rows.map(({ item, showGroup }) => {
132
+ const isActive = activeId === item.id;
133
+ const optionDomId = useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : void 0;
134
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
135
+ showGroup ? /* @__PURE__ */ jsxRuntime.jsx(
136
+ "div",
137
+ {
138
+ role: "presentation",
139
+ 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",
140
+ children: item.group
141
+ }
142
+ ) : null,
143
+ /* @__PURE__ */ jsxRuntime.jsxs(
144
+ "button",
145
+ {
146
+ type: "button",
147
+ id: optionDomId,
148
+ role: useListbox ? "option" : void 0,
149
+ "aria-selected": useListbox ? isActive : void 0,
150
+ "data-active": isActive ? "" : void 0,
151
+ className: chunkUOZYPWDZ_js.cn(rowClassName, isActive ? "bg-white/5" : null),
152
+ onMouseEnter: () => onActiveIdChange?.(item.id),
153
+ onFocus: () => onActiveIdChange?.(item.id),
154
+ onClick: () => onSelect(item.id),
155
+ children: [
156
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-slate-100", children: item.label }),
157
+ item.description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-slate-500", children: item.description }) : null
158
+ ]
159
+ }
160
+ )
161
+ ] }, item.id);
162
+ })
163
+ }
164
+ )
165
+ }
166
+ );
167
+ }
168
+ SearchSuggestionList.displayName = "SearchSuggestionList";
169
+
170
+ // src/ui/search/filter-search-suggestions.ts
171
+ function filterSearchSuggestions({
172
+ query,
173
+ items,
174
+ options = { maxResults: 20 }
175
+ }) {
176
+ const maxResults = options.maxResults ?? 20;
177
+ const normalized = query.trim().toLowerCase();
178
+ if (!normalized) {
179
+ return [];
180
+ }
181
+ const matches = [];
182
+ for (const item of items) {
183
+ 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));
184
+ if (isMatch) {
185
+ matches.push(item);
186
+ if (matches.length >= maxResults || maxResults <= 0) {
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ return matches;
192
+ }
193
+
194
+ exports.SearchBar = SearchBar;
195
+ exports.SearchSuggestionList = SearchSuggestionList;
196
+ exports.filterSearchSuggestions = filterSearchSuggestions;
197
+ exports.searchSuggestionOptionDomId = searchSuggestionOptionDomId;
198
+ //# sourceMappingURL=search.js.map
199
+ //# 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 { 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 const useListbox = Boolean(listboxId);\n\n const rows: Array<{\n item: (typeof items)[number];\n showGroup: boolean;\n }> = [];\n let lastGroupSeen: string | undefined;\n for (const item of items) {\n const showGroup = Boolean(item.group && item.group !== lastGroupSeen);\n if (item.group) {\n lastGroupSeen = item.group;\n }\n rows.push({ item, showGroup });\n }\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 {rows.map(({ item, showGroup }) => {\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,194 @@
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
+ const useListbox = Boolean(listboxId);
106
+ const rows = [];
107
+ let lastGroupSeen;
108
+ for (const item of items) {
109
+ const showGroup = Boolean(item.group && item.group !== lastGroupSeen);
110
+ if (item.group) {
111
+ lastGroupSeen = item.group;
112
+ }
113
+ rows.push({ item, showGroup });
114
+ }
115
+ return /* @__PURE__ */ jsx(
116
+ "nav",
117
+ {
118
+ "data-slot": "search-suggestion-list",
119
+ "aria-label": "Search results",
120
+ className: cn("flex max-h-[min(50vh,360px)] flex-col gap-1 overflow-y-auto pr-1", className),
121
+ children: /* @__PURE__ */ jsx(
122
+ "div",
123
+ {
124
+ ...useListbox ? {
125
+ id: listboxId,
126
+ role: "listbox"
127
+ } : {},
128
+ className: cn("flex flex-col gap-0.5", listClassName),
129
+ children: rows.map(({ item, showGroup }) => {
130
+ const isActive = activeId === item.id;
131
+ const optionDomId = useListbox && listboxId ? searchSuggestionOptionDomId(listboxId, item.id) : void 0;
132
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
133
+ showGroup ? /* @__PURE__ */ jsx(
134
+ "div",
135
+ {
136
+ role: "presentation",
137
+ 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",
138
+ children: item.group
139
+ }
140
+ ) : null,
141
+ /* @__PURE__ */ jsxs(
142
+ "button",
143
+ {
144
+ type: "button",
145
+ id: optionDomId,
146
+ role: useListbox ? "option" : void 0,
147
+ "aria-selected": useListbox ? isActive : void 0,
148
+ "data-active": isActive ? "" : void 0,
149
+ className: cn(rowClassName, isActive ? "bg-white/5" : null),
150
+ onMouseEnter: () => onActiveIdChange?.(item.id),
151
+ onFocus: () => onActiveIdChange?.(item.id),
152
+ onClick: () => onSelect(item.id),
153
+ children: [
154
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-100", children: item.label }),
155
+ item.description ? /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-slate-500", children: item.description }) : null
156
+ ]
157
+ }
158
+ )
159
+ ] }, item.id);
160
+ })
161
+ }
162
+ )
163
+ }
164
+ );
165
+ }
166
+ SearchSuggestionList.displayName = "SearchSuggestionList";
167
+
168
+ // src/ui/search/filter-search-suggestions.ts
169
+ function filterSearchSuggestions({
170
+ query,
171
+ items,
172
+ options = { maxResults: 20 }
173
+ }) {
174
+ const maxResults = options.maxResults ?? 20;
175
+ const normalized = query.trim().toLowerCase();
176
+ if (!normalized) {
177
+ return [];
178
+ }
179
+ const matches = [];
180
+ for (const item of items) {
181
+ 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));
182
+ if (isMatch) {
183
+ matches.push(item);
184
+ if (matches.length >= maxResults || maxResults <= 0) {
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ return matches;
190
+ }
191
+
192
+ export { SearchBar, SearchSuggestionList, filterSearchSuggestions, searchSuggestionOptionDomId };
193
+ //# sourceMappingURL=search.mjs.map
194
+ //# sourceMappingURL=search.mjs.map