@etoile-dev/react 0.2.2 → 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 -205
  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 +8 -4
  55. package/dist/Search.d.ts +0 -37
  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,31 @@
1
+ import * as React from "react";
2
+ export type SearchbarThumbnailProps = {
3
+ /** Explicit image source. Defaults to `metadata.thumbnailUrl` from item context. */
4
+ src?: string;
5
+ /** Alt text. Defaults to item title from context. */
6
+ alt?: string;
7
+ /** Width and height in pixels (default: 40) */
8
+ size?: number;
9
+ className?: string;
10
+ } & Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height">;
11
+ /**
12
+ * Thumbnail image for a search result item.
13
+ *
14
+ * When used inside the Etoile `<Searchbar />` wrapper, automatically reads
15
+ * `metadata.thumbnailUrl` and the item title from context. Pass `src` and
16
+ * `alt` explicitly when using headless primitives.
17
+ *
18
+ * Returns null if no source is found.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Searchbar.Item value={result.id}>
23
+ * <Searchbar.Thumbnail src={result.thumbnailUrl} alt={result.title} />
24
+ * {result.title}
25
+ * </Searchbar.Item>
26
+ * ```
27
+ */
28
+ export declare const Thumbnail: {
29
+ ({ src, alt, size, className, ...props }: SearchbarThumbnailProps): import("react/jsx-runtime").JSX.Element | null;
30
+ displayName: string;
31
+ };
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { SearchbarItemDataContext } from "../context.js";
4
+ /**
5
+ * Thumbnail image for a search result item.
6
+ *
7
+ * When used inside the Etoile `<Searchbar />` wrapper, automatically reads
8
+ * `metadata.thumbnailUrl` and the item title from context. Pass `src` and
9
+ * `alt` explicitly when using headless primitives.
10
+ *
11
+ * Returns null if no source is found.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Searchbar.Item value={result.id}>
16
+ * <Searchbar.Thumbnail src={result.thumbnailUrl} alt={result.title} />
17
+ * {result.title}
18
+ * </Searchbar.Item>
19
+ * ```
20
+ */
21
+ export const Thumbnail = ({ src, alt, size = 40, className, ...props }) => {
22
+ const itemData = React.useContext(SearchbarItemDataContext);
23
+ const metadata = itemData?.metadata;
24
+ const imageSrc = src ??
25
+ readImageSource(metadata, [
26
+ "thumbnailUrl",
27
+ "thumbnail_url",
28
+ "thumbnail",
29
+ "image",
30
+ "imageUrl",
31
+ "image_url",
32
+ "cover",
33
+ "coverUrl",
34
+ "cover_url",
35
+ "artwork",
36
+ "artworkUrl",
37
+ "artwork_url",
38
+ ]);
39
+ const imageAlt = alt ?? itemData?.title ?? "";
40
+ if (!imageSrc)
41
+ return null;
42
+ return (_jsx("img", { ...props, src: imageSrc, alt: imageAlt, width: size, height: size, className: className, draggable: false }));
43
+ };
44
+ Thumbnail.displayName = "Searchbar.Thumbnail";
45
+ const readImageSource = (metadata, keys) => {
46
+ if (!metadata)
47
+ return undefined;
48
+ for (const key of keys) {
49
+ const value = metadata[key];
50
+ if (typeof value === "string" && value.trim() !== "")
51
+ return value;
52
+ if (value && typeof value === "object") {
53
+ const nestedUrl = value.url;
54
+ if (typeof nestedUrl === "string" && nestedUrl.trim() !== "")
55
+ return nestedUrl;
56
+ }
57
+ }
58
+ return undefined;
59
+ };
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ export type SearchbarTriggerProps = {
3
+ children?: React.ReactNode;
4
+ className?: string;
5
+ asChild?: boolean;
6
+ } & React.ButtonHTMLAttributes<HTMLButtonElement>;
7
+ /**
8
+ * Button that toggles the search open/closed state.
9
+ * Designed for command palette / modal mode.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <Searchbar.Root>
14
+ * <Searchbar.Trigger>
15
+ * <Searchbar.Icon /> Search
16
+ * </Searchbar.Trigger>
17
+ * <Searchbar.Portal>
18
+ * <Searchbar.Overlay />
19
+ * <Searchbar.Content>…</Searchbar.Content>
20
+ * </Searchbar.Portal>
21
+ * </Searchbar.Root>
22
+ * ```
23
+ */
24
+ export declare const Trigger: React.ForwardRefExoticComponent<{
25
+ children?: React.ReactNode;
26
+ className?: string;
27
+ asChild?: boolean;
28
+ } & React.ButtonHTMLAttributes<HTMLButtonElement> & React.RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,35 @@
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
+ import { useComposeRefs } from "../utils/composeRefs.js";
6
+ /**
7
+ * Button that toggles the search open/closed state.
8
+ * Designed for command palette / modal mode.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Searchbar.Root>
13
+ * <Searchbar.Trigger>
14
+ * <Searchbar.Icon /> Search
15
+ * </Searchbar.Trigger>
16
+ * <Searchbar.Portal>
17
+ * <Searchbar.Overlay />
18
+ * <Searchbar.Content>…</Searchbar.Content>
19
+ * </Searchbar.Portal>
20
+ * </Searchbar.Root>
21
+ * ```
22
+ */
23
+ export const Trigger = React.forwardRef(({ children, className, asChild = false, ...props }, forwardedRef) => {
24
+ const { store, triggerRef } = useSearchbarContext();
25
+ const isOpen = useSearchbarStore(store, (s) => s.open);
26
+ const composedRef = useComposeRefs(triggerRef, forwardedRef);
27
+ const Comp = asChild ? Slot : "button";
28
+ return (_jsx(Comp, { ...props, ref: composedRef, type: asChild ? undefined : "button", "aria-expanded": isOpen, "aria-haspopup": "listbox", className: className, "data-state": isOpen ? "open" : "closed", "data-slot": "searchbar-trigger", onClick: (event) => {
29
+ props.onClick?.(event);
30
+ if (event.defaultPrevented)
31
+ return;
32
+ store.setState((s) => ({ ...s, open: !s.open }));
33
+ }, children: children }));
34
+ });
35
+ Trigger.displayName = "Searchbar.Trigger";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SearchbarStore — external state container for the Searchbar primitives.
3
+ *
4
+ * Uses the subscribe/getSnapshot pattern compatible with useSyncExternalStore,
5
+ * so each component can subscribe to exactly the slice of state it needs and
6
+ * avoid re-rendering on unrelated updates (e.g. a keystroke should not force
7
+ * every item to re-render).
8
+ */
9
+ export type ItemMeta = {
10
+ /** Stable identifier matching the `value` prop on Searchbar.Item */
11
+ value: string;
12
+ /** Optional item label (consumer-defined, useful for analytics/debugging). */
13
+ label: string;
14
+ disabled: boolean;
15
+ node: HTMLElement | null;
16
+ /** Per-item select handler — called in addition to root onSelect */
17
+ onSelect?: (value: string) => void;
18
+ };
19
+ export type SearchbarState = {
20
+ query: string;
21
+ open: boolean;
22
+ selectedValue: string | null;
23
+ isLoading: boolean;
24
+ error: unknown;
25
+ items: Map<string, ItemMeta>;
26
+ /** Registration order — determines keyboard navigation sequence */
27
+ sortedValues: string[];
28
+ /** Subset of enabled items currently rendered in navigation order */
29
+ filteredValues: string[];
30
+ /** Set mirror of filteredValues for O(1) lookup by Item primitives */
31
+ filteredSet: Set<string>;
32
+ };
33
+ export type SearchbarStore = {
34
+ getState: () => SearchbarState;
35
+ setState: (updater: (prev: SearchbarState) => SearchbarState) => void;
36
+ subscribe: (listener: () => void) => () => void;
37
+ };
38
+ export declare function createSearchbarStore(initial?: Partial<Pick<SearchbarState, "query" | "open" | "selectedValue" | "isLoading">>): SearchbarStore;
package/dist/store.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SearchbarStore — external state container for the Searchbar primitives.
3
+ *
4
+ * Uses the subscribe/getSnapshot pattern compatible with useSyncExternalStore,
5
+ * so each component can subscribe to exactly the slice of state it needs and
6
+ * avoid re-rendering on unrelated updates (e.g. a keystroke should not force
7
+ * every item to re-render).
8
+ */
9
+ function deriveFiltered(state) {
10
+ const base = state.sortedValues.filter((v) => {
11
+ const item = state.items.get(v);
12
+ return item && !item.disabled;
13
+ });
14
+ return { filteredValues: base, filteredSet: new Set(base) };
15
+ }
16
+ export function createSearchbarStore(initial = {}) {
17
+ let state = {
18
+ query: "",
19
+ open: false,
20
+ selectedValue: null,
21
+ isLoading: false,
22
+ error: null,
23
+ items: new Map(),
24
+ sortedValues: [],
25
+ filteredValues: [],
26
+ filteredSet: new Set(),
27
+ ...initial,
28
+ };
29
+ // Compute initial derived state
30
+ const derived = deriveFiltered(state);
31
+ state = { ...state, ...derived };
32
+ const listeners = new Set();
33
+ const emit = () => listeners.forEach((fn) => fn());
34
+ const shallowEqual = (a, b) => a.query === b.query &&
35
+ a.open === b.open &&
36
+ a.selectedValue === b.selectedValue &&
37
+ a.isLoading === b.isLoading &&
38
+ a.error === b.error &&
39
+ a.items === b.items &&
40
+ a.sortedValues === b.sortedValues &&
41
+ a.filteredValues === b.filteredValues;
42
+ return {
43
+ getState: () => state,
44
+ setState: (updater) => {
45
+ const prev = state;
46
+ const next = updater(prev);
47
+ const needsRefilter = next.query !== prev.query ||
48
+ next.sortedValues !== prev.sortedValues ||
49
+ next.items !== prev.items;
50
+ const nextState = needsRefilter
51
+ ? { ...next, ...deriveFiltered(next) }
52
+ : next;
53
+ if (shallowEqual(state, nextState))
54
+ return;
55
+ state = nextState;
56
+ emit();
57
+ },
58
+ subscribe: (fn) => {
59
+ listeners.add(fn);
60
+ return () => listeners.delete(fn);
61
+ },
62
+ };
63
+ }