@etoile-dev/react 0.2.3 → 1.0.0

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 (74) hide show
  1. package/README.md +341 -206
  2. package/dist/Searchbar.d.ts +315 -0
  3. package/dist/Searchbar.js +207 -0
  4. package/dist/context.d.ts +57 -0
  5. package/dist/context.js +32 -0
  6. package/dist/hooks/useEtoileSearch.d.ts +122 -0
  7. package/dist/hooks/useEtoileSearch.js +138 -0
  8. package/dist/index.d.ts +44 -19
  9. package/dist/index.js +37 -12
  10. package/dist/primitives/Content.d.ts +34 -0
  11. package/dist/primitives/Content.js +108 -0
  12. package/dist/primitives/Empty.d.ts +25 -0
  13. package/dist/primitives/Empty.js +25 -0
  14. package/dist/primitives/Error.d.ts +29 -0
  15. package/dist/primitives/Error.js +26 -0
  16. package/dist/primitives/Group.d.ts +30 -0
  17. package/dist/primitives/Group.js +22 -0
  18. package/dist/primitives/Icon.d.ts +21 -0
  19. package/dist/primitives/Icon.js +14 -0
  20. package/dist/primitives/Input.d.ts +32 -0
  21. package/dist/primitives/Input.js +70 -0
  22. package/dist/primitives/Item.d.ts +61 -0
  23. package/dist/primitives/Item.js +76 -0
  24. package/dist/primitives/Kbd.d.ts +20 -0
  25. package/dist/primitives/Kbd.js +13 -0
  26. package/dist/primitives/List.d.ts +35 -0
  27. package/dist/primitives/List.js +37 -0
  28. package/dist/primitives/Loading.d.ts +25 -0
  29. package/dist/primitives/Loading.js +26 -0
  30. package/dist/primitives/Modal.d.ts +39 -0
  31. package/dist/primitives/Modal.js +37 -0
  32. package/dist/primitives/ModalInput.d.ts +61 -0
  33. package/dist/primitives/ModalInput.js +33 -0
  34. package/dist/primitives/Overlay.d.ts +21 -0
  35. package/dist/primitives/Overlay.js +41 -0
  36. package/dist/primitives/Portal.d.ts +28 -0
  37. package/dist/primitives/Portal.js +30 -0
  38. package/dist/primitives/Root.d.ts +116 -0
  39. package/dist/primitives/Root.js +413 -0
  40. package/dist/primitives/Separator.d.ts +19 -0
  41. package/dist/primitives/Separator.js +18 -0
  42. package/dist/primitives/Thumbnail.d.ts +31 -0
  43. package/dist/primitives/Thumbnail.js +59 -0
  44. package/dist/primitives/Trigger.d.ts +28 -0
  45. package/dist/primitives/Trigger.js +35 -0
  46. package/dist/store.d.ts +38 -0
  47. package/dist/store.js +63 -0
  48. package/dist/styles.css +480 -133
  49. package/dist/types.d.ts +3 -31
  50. package/dist/utils/composeRefs.d.ts +12 -0
  51. package/dist/utils/composeRefs.js +27 -0
  52. package/dist/utils/slot.d.ts +22 -0
  53. package/dist/utils/slot.js +58 -0
  54. package/package.json +9 -5
  55. package/dist/Search.d.ts +0 -39
  56. package/dist/Search.js +0 -31
  57. package/dist/components/SearchIcon.d.ts +0 -22
  58. package/dist/components/SearchIcon.js +0 -17
  59. package/dist/components/SearchInput.d.ts +0 -30
  60. package/dist/components/SearchInput.js +0 -59
  61. package/dist/components/SearchKbd.d.ts +0 -30
  62. package/dist/components/SearchKbd.js +0 -24
  63. package/dist/components/SearchResult.d.ts +0 -31
  64. package/dist/components/SearchResult.js +0 -40
  65. package/dist/components/SearchResultThumbnail.d.ts +0 -38
  66. package/dist/components/SearchResultThumbnail.js +0 -38
  67. package/dist/components/SearchResults.d.ts +0 -39
  68. package/dist/components/SearchResults.js +0 -53
  69. package/dist/components/SearchRoot.d.ts +0 -44
  70. package/dist/components/SearchRoot.js +0 -132
  71. package/dist/context/SearchContext.d.ts +0 -55
  72. package/dist/context/SearchContext.js +0 -36
  73. package/dist/hooks/useSearch.d.ts +0 -56
  74. package/dist/hooks/useSearch.js +0 -116
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Slot } from "../utils/slot.js";
4
+ /**
5
+ * Groups related search items under a labelled section.
6
+ *
7
+ * Renders a heading and wraps children in an ARIA group role.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <Searchbar.Group label="Paintings">
12
+ * <Searchbar.Item value="starry-night">The Starry Night</Searchbar.Item>
13
+ * <Searchbar.Item value="irises">Irises</Searchbar.Item>
14
+ * </Searchbar.Group>
15
+ * ```
16
+ */
17
+ export const Group = React.forwardRef(({ label, className, asChild = false, children, ...props }, forwardedRef) => {
18
+ const labelId = React.useId();
19
+ const Comp = asChild ? Slot : "div";
20
+ return (_jsxs(Comp, { ...props, ref: forwardedRef, role: "group", "aria-labelledby": label ? labelId : undefined, className: className, "data-slot": "searchbar-group", children: [label && (_jsx("div", { id: labelId, "data-slot": "searchbar-group-label", children: label })), children] }));
21
+ });
22
+ Group.displayName = "Searchbar.Group";
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ export type SearchbarIconProps = {
3
+ /** Width and height in pixels (default: 18) */
4
+ size?: number;
5
+ className?: string;
6
+ } & React.SVGAttributes<SVGElement>;
7
+ /**
8
+ * Search magnifying glass icon.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <div data-slot="searchbar-input-row">
13
+ * <Searchbar.Icon />
14
+ * <Searchbar.Input />
15
+ * </div>
16
+ * ```
17
+ */
18
+ export declare const Icon: {
19
+ ({ size, className, ...props }: SearchbarIconProps): import("react/jsx-runtime").JSX.Element;
20
+ displayName: string;
21
+ };
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Search magnifying glass icon.
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * <div data-slot="searchbar-input-row">
8
+ * <Searchbar.Icon />
9
+ * <Searchbar.Input />
10
+ * </div>
11
+ * ```
12
+ */
13
+ export const Icon = ({ size = 18, className, ...props }) => (_jsxs("svg", { ...props, xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true", children: [_jsx("path", { d: "m21 21-4.34-4.34" }), _jsx("circle", { cx: "11", cy: "11", r: "8" })] }));
14
+ Icon.displayName = "Searchbar.Icon";
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ export type SearchbarInputProps = {
3
+ /** Placeholder text */
4
+ placeholder?: string;
5
+ /** Additional CSS class name */
6
+ className?: string;
7
+ /** Render as child element instead of <input> */
8
+ asChild?: boolean;
9
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange">;
10
+ /**
11
+ * Combobox input primitive.
12
+ *
13
+ * Wires to Searchbar.Root state and applies ARIA combobox attributes.
14
+ * In uncontrolled mode, it updates internal query/open state.
15
+ * In controlled mode (`search` prop on Root), it emits `onSearchChange`
16
+ * without mutating the internal query directly.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <Searchbar.Root>
21
+ * <Searchbar.Input placeholder="Search paintings…" />
22
+ * </Searchbar.Root>
23
+ * ```
24
+ */
25
+ export declare const Input: React.ForwardRefExoticComponent<{
26
+ /** Placeholder text */
27
+ placeholder?: string;
28
+ /** Additional CSS class name */
29
+ className?: string;
30
+ /** Render as child element instead of <input> */
31
+ asChild?: boolean;
32
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> & React.RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ /**
6
+ * Combobox input primitive.
7
+ *
8
+ * Wires to Searchbar.Root state and applies ARIA combobox attributes.
9
+ * In uncontrolled mode, it updates internal query/open state.
10
+ * In controlled mode (`search` prop on Root), it emits `onSearchChange`
11
+ * without mutating the internal query directly.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Searchbar.Root>
16
+ * <Searchbar.Input placeholder="Search paintings…" />
17
+ * </Searchbar.Root>
18
+ * ```
19
+ */
20
+ export const Input = React.forwardRef(({ placeholder, className, asChild = false, ...props }, forwardedRef) => {
21
+ const { store, listId, getItemId, isSearchControlled, onSearchChange, handleKeyDown } = useSearchbarContext();
22
+ const query = useSearchbarStore(store, (s) => s.query);
23
+ const isOpen = useSearchbarStore(store, (s) => s.open);
24
+ const selectedValue = useSearchbarStore(store, (s) => s.selectedValue);
25
+ const hasItems = useSearchbarStore(store, (s) => s.filteredValues.length > 0);
26
+ const showResults = isOpen && hasItems;
27
+ const activeId = selectedValue && showResults ? getItemId(selectedValue) : undefined;
28
+ const handleChange = (event) => {
29
+ props.onChange?.(event);
30
+ const nextQuery = event.target.value;
31
+ if (isSearchControlled) {
32
+ onSearchChange?.(nextQuery);
33
+ }
34
+ else {
35
+ store.setState((s) => ({
36
+ ...s,
37
+ query: nextQuery,
38
+ open: nextQuery.trim() !== "",
39
+ }));
40
+ }
41
+ };
42
+ const Comp = asChild ? Slot : "input";
43
+ return (_jsxs(_Fragment, { children: [_jsx(Comp, { ...props, ref: forwardedRef, type: asChild ? undefined : "text", role: "combobox", "aria-expanded": showResults, "aria-controls": listId, "aria-activedescendant": activeId, "aria-autocomplete": "list", autoComplete: "off", autoCorrect: "off", spellCheck: false, placeholder: placeholder, className: className, value: query, "data-slot": "searchbar-input", "data-state": isOpen ? "open" : "closed", onChange: handleChange, onKeyDown: (e) => {
44
+ handleKeyDown(e);
45
+ props.onKeyDown?.(e);
46
+ }, onFocus: (event) => {
47
+ props.onFocus?.(event);
48
+ if (!event.defaultPrevented && query.trim() !== "" && hasItems) {
49
+ store.setState((s) => ({ ...s, open: true }));
50
+ }
51
+ } }), _jsx("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: {
52
+ position: "absolute",
53
+ width: 1,
54
+ height: 1,
55
+ padding: 0,
56
+ margin: -1,
57
+ overflow: "hidden",
58
+ clip: "rect(0,0,0,0)",
59
+ whiteSpace: "nowrap",
60
+ border: 0,
61
+ }, children: _jsx(ResultCount, { store: store, isOpen: showResults }) })] }));
62
+ });
63
+ Input.displayName = "Searchbar.Input";
64
+ // Isolated component so only it re-renders for the count announcement
65
+ const ResultCount = ({ store, isOpen, }) => {
66
+ const count = useSearchbarStore(store, (s) => s.filteredValues.length);
67
+ if (!isOpen)
68
+ return null;
69
+ return _jsxs(_Fragment, { children: [count === 1 ? "1 result" : `${count} results`, " available"] });
70
+ };
@@ -0,0 +1,61 @@
1
+ import * as React from "react";
2
+ export type SearchbarItemProps = {
3
+ /**
4
+ * Stable identifier for this item. Used for selection, keyboard navigation,
5
+ * and ARIA relationships. Must be unique within the list.
6
+ */
7
+ value: string;
8
+ /**
9
+ * Optional human-readable label. Useful when `value` is an opaque ID.
10
+ */
11
+ label?: string;
12
+ /** Whether this item can be selected */
13
+ disabled?: boolean;
14
+ /**
15
+ * Called when this specific item is selected (in addition to root onSelect).
16
+ * Receives the item's `value`.
17
+ */
18
+ onSelect?: (value: string) => void;
19
+ children: React.ReactNode;
20
+ className?: string;
21
+ asChild?: boolean;
22
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect">;
23
+ /**
24
+ * Individual search result item with stable value-based identity.
25
+ *
26
+ * Registers itself in the store on mount and de-registers on unmount.
27
+ * Visibility is controlled by what you render in `Searchbar.List`
28
+ * (UI-only primitive; no internal filtering).
29
+ *
30
+ * When rendered inside the Etoile `<Searchbar />` wrapper, children can read
31
+ * result data via `SearchbarItemDataContext` (used by `Searchbar.Thumbnail`).
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <Searchbar.Item value="starry-night" label="The Starry Night">
36
+ * <img src={thumbnail} />
37
+ * The Starry Night
38
+ * </Searchbar.Item>
39
+ * ```
40
+ */
41
+ export declare const Item: React.ForwardRefExoticComponent<{
42
+ /**
43
+ * Stable identifier for this item. Used for selection, keyboard navigation,
44
+ * and ARIA relationships. Must be unique within the list.
45
+ */
46
+ value: string;
47
+ /**
48
+ * Optional human-readable label. Useful when `value` is an opaque ID.
49
+ */
50
+ label?: string;
51
+ /** Whether this item can be selected */
52
+ disabled?: boolean;
53
+ /**
54
+ * Called when this specific item is selected (in addition to root onSelect).
55
+ * Receives the item's `value`.
56
+ */
57
+ onSelect?: (value: string) => void;
58
+ children: React.ReactNode;
59
+ className?: string;
60
+ asChild?: boolean;
61
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore, SearchbarItemContext } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ import { useComposeRefs } from "../utils/composeRefs.js";
6
+ /**
7
+ * Individual search result item with stable value-based identity.
8
+ *
9
+ * Registers itself in the store on mount and de-registers on unmount.
10
+ * Visibility is controlled by what you render in `Searchbar.List`
11
+ * (UI-only primitive; no internal filtering).
12
+ *
13
+ * When rendered inside the Etoile `<Searchbar />` wrapper, children can read
14
+ * result data via `SearchbarItemDataContext` (used by `Searchbar.Thumbnail`).
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <Searchbar.Item value="starry-night" label="The Starry Night">
19
+ * <img src={thumbnail} />
20
+ * The Starry Night
21
+ * </Searchbar.Item>
22
+ * ```
23
+ */
24
+ export const Item = React.forwardRef(({ value, label, disabled = false, onSelect, children, className, asChild = false, ...props }, forwardedRef) => {
25
+ const { store, getItemId, onSelect: ctxOnSelect, registerItem, unregisterItem } = useSearchbarContext();
26
+ const internalRef = React.useRef(null);
27
+ const composedRef = useComposeRefs(internalRef, forwardedRef);
28
+ const effectiveLabel = label ?? value;
29
+ // Register on mount, update when meta changes, unregister on unmount
30
+ React.useLayoutEffect(() => {
31
+ registerItem({
32
+ value,
33
+ label: effectiveLabel,
34
+ disabled,
35
+ node: internalRef.current,
36
+ onSelect,
37
+ });
38
+ return () => unregisterItem(value);
39
+ }, [value, effectiveLabel, disabled]); // eslint-disable-line react-hooks/exhaustive-deps
40
+ // Update onSelect callback reference without full re-register
41
+ React.useEffect(() => {
42
+ store.setState((s) => {
43
+ const items = new Map(s.items);
44
+ const existing = items.get(value);
45
+ if (existing) {
46
+ items.set(value, { ...existing, onSelect });
47
+ }
48
+ return { ...s, items };
49
+ });
50
+ }, [onSelect, value, store]);
51
+ const isSelected = useSearchbarStore(store, (s) => s.selectedValue === value);
52
+ const isVisible = useSearchbarStore(store, (s) => s.filteredSet.has(value));
53
+ if (!isVisible) {
54
+ return null;
55
+ }
56
+ const id = getItemId(value);
57
+ const handleClick = () => {
58
+ if (!disabled) {
59
+ ctxOnSelect(value);
60
+ }
61
+ };
62
+ const handlePointerMove = () => {
63
+ if (!disabled) {
64
+ store.setState((s) => ({ ...s, selectedValue: value }));
65
+ }
66
+ };
67
+ const Comp = asChild ? Slot : "div";
68
+ return (_jsx(SearchbarItemContext.Provider, { value: value, children: _jsx(Comp, { ...props, ref: composedRef, id: id, role: "option", "aria-selected": isSelected, "aria-disabled": disabled || undefined, "data-selected": isSelected ? "true" : "false", "data-disabled": disabled ? "true" : undefined, "data-value": value, "data-slot": "searchbar-item", tabIndex: isSelected ? 0 : -1, className: className, onClick: (e) => {
69
+ props.onClick?.(e);
70
+ handleClick();
71
+ }, onPointerMove: (e) => {
72
+ props.onPointerMove?.(e);
73
+ handlePointerMove();
74
+ }, children: children }) }));
75
+ });
76
+ Item.displayName = "Searchbar.Item";
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ export type SearchbarKbdProps = {
3
+ /** Shortcut text (default: "⌘K") */
4
+ children?: React.ReactNode;
5
+ className?: string;
6
+ } & React.HTMLAttributes<HTMLElement>;
7
+ /**
8
+ * Keyboard shortcut badge.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Searchbar.Kbd /> // renders "⌘K"
13
+ * <Searchbar.Kbd>/</Searchbar.Kbd>
14
+ * <Searchbar.Kbd>Ctrl K</Searchbar.Kbd>
15
+ * ```
16
+ */
17
+ export declare const Kbd: {
18
+ ({ children, className, ...props }: SearchbarKbdProps): import("react/jsx-runtime").JSX.Element;
19
+ displayName: string;
20
+ };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Keyboard shortcut badge.
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * <Searchbar.Kbd /> // renders "⌘K"
8
+ * <Searchbar.Kbd>/</Searchbar.Kbd>
9
+ * <Searchbar.Kbd>Ctrl K</Searchbar.Kbd>
10
+ * ```
11
+ */
12
+ export const Kbd = ({ children = "⌘K", className, ...props }) => (_jsx("kbd", { ...props, "data-slot": "searchbar-kbd", className: className, children: children }));
13
+ Kbd.displayName = "Searchbar.Kbd";
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ export type SearchbarListProps = {
3
+ /** Additional CSS class name */
4
+ className?: string;
5
+ /** Render as child element instead of <div> */
6
+ asChild?: boolean;
7
+ children: React.ReactNode;
8
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role">;
9
+ /**
10
+ * Container for search result items implementing the ARIA listbox role.
11
+ *
12
+ * Renders while the list is open. This allows command-palette flows where
13
+ * the list stays visible even with an empty query (default actions).
14
+ *
15
+ * Place Searchbar.Item, Searchbar.Empty, Searchbar.Loading, and
16
+ * Searchbar.Error inside it.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <Searchbar.List>
21
+ * {items.map((item) => (
22
+ * <Searchbar.Item key={item.id} value={item.id}>{item.title}</Searchbar.Item>
23
+ * ))}
24
+ * <Searchbar.Empty>No results</Searchbar.Empty>
25
+ * <Searchbar.Loading>Searching…</Searchbar.Loading>
26
+ * </Searchbar.List>
27
+ * ```
28
+ */
29
+ export declare const List: React.ForwardRefExoticComponent<{
30
+ /** Additional CSS class name */
31
+ className?: string;
32
+ /** Render as child element instead of <div> */
33
+ asChild?: boolean;
34
+ children: React.ReactNode;
35
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore, SearchbarHideListWhenQueryEmptyContext, } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ /**
6
+ * Container for search result items implementing the ARIA listbox role.
7
+ *
8
+ * Renders while the list is open. This allows command-palette flows where
9
+ * the list stays visible even with an empty query (default actions).
10
+ *
11
+ * Place Searchbar.Item, Searchbar.Empty, Searchbar.Loading, and
12
+ * Searchbar.Error inside it.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Searchbar.List>
17
+ * {items.map((item) => (
18
+ * <Searchbar.Item key={item.id} value={item.id}>{item.title}</Searchbar.Item>
19
+ * ))}
20
+ * <Searchbar.Empty>No results</Searchbar.Empty>
21
+ * <Searchbar.Loading>Searching…</Searchbar.Loading>
22
+ * </Searchbar.List>
23
+ * ```
24
+ */
25
+ export const List = React.forwardRef(({ className, asChild = false, children, ...props }, forwardedRef) => {
26
+ const { store, listId } = useSearchbarContext();
27
+ const hideWhenQueryEmpty = React.useContext(SearchbarHideListWhenQueryEmptyContext);
28
+ const isOpen = useSearchbarStore(store, (s) => s.open);
29
+ const query = useSearchbarStore(store, (s) => s.query);
30
+ if (!isOpen)
31
+ return null;
32
+ if (hideWhenQueryEmpty && query.trim() === "")
33
+ return null;
34
+ const Comp = asChild ? Slot : "div";
35
+ return (_jsx(Comp, { ...props, ref: forwardedRef, id: listId, role: "listbox", className: className, "data-state": "open", "data-slot": "searchbar-list", children: children }));
36
+ });
37
+ List.displayName = "Searchbar.List";
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ export type SearchbarLoadingProps = {
3
+ /** Defaults to a built-in spinner */
4
+ children?: React.ReactNode;
5
+ className?: string;
6
+ asChild?: boolean;
7
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role">;
8
+ /**
9
+ * Renders only while a search is in progress (isLoading=true).
10
+ * Designed to be placed inside or alongside Searchbar.List.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <Searchbar.List>
15
+ * {items.map(…)}
16
+ * <Searchbar.Loading>Searching…</Searchbar.Loading>
17
+ * </Searchbar.List>
18
+ * ```
19
+ */
20
+ export declare const Loading: React.ForwardRefExoticComponent<{
21
+ /** Defaults to a built-in spinner */
22
+ children?: React.ReactNode;
23
+ className?: string;
24
+ asChild?: boolean;
25
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ /**
6
+ * Renders only while a search is in progress (isLoading=true).
7
+ * Designed to be placed inside or alongside Searchbar.List.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <Searchbar.List>
12
+ * {items.map(…)}
13
+ * <Searchbar.Loading>Searching…</Searchbar.Loading>
14
+ * </Searchbar.List>
15
+ * ```
16
+ */
17
+ export const Loading = React.forwardRef(({ children, className, asChild = false, ...props }, forwardedRef) => {
18
+ const { store } = useSearchbarContext();
19
+ const show = useSearchbarStore(store, (s) => s.isLoading && s.open && s.query.trim() !== "");
20
+ if (!show)
21
+ return null;
22
+ const Comp = asChild ? Slot : "div";
23
+ return (_jsx(Comp, { ...props, ref: forwardedRef, role: "status", "aria-label": "Loading results", className: className, "data-slot": "searchbar-loading", "data-state": "loading", children: children ?? _jsx(DefaultSpinner, {}) }));
24
+ });
25
+ Loading.displayName = "Searchbar.Loading";
26
+ const DefaultSpinner = () => (_jsx("svg", { "data-slot": "searchbar-spinner", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import type { SearchbarRootProps } from "./Root.js";
3
+ export type SearchbarModalProps = {
4
+ /** Accessible label for the dialog (default: "Search") */
5
+ "aria-label"?: string;
6
+ } & Omit<SearchbarRootProps, "asChild">;
7
+ /**
8
+ * Command palette primitive. Combines Root + Portal + Overlay + Content
9
+ * into a single component — analogous to `Command.Dialog` in cmdk.
10
+ *
11
+ * Handles portal rendering, backdrop overlay, focus trapping, and
12
+ * Escape/outside-click close. Children go directly into the Content panel.
13
+ * This primitive is unstyled; add `className="etoile-search"` on Modal/Root
14
+ * and import `@etoile-dev/react/styles.css` to use the default theme.
15
+ *
16
+ * @example Basic usage
17
+ * ```tsx
18
+ * <Searchbar.Modal hotkey="mod+k">
19
+ * <Searchbar.ModalInput />
20
+ * <Searchbar.List>…</Searchbar.List>
21
+ * </Searchbar.Modal>
22
+ * ```
23
+ *
24
+ * @example Controlled open state
25
+ * ```tsx
26
+ * <Searchbar.Modal open={open} onOpenChange={setOpen}>
27
+ * <Searchbar.ModalInput placeholder="Search paintings…" />
28
+ * <Searchbar.List>
29
+ * {items.map((item) => (
30
+ * <Searchbar.Item key={item.id} value={item.id}>{item.title}</Searchbar.Item>
31
+ * ))}
32
+ * </Searchbar.List>
33
+ * </Searchbar.Modal>
34
+ * ```
35
+ */
36
+ export declare const Modal: React.ForwardRefExoticComponent<{
37
+ /** Accessible label for the dialog (default: "Search") */
38
+ "aria-label"?: string;
39
+ } & Omit<SearchbarRootProps, "asChild"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Root } from "./Root.js";
4
+ import { Portal } from "./Portal.js";
5
+ import { Overlay } from "./Overlay.js";
6
+ import { Content } from "./Content.js";
7
+ /**
8
+ * Command palette primitive. Combines Root + Portal + Overlay + Content
9
+ * into a single component — analogous to `Command.Dialog` in cmdk.
10
+ *
11
+ * Handles portal rendering, backdrop overlay, focus trapping, and
12
+ * Escape/outside-click close. Children go directly into the Content panel.
13
+ * This primitive is unstyled; add `className="etoile-search"` on Modal/Root
14
+ * and import `@etoile-dev/react/styles.css` to use the default theme.
15
+ *
16
+ * @example Basic usage
17
+ * ```tsx
18
+ * <Searchbar.Modal hotkey="mod+k">
19
+ * <Searchbar.ModalInput />
20
+ * <Searchbar.List>…</Searchbar.List>
21
+ * </Searchbar.Modal>
22
+ * ```
23
+ *
24
+ * @example Controlled open state
25
+ * ```tsx
26
+ * <Searchbar.Modal open={open} onOpenChange={setOpen}>
27
+ * <Searchbar.ModalInput placeholder="Search paintings…" />
28
+ * <Searchbar.List>
29
+ * {items.map((item) => (
30
+ * <Searchbar.Item key={item.id} value={item.id}>{item.title}</Searchbar.Item>
31
+ * ))}
32
+ * </Searchbar.List>
33
+ * </Searchbar.Modal>
34
+ * ```
35
+ */
36
+ export const Modal = React.forwardRef(({ "aria-label": ariaLabel = "Search", children, className, ...rootProps }, ref) => (_jsx(Root, { ...rootProps, ref: ref, className: className, children: _jsxs(Portal, { children: [_jsx(Overlay, {}), _jsx(Content, { "aria-label": ariaLabel, children: children })] }) })));
37
+ Modal.displayName = "Searchbar.Modal";
@@ -0,0 +1,61 @@
1
+ import * as React from "react";
2
+ export type SearchbarModalInputProps = {
3
+ /** Placeholder for the search input (default: "Search…") */
4
+ placeholder?: string;
5
+ /**
6
+ * Leading icon content.
7
+ * Pass `null` to hide it.
8
+ * Default: built-in Searchbar.Icon
9
+ */
10
+ icon?: React.ReactNode | null;
11
+ /**
12
+ * Content for the keyboard shortcut badge.
13
+ * Pass `null` to hide the badge entirely.
14
+ * Default: "Esc"
15
+ */
16
+ kbd?: React.ReactNode | null;
17
+ className?: string;
18
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children">;
19
+ /**
20
+ * Pre-composed input row for command palette mode.
21
+ *
22
+ * Renders a search icon, combobox input, and optional keyboard badge in a
23
+ * flex row. A separator border appears automatically when results are
24
+ * visible below (via `:has(+ [role="listbox"])`).
25
+ *
26
+ * Styled via `data-slot="searchbar-modal-input"` — fully independent from
27
+ * the inline `Searchbar` input wrapper.
28
+ *
29
+ * @example Basic usage
30
+ * ```tsx
31
+ * <Searchbar.Modal hotkey="mod+k">
32
+ * <Searchbar.ModalInput />
33
+ * <Searchbar.List>…</Searchbar.List>
34
+ * </Searchbar.Modal>
35
+ * ```
36
+ *
37
+ * @example Custom placeholder, no kbd badge
38
+ * ```tsx
39
+ * <Searchbar.Modal>
40
+ * <Searchbar.ModalInput placeholder="Search paintings…" icon={null} kbd={null} />
41
+ * <Searchbar.List>…</Searchbar.List>
42
+ * </Searchbar.Modal>
43
+ * ```
44
+ */
45
+ export declare const ModalInput: React.ForwardRefExoticComponent<{
46
+ /** Placeholder for the search input (default: "Search…") */
47
+ placeholder?: string;
48
+ /**
49
+ * Leading icon content.
50
+ * Pass `null` to hide it.
51
+ * Default: built-in Searchbar.Icon
52
+ */
53
+ icon?: React.ReactNode | null;
54
+ /**
55
+ * Content for the keyboard shortcut badge.
56
+ * Pass `null` to hide the badge entirely.
57
+ * Default: "Esc"
58
+ */
59
+ kbd?: React.ReactNode | null;
60
+ className?: string;
61
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Input } from "./Input.js";
4
+ import { Icon } from "./Icon.js";
5
+ import { Kbd } from "./Kbd.js";
6
+ /**
7
+ * Pre-composed input row for command palette mode.
8
+ *
9
+ * Renders a search icon, combobox input, and optional keyboard badge in a
10
+ * flex row. A separator border appears automatically when results are
11
+ * visible below (via `:has(+ [role="listbox"])`).
12
+ *
13
+ * Styled via `data-slot="searchbar-modal-input"` — fully independent from
14
+ * the inline `Searchbar` input wrapper.
15
+ *
16
+ * @example Basic usage
17
+ * ```tsx
18
+ * <Searchbar.Modal hotkey="mod+k">
19
+ * <Searchbar.ModalInput />
20
+ * <Searchbar.List>…</Searchbar.List>
21
+ * </Searchbar.Modal>
22
+ * ```
23
+ *
24
+ * @example Custom placeholder, no kbd badge
25
+ * ```tsx
26
+ * <Searchbar.Modal>
27
+ * <Searchbar.ModalInput placeholder="Search paintings…" icon={null} kbd={null} />
28
+ * <Searchbar.List>…</Searchbar.List>
29
+ * </Searchbar.Modal>
30
+ * ```
31
+ */
32
+ export const ModalInput = React.forwardRef(({ placeholder = "Search…", icon = _jsx(Icon, {}), kbd = "Esc", className, ...props }, ref) => (_jsxs("div", { ...props, ref: ref, "data-slot": "searchbar-modal-input", className: className, children: [icon, _jsx(Input, { autoFocus: true, placeholder: placeholder }), kbd !== null && _jsx(Kbd, { children: kbd })] })));
33
+ ModalInput.displayName = "Searchbar.ModalInput";
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ export type SearchbarOverlayProps = {
3
+ className?: string;
4
+ asChild?: boolean;
5
+ } & React.HTMLAttributes<HTMLDivElement>;
6
+ /**
7
+ * Backdrop overlay for command palette / modal mode.
8
+ * Only renders when the search is open.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Searchbar.Portal>
13
+ * <Searchbar.Overlay className="fixed inset-0 bg-black/40" />
14
+ * <Searchbar.Content>…</Searchbar.Content>
15
+ * </Searchbar.Portal>
16
+ * ```
17
+ */
18
+ export declare const Overlay: React.ForwardRefExoticComponent<{
19
+ className?: string;
20
+ asChild?: boolean;
21
+ } & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;