@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.
- package/README.md +341 -205
- package/dist/Searchbar.d.ts +315 -0
- package/dist/Searchbar.js +207 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.js +32 -0
- package/dist/hooks/useEtoileSearch.d.ts +122 -0
- package/dist/hooks/useEtoileSearch.js +138 -0
- package/dist/index.d.ts +44 -19
- package/dist/index.js +37 -12
- package/dist/primitives/Content.d.ts +34 -0
- package/dist/primitives/Content.js +108 -0
- package/dist/primitives/Empty.d.ts +25 -0
- package/dist/primitives/Empty.js +25 -0
- package/dist/primitives/Error.d.ts +29 -0
- package/dist/primitives/Error.js +26 -0
- package/dist/primitives/Group.d.ts +30 -0
- package/dist/primitives/Group.js +22 -0
- package/dist/primitives/Icon.d.ts +21 -0
- package/dist/primitives/Icon.js +14 -0
- package/dist/primitives/Input.d.ts +32 -0
- package/dist/primitives/Input.js +70 -0
- package/dist/primitives/Item.d.ts +61 -0
- package/dist/primitives/Item.js +76 -0
- package/dist/primitives/Kbd.d.ts +20 -0
- package/dist/primitives/Kbd.js +13 -0
- package/dist/primitives/List.d.ts +35 -0
- package/dist/primitives/List.js +37 -0
- package/dist/primitives/Loading.d.ts +25 -0
- package/dist/primitives/Loading.js +26 -0
- package/dist/primitives/Modal.d.ts +39 -0
- package/dist/primitives/Modal.js +37 -0
- package/dist/primitives/ModalInput.d.ts +61 -0
- package/dist/primitives/ModalInput.js +33 -0
- package/dist/primitives/Overlay.d.ts +21 -0
- package/dist/primitives/Overlay.js +41 -0
- package/dist/primitives/Portal.d.ts +28 -0
- package/dist/primitives/Portal.js +30 -0
- package/dist/primitives/Root.d.ts +116 -0
- package/dist/primitives/Root.js +413 -0
- package/dist/primitives/Separator.d.ts +19 -0
- package/dist/primitives/Separator.js +18 -0
- package/dist/primitives/Thumbnail.d.ts +31 -0
- package/dist/primitives/Thumbnail.js +59 -0
- package/dist/primitives/Trigger.d.ts +28 -0
- package/dist/primitives/Trigger.js +35 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +63 -0
- package/dist/styles.css +480 -133
- package/dist/types.d.ts +3 -31
- package/dist/utils/composeRefs.d.ts +12 -0
- package/dist/utils/composeRefs.js +27 -0
- package/dist/utils/slot.d.ts +22 -0
- package/dist/utils/slot.js +58 -0
- package/package.json +8 -4
- package/dist/Search.d.ts +0 -37
- package/dist/Search.js +0 -31
- package/dist/components/SearchIcon.d.ts +0 -22
- package/dist/components/SearchIcon.js +0 -17
- package/dist/components/SearchInput.d.ts +0 -30
- package/dist/components/SearchInput.js +0 -59
- package/dist/components/SearchKbd.d.ts +0 -30
- package/dist/components/SearchKbd.js +0 -24
- package/dist/components/SearchResult.d.ts +0 -31
- package/dist/components/SearchResult.js +0 -40
- package/dist/components/SearchResultThumbnail.d.ts +0 -38
- package/dist/components/SearchResultThumbnail.js +0 -38
- package/dist/components/SearchResults.d.ts +0 -39
- package/dist/components/SearchResults.js +0 -53
- package/dist/components/SearchRoot.d.ts +0 -44
- package/dist/components/SearchRoot.js +0 -132
- package/dist/context/SearchContext.d.ts +0 -55
- package/dist/context/SearchContext.js +0 -36
- package/dist/hooks/useSearch.d.ts +0 -56
- 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";
|
package/dist/store.d.ts
ADDED
|
@@ -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
|
+
}
|