@particle-academy/react-fancy 1.6.1 → 1.7.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/dist/index.cjs CHANGED
@@ -1924,10 +1924,157 @@ var Textarea = react.forwardRef(
1924
1924
  }
1925
1925
  );
1926
1926
  Textarea.displayName = "Textarea";
1927
+ function Portal({ children, container }) {
1928
+ if (typeof document === "undefined") return null;
1929
+ const target = container ?? document.body;
1930
+ return reactDom.createPortal(
1931
+ /* @__PURE__ */ jsxRuntime.jsx(PortalDarkWrapper, { children }),
1932
+ target
1933
+ );
1934
+ }
1935
+ function PortalDarkWrapper({ children }) {
1936
+ const ref = react.useRef(null);
1937
+ react.useEffect(() => {
1938
+ const root = document.documentElement;
1939
+ const wrapper = ref.current;
1940
+ if (!wrapper) return;
1941
+ const sync = () => {
1942
+ const isDark = root.classList.contains("dark") || root.getAttribute("data-theme") === "dark";
1943
+ wrapper.classList.toggle("dark", isDark);
1944
+ };
1945
+ sync();
1946
+ const observer = new MutationObserver(sync);
1947
+ observer.observe(root, {
1948
+ attributes: true,
1949
+ attributeFilter: ["class", "data-theme"]
1950
+ });
1951
+ return () => observer.disconnect();
1952
+ }, []);
1953
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, "data-react-fancy-portal": "", style: { display: "contents" }, children });
1954
+ }
1955
+ function getPosition(anchor, floating, placement, offset) {
1956
+ let x = 0;
1957
+ let y = 0;
1958
+ const base = placement.split("-")[0];
1959
+ const align = placement.split("-")[1];
1960
+ switch (base) {
1961
+ case "top":
1962
+ x = anchor.left + anchor.width / 2 - floating.width / 2;
1963
+ y = anchor.top - floating.height - offset;
1964
+ break;
1965
+ case "bottom":
1966
+ x = anchor.left + anchor.width / 2 - floating.width / 2;
1967
+ y = anchor.bottom + offset;
1968
+ break;
1969
+ case "left":
1970
+ x = anchor.left - floating.width - offset;
1971
+ y = anchor.top + anchor.height / 2 - floating.height / 2;
1972
+ break;
1973
+ case "right":
1974
+ x = anchor.right + offset;
1975
+ y = anchor.top + anchor.height / 2 - floating.height / 2;
1976
+ break;
1977
+ }
1978
+ if (base === "top" || base === "bottom") {
1979
+ if (align === "start") x = anchor.left;
1980
+ else if (align === "end") x = anchor.right - floating.width;
1981
+ }
1982
+ if (base === "left" || base === "right") {
1983
+ if (align === "start") y = anchor.top;
1984
+ else if (align === "end") y = anchor.bottom - floating.height;
1985
+ }
1986
+ let finalPlacement = placement;
1987
+ const vw = window.innerWidth;
1988
+ const vh = window.innerHeight;
1989
+ if (base === "bottom" && y + floating.height > vh) {
1990
+ y = anchor.top - floating.height - offset;
1991
+ finalPlacement = placement.replace("bottom", "top");
1992
+ } else if (base === "top" && y < 0) {
1993
+ y = anchor.bottom + offset;
1994
+ finalPlacement = placement.replace("top", "bottom");
1995
+ }
1996
+ x = Math.max(4, Math.min(x, vw - floating.width - 4));
1997
+ y = Math.max(4, Math.min(y, vh - floating.height - 4));
1998
+ return { x, y, placement: finalPlacement };
1999
+ }
2000
+ function useFloatingPosition(anchorRef, floatingRef, options = {}) {
2001
+ const { placement = "bottom", offset = 8, enabled = true } = options;
2002
+ const [position, setPosition] = react.useState({
2003
+ x: -9999,
2004
+ y: -9999,
2005
+ placement
2006
+ });
2007
+ const update = react.useCallback(() => {
2008
+ const anchor = anchorRef.current;
2009
+ const floating = floatingRef.current;
2010
+ if (!anchor || !floating) return;
2011
+ const anchorRect = anchor.getBoundingClientRect();
2012
+ const floatingRect = floating.getBoundingClientRect();
2013
+ setPosition(getPosition(anchorRect, floatingRect, placement, offset));
2014
+ }, [anchorRef, floatingRef, placement, offset]);
2015
+ react.useLayoutEffect(() => {
2016
+ if (!enabled) return;
2017
+ update();
2018
+ const raf = requestAnimationFrame(() => {
2019
+ update();
2020
+ });
2021
+ return () => cancelAnimationFrame(raf);
2022
+ }, [update, enabled]);
2023
+ react.useEffect(() => {
2024
+ if (!enabled) return;
2025
+ window.addEventListener("scroll", update, true);
2026
+ window.addEventListener("resize", update);
2027
+ return () => {
2028
+ window.removeEventListener("scroll", update, true);
2029
+ window.removeEventListener("resize", update);
2030
+ };
2031
+ }, [update, enabled]);
2032
+ return position;
2033
+ }
2034
+ function useOutsideClick(ref, handler, enabled = true, ignoreRef) {
2035
+ react.useEffect(() => {
2036
+ if (!enabled) return;
2037
+ const listener = (event) => {
2038
+ const el = ref.current;
2039
+ if (!el || el.contains(event.target)) return;
2040
+ if (ignoreRef?.current?.contains(event.target)) return;
2041
+ handler(event);
2042
+ };
2043
+ document.addEventListener("mousedown", listener);
2044
+ document.addEventListener("touchstart", listener);
2045
+ return () => {
2046
+ document.removeEventListener("mousedown", listener);
2047
+ document.removeEventListener("touchstart", listener);
2048
+ };
2049
+ }, [ref, handler, enabled, ignoreRef]);
2050
+ }
2051
+ function useEscapeKey(handler, enabled = true) {
2052
+ react.useEffect(() => {
2053
+ if (!enabled) return;
2054
+ const listener = (event) => {
2055
+ if (event.key === "Escape") {
2056
+ handler();
2057
+ }
2058
+ };
2059
+ document.addEventListener("keydown", listener);
2060
+ return () => document.removeEventListener("keydown", listener);
2061
+ }, [handler, enabled]);
2062
+ }
1927
2063
  function isOptionGroup(item) {
1928
2064
  return typeof item === "object" && "options" in item;
1929
2065
  }
1930
- function renderOption(option, index) {
2066
+ function flattenOptions(list) {
2067
+ const flat = [];
2068
+ for (const item of list) {
2069
+ if (isOptionGroup(item)) {
2070
+ flat.push(...item.options);
2071
+ } else {
2072
+ flat.push(item);
2073
+ }
2074
+ }
2075
+ return flat;
2076
+ }
2077
+ function renderNativeOption(option, index) {
1931
2078
  const resolved = resolveOption(option);
1932
2079
  return /* @__PURE__ */ jsxRuntime.jsx(
1933
2080
  "option",
@@ -1939,7 +2086,7 @@ function renderOption(option, index) {
1939
2086
  `${resolved.value}-${index}`
1940
2087
  );
1941
2088
  }
1942
- var Select = react.forwardRef(
2089
+ var NativeSelect = react.forwardRef(
1943
2090
  ({
1944
2091
  size = "md",
1945
2092
  dirty,
@@ -1997,8 +2144,8 @@ var Select = react.forwardRef(
1997
2144
  placeholder && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder }),
1998
2145
  list.map(
1999
2146
  (item, index) => isOptionGroup(item) ? /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: item.label, children: item.options.map(
2000
- (opt, optIndex) => renderOption(opt, optIndex)
2001
- ) }, `group-${index}`) : renderOption(item, index)
2147
+ (opt, optIndex) => renderNativeOption(opt, optIndex)
2148
+ ) }, `group-${index}`) : renderNativeOption(item, index)
2002
2149
  )
2003
2150
  ]
2004
2151
  }
@@ -2022,6 +2169,270 @@ var Select = react.forwardRef(
2022
2169
  return select;
2023
2170
  }
2024
2171
  );
2172
+ NativeSelect.displayName = "NativeSelect";
2173
+ var ListboxSelect = react.forwardRef(
2174
+ ({
2175
+ size = "md",
2176
+ dirty,
2177
+ error,
2178
+ label,
2179
+ description,
2180
+ required,
2181
+ disabled,
2182
+ className,
2183
+ id,
2184
+ list,
2185
+ placeholder = "Select...",
2186
+ multiple = false,
2187
+ values: controlledValues,
2188
+ defaultValues,
2189
+ onValueChange,
2190
+ onValuesChange,
2191
+ searchable = false,
2192
+ selectedSuffix = "selected",
2193
+ indicator = "check",
2194
+ value: controlledSingleValue,
2195
+ defaultValue: defaultSingleValue
2196
+ }, _ref) => {
2197
+ const autoId = react.useId();
2198
+ const selectId = id ?? autoId;
2199
+ const [open, setOpen] = react.useState(false);
2200
+ const [search2, setSearch] = react.useState("");
2201
+ const [activeIndex, setActiveIndex] = react.useState(-1);
2202
+ const [singleValue, setSingleValue] = react.useState(
2203
+ controlledSingleValue ?? defaultSingleValue ?? ""
2204
+ );
2205
+ const currentSingle = controlledSingleValue ?? singleValue;
2206
+ const [multiValues, setMultiValues] = react.useState(
2207
+ controlledValues ?? defaultValues ?? []
2208
+ );
2209
+ const currentMulti = controlledValues ?? multiValues;
2210
+ react.useEffect(() => {
2211
+ if (controlledValues) setMultiValues(controlledValues);
2212
+ }, [controlledValues]);
2213
+ react.useEffect(() => {
2214
+ if (controlledSingleValue !== void 0) setSingleValue(controlledSingleValue);
2215
+ }, [controlledSingleValue]);
2216
+ const anchorRef = react.useRef(null);
2217
+ const listRef = react.useRef(null);
2218
+ const wrapperRef = react.useRef(null);
2219
+ const searchRef = react.useRef(null);
2220
+ const position = useFloatingPosition(anchorRef, listRef, {
2221
+ placement: "bottom-start",
2222
+ offset: 4,
2223
+ enabled: open
2224
+ });
2225
+ const close = react.useCallback(() => {
2226
+ setOpen(false);
2227
+ setSearch("");
2228
+ setActiveIndex(-1);
2229
+ }, []);
2230
+ useOutsideClick(wrapperRef, close, open);
2231
+ useEscapeKey(close, open);
2232
+ react.useEffect(() => {
2233
+ if (open && searchable) {
2234
+ requestAnimationFrame(() => searchRef.current?.focus());
2235
+ }
2236
+ }, [open, searchable]);
2237
+ const allOptions = flattenOptions(list);
2238
+ const resolvedOptions = allOptions.map(resolveOption);
2239
+ const filtered = search2 ? resolvedOptions.filter(
2240
+ (o) => o.label.toLowerCase().includes(search2.toLowerCase())
2241
+ ) : resolvedOptions;
2242
+ const isSelected = (value) => {
2243
+ if (multiple) return currentMulti.includes(value);
2244
+ return currentSingle === value;
2245
+ };
2246
+ const toggleOption = react.useCallback(
2247
+ (value) => {
2248
+ if (multiple) {
2249
+ const next = currentMulti.includes(value) ? currentMulti.filter((v) => v !== value) : [...currentMulti, value];
2250
+ setMultiValues(next);
2251
+ onValuesChange?.(next);
2252
+ } else {
2253
+ setSingleValue(value);
2254
+ onValueChange?.(value);
2255
+ close();
2256
+ }
2257
+ },
2258
+ [multiple, currentMulti, onValuesChange, onValueChange, close]
2259
+ );
2260
+ const getDisplayText = () => {
2261
+ if (multiple) {
2262
+ if (currentMulti.length === 0) return placeholder;
2263
+ if (currentMulti.length === 1) {
2264
+ const opt2 = resolvedOptions.find((o) => o.value === currentMulti[0]);
2265
+ return opt2?.label ?? currentMulti[0];
2266
+ }
2267
+ return `${currentMulti.length} ${selectedSuffix}`;
2268
+ }
2269
+ if (!currentSingle) return placeholder;
2270
+ const opt = resolvedOptions.find((o) => o.value === currentSingle);
2271
+ return opt?.label ?? currentSingle;
2272
+ };
2273
+ const hasValue = multiple ? currentMulti.length > 0 : !!currentSingle;
2274
+ const handleKeyDown = (e) => {
2275
+ if (!open) {
2276
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
2277
+ e.preventDefault();
2278
+ setOpen(true);
2279
+ }
2280
+ return;
2281
+ }
2282
+ if (e.key === "ArrowDown") {
2283
+ e.preventDefault();
2284
+ setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
2285
+ } else if (e.key === "ArrowUp") {
2286
+ e.preventDefault();
2287
+ setActiveIndex((i) => Math.max(i - 1, 0));
2288
+ } else if (e.key === "Enter" && activeIndex >= 0) {
2289
+ e.preventDefault();
2290
+ const item = filtered[activeIndex];
2291
+ if (item && !item.disabled) toggleOption(item.value);
2292
+ }
2293
+ };
2294
+ const trigger = /* @__PURE__ */ jsxRuntime.jsxs(
2295
+ "button",
2296
+ {
2297
+ ref: anchorRef,
2298
+ type: "button",
2299
+ id: selectId,
2300
+ disabled,
2301
+ onClick: () => setOpen((o) => !o),
2302
+ onKeyDown: handleKeyDown,
2303
+ role: "combobox",
2304
+ "aria-expanded": open,
2305
+ "aria-haspopup": "listbox",
2306
+ "data-react-fancy-select": "",
2307
+ "data-variant": "listbox",
2308
+ className: cn(
2309
+ inputBaseClasses,
2310
+ inputSizeClasses[size],
2311
+ dirtyClasses(dirty),
2312
+ errorClasses(error),
2313
+ "flex w-full cursor-pointer items-center justify-between gap-2 text-left",
2314
+ !hasValue && "text-zinc-400 dark:text-zinc-500",
2315
+ className
2316
+ ),
2317
+ children: [
2318
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: getDisplayText() }),
2319
+ /* @__PURE__ */ jsxRuntime.jsx(
2320
+ "svg",
2321
+ {
2322
+ className: cn("h-4 w-4 shrink-0 text-zinc-400 transition-transform", open && "rotate-180"),
2323
+ viewBox: "0 0 20 20",
2324
+ fill: "currentColor",
2325
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2326
+ "path",
2327
+ {
2328
+ fillRule: "evenodd",
2329
+ d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z",
2330
+ clipRule: "evenodd"
2331
+ }
2332
+ )
2333
+ }
2334
+ )
2335
+ ]
2336
+ }
2337
+ );
2338
+ const dropdown = open && /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(
2339
+ "div",
2340
+ {
2341
+ ref: listRef,
2342
+ role: "listbox",
2343
+ "aria-multiselectable": multiple || void 0,
2344
+ className: "fixed z-50 max-h-60 min-w-[8rem] overflow-y-auto rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-zinc-950/50 fancy-scale-in",
2345
+ style: {
2346
+ left: position.x,
2347
+ top: position.y,
2348
+ width: anchorRef.current?.offsetWidth
2349
+ },
2350
+ children: [
2351
+ searchable && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 pb-1", children: /* @__PURE__ */ jsxRuntime.jsx(
2352
+ "input",
2353
+ {
2354
+ ref: searchRef,
2355
+ type: "text",
2356
+ value: search2,
2357
+ onChange: (e) => {
2358
+ setSearch(e.target.value);
2359
+ setActiveIndex(-1);
2360
+ },
2361
+ onKeyDown: handleKeyDown,
2362
+ placeholder: "Search...",
2363
+ className: "w-full rounded-md border-0 bg-zinc-100 px-2.5 py-1.5 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder:text-zinc-500"
2364
+ }
2365
+ ) }),
2366
+ filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-zinc-400", children: "No results found" }) : filtered.map((option, i) => {
2367
+ const selected = isSelected(option.value);
2368
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2369
+ "button",
2370
+ {
2371
+ type: "button",
2372
+ role: "option",
2373
+ "aria-selected": selected,
2374
+ disabled: option.disabled,
2375
+ onClick: () => toggleOption(option.value),
2376
+ className: cn(
2377
+ "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors",
2378
+ i === activeIndex ? "bg-zinc-100 dark:bg-zinc-800" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/50",
2379
+ selected ? "text-zinc-900 dark:text-zinc-100" : "text-zinc-700 dark:text-zinc-300",
2380
+ option.disabled && "cursor-not-allowed opacity-50"
2381
+ ),
2382
+ children: [
2383
+ indicator === "checkbox" ? /* @__PURE__ */ jsxRuntime.jsx(
2384
+ "span",
2385
+ {
2386
+ className: cn(
2387
+ "flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
2388
+ selected ? "border-blue-500 bg-blue-500 text-white dark:border-blue-400 dark:bg-blue-400" : "border-zinc-300 dark:border-zinc-600"
2389
+ ),
2390
+ children: selected && /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-3 w-3", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2.5 6 5 8.5 9.5 3.5" }) })
2391
+ }
2392
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: selected && /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-4 w-4 text-blue-500 dark:text-blue-400", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z", clipRule: "evenodd" }) }) }),
2393
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1", children: [
2394
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate", children: option.label }),
2395
+ option.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-xs text-zinc-400 dark:text-zinc-500", children: option.description })
2396
+ ] })
2397
+ ]
2398
+ },
2399
+ option.value
2400
+ );
2401
+ })
2402
+ ]
2403
+ }
2404
+ ) });
2405
+ const content = /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapperRef, className: "relative", children: [
2406
+ trigger,
2407
+ dropdown
2408
+ ] });
2409
+ if (label || error || description) {
2410
+ return /* @__PURE__ */ jsxRuntime.jsx(
2411
+ Field,
2412
+ {
2413
+ label,
2414
+ description,
2415
+ error,
2416
+ required,
2417
+ htmlFor: selectId,
2418
+ size,
2419
+ children: content
2420
+ }
2421
+ );
2422
+ }
2423
+ return content;
2424
+ }
2425
+ );
2426
+ ListboxSelect.displayName = "ListboxSelect";
2427
+ var Select = react.forwardRef(
2428
+ (props, ref) => {
2429
+ const variant = props.variant ?? (props.multiple ? "listbox" : "native");
2430
+ if (variant === "listbox") {
2431
+ return /* @__PURE__ */ jsxRuntime.jsx(ListboxSelect, { ...props, ref });
2432
+ }
2433
+ return /* @__PURE__ */ jsxRuntime.jsx(NativeSelect, { ...props, ref });
2434
+ }
2435
+ );
2025
2436
  Select.displayName = "Select";
2026
2437
  function useControllableState(controlledValue, defaultValue, onChange) {
2027
2438
  const [uncontrolledValue, setUncontrolledValue] = react.useState(defaultValue);
@@ -3690,34 +4101,6 @@ var Table = Object.assign(TableRoot, {
3690
4101
  Tray: TableTray,
3691
4102
  RowTray: TableRowTray
3692
4103
  });
3693
- function Portal({ children, container }) {
3694
- if (typeof document === "undefined") return null;
3695
- const target = container ?? document.body;
3696
- return reactDom.createPortal(
3697
- /* @__PURE__ */ jsxRuntime.jsx(PortalDarkWrapper, { children }),
3698
- target
3699
- );
3700
- }
3701
- function PortalDarkWrapper({ children }) {
3702
- const ref = react.useRef(null);
3703
- react.useEffect(() => {
3704
- const root = document.documentElement;
3705
- const wrapper = ref.current;
3706
- if (!wrapper) return;
3707
- const sync = () => {
3708
- const isDark = root.classList.contains("dark") || root.getAttribute("data-theme") === "dark";
3709
- wrapper.classList.toggle("dark", isDark);
3710
- };
3711
- sync();
3712
- const observer = new MutationObserver(sync);
3713
- observer.observe(root, {
3714
- attributes: true,
3715
- attributeFilter: ["class", "data-theme"]
3716
- });
3717
- return () => observer.disconnect();
3718
- }, []);
3719
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, "data-react-fancy-portal": "", style: { display: "contents" }, children });
3720
- }
3721
4104
  var sizeClasses3 = {
3722
4105
  xs: "text-xs",
3723
4106
  sm: "text-sm",
@@ -4759,85 +5142,6 @@ var Timeline = Object.assign(TimelineRoot, {
4759
5142
  Item: TimelineItem,
4760
5143
  Block: TimelineBlock
4761
5144
  });
4762
- function getPosition(anchor, floating, placement, offset) {
4763
- let x = 0;
4764
- let y = 0;
4765
- const base = placement.split("-")[0];
4766
- const align = placement.split("-")[1];
4767
- switch (base) {
4768
- case "top":
4769
- x = anchor.left + anchor.width / 2 - floating.width / 2;
4770
- y = anchor.top - floating.height - offset;
4771
- break;
4772
- case "bottom":
4773
- x = anchor.left + anchor.width / 2 - floating.width / 2;
4774
- y = anchor.bottom + offset;
4775
- break;
4776
- case "left":
4777
- x = anchor.left - floating.width - offset;
4778
- y = anchor.top + anchor.height / 2 - floating.height / 2;
4779
- break;
4780
- case "right":
4781
- x = anchor.right + offset;
4782
- y = anchor.top + anchor.height / 2 - floating.height / 2;
4783
- break;
4784
- }
4785
- if (base === "top" || base === "bottom") {
4786
- if (align === "start") x = anchor.left;
4787
- else if (align === "end") x = anchor.right - floating.width;
4788
- }
4789
- if (base === "left" || base === "right") {
4790
- if (align === "start") y = anchor.top;
4791
- else if (align === "end") y = anchor.bottom - floating.height;
4792
- }
4793
- let finalPlacement = placement;
4794
- const vw = window.innerWidth;
4795
- const vh = window.innerHeight;
4796
- if (base === "bottom" && y + floating.height > vh) {
4797
- y = anchor.top - floating.height - offset;
4798
- finalPlacement = placement.replace("bottom", "top");
4799
- } else if (base === "top" && y < 0) {
4800
- y = anchor.bottom + offset;
4801
- finalPlacement = placement.replace("top", "bottom");
4802
- }
4803
- x = Math.max(4, Math.min(x, vw - floating.width - 4));
4804
- y = Math.max(4, Math.min(y, vh - floating.height - 4));
4805
- return { x, y, placement: finalPlacement };
4806
- }
4807
- function useFloatingPosition(anchorRef, floatingRef, options = {}) {
4808
- const { placement = "bottom", offset = 8, enabled = true } = options;
4809
- const [position, setPosition] = react.useState({
4810
- x: -9999,
4811
- y: -9999,
4812
- placement
4813
- });
4814
- const update = react.useCallback(() => {
4815
- const anchor = anchorRef.current;
4816
- const floating = floatingRef.current;
4817
- if (!anchor || !floating) return;
4818
- const anchorRect = anchor.getBoundingClientRect();
4819
- const floatingRect = floating.getBoundingClientRect();
4820
- setPosition(getPosition(anchorRect, floatingRect, placement, offset));
4821
- }, [anchorRef, floatingRef, placement, offset]);
4822
- react.useLayoutEffect(() => {
4823
- if (!enabled) return;
4824
- update();
4825
- const raf = requestAnimationFrame(() => {
4826
- update();
4827
- });
4828
- return () => cancelAnimationFrame(raf);
4829
- }, [update, enabled]);
4830
- react.useEffect(() => {
4831
- if (!enabled) return;
4832
- window.addEventListener("scroll", update, true);
4833
- window.addEventListener("resize", update);
4834
- return () => {
4835
- window.removeEventListener("scroll", update, true);
4836
- window.removeEventListener("resize", update);
4837
- };
4838
- }, [update, enabled]);
4839
- return position;
4840
- }
4841
5145
  var Tooltip = react.forwardRef(
4842
5146
  function Tooltip2({ children, content, placement = "top", delay = 200, offset = 8, className }, _ref) {
4843
5147
  const [open, setOpen] = react.useState(false);
@@ -4905,78 +5209,26 @@ function usePopover() {
4905
5209
  }
4906
5210
  return ctx;
4907
5211
  }
4908
- function PopoverTrigger({ children }) {
4909
- const { setOpen, open, anchorRef } = usePopover();
4910
- return react.cloneElement(children, {
4911
- ref: anchorRef,
4912
- onClick: () => setOpen(!open),
4913
- "aria-expanded": open,
4914
- "aria-haspopup": true
4915
- });
4916
- }
4917
- PopoverTrigger.displayName = "PopoverTrigger";
4918
- function useOutsideClick(ref, handler, enabled = true, ignoreRef) {
4919
- react.useEffect(() => {
4920
- if (!enabled) return;
4921
- const listener = (event) => {
4922
- const el = ref.current;
4923
- if (!el || el.contains(event.target)) return;
4924
- if (ignoreRef?.current?.contains(event.target)) return;
4925
- handler(event);
4926
- };
4927
- document.addEventListener("mousedown", listener);
4928
- document.addEventListener("touchstart", listener);
4929
- return () => {
4930
- document.removeEventListener("mousedown", listener);
4931
- document.removeEventListener("touchstart", listener);
4932
- };
4933
- }, [ref, handler, enabled, ignoreRef]);
4934
- }
4935
- function useEscapeKey(handler, enabled = true) {
4936
- react.useEffect(() => {
4937
- if (!enabled) return;
4938
- const listener = (event) => {
4939
- if (event.key === "Escape") {
4940
- handler();
4941
- }
4942
- };
4943
- document.addEventListener("keydown", listener);
4944
- return () => document.removeEventListener("keydown", listener);
4945
- }, [handler, enabled]);
4946
- }
4947
- function useAnimation({
4948
- open,
4949
- enterClass,
4950
- exitClass
4951
- }) {
4952
- const [mounted, setMounted] = react.useState(open);
4953
- const [animClass, setAnimClass] = react.useState(open ? enterClass : "");
4954
- const ref = react.useRef(null);
4955
- react.useEffect(() => {
4956
- if (open) {
4957
- setMounted(true);
4958
- setAnimClass(enterClass);
4959
- } else if (mounted) {
4960
- setAnimClass(exitClass);
4961
- }
4962
- }, [open, enterClass, exitClass, mounted]);
4963
- const handleAnimationEnd = react.useCallback(() => {
4964
- if (!open) {
4965
- setMounted(false);
4966
- setAnimClass("");
5212
+ function PopoverTrigger({ children, className }) {
5213
+ const { setOpen, open, anchorRef, hover, onHoverEnter, onHoverLeave } = usePopover();
5214
+ return /* @__PURE__ */ jsxRuntime.jsx(
5215
+ "span",
5216
+ {
5217
+ ref: anchorRef,
5218
+ "data-react-fancy-popover-trigger": "",
5219
+ className: cn("inline-flex", className),
5220
+ onClick: hover ? void 0 : () => setOpen(!open),
5221
+ onMouseEnter: hover ? onHoverEnter : void 0,
5222
+ onMouseLeave: hover ? onHoverLeave : void 0,
5223
+ "aria-expanded": open,
5224
+ "aria-haspopup": true,
5225
+ children
4967
5226
  }
4968
- }, [open]);
4969
- react.useEffect(() => {
4970
- const el = ref.current;
4971
- if (!el) return;
4972
- el.addEventListener("animationend", handleAnimationEnd);
4973
- return () => el.removeEventListener("animationend", handleAnimationEnd);
4974
- }, [handleAnimationEnd, mounted]);
4975
- return { mounted, className: animClass, ref };
5227
+ );
4976
5228
  }
5229
+ PopoverTrigger.displayName = "PopoverTrigger";
4977
5230
  function PopoverContent({ children, className }) {
4978
- const { open, setOpen, anchorRef, placement, offset } = usePopover();
4979
- const floatingRef = react.useRef(null);
5231
+ const { open, setOpen, anchorRef, floatingRef, placement, offset, hover, onHoverEnter, onHoverLeave } = usePopover();
4980
5232
  const outsideRef = react.useRef(null);
4981
5233
  const position = useFloatingPosition(anchorRef, floatingRef, {
4982
5234
  placement,
@@ -4984,29 +5236,26 @@ function PopoverContent({ children, className }) {
4984
5236
  enabled: open
4985
5237
  });
4986
5238
  const close = react.useCallback(() => setOpen(false), [setOpen]);
4987
- useOutsideClick(outsideRef, close, open, anchorRef);
5239
+ useOutsideClick(outsideRef, close, open && !hover, anchorRef);
4988
5240
  useEscapeKey(close, open);
4989
- const { mounted, className: animClass, ref: animRef } = useAnimation({
4990
- open,
4991
- enterClass: "fancy-scale-in",
4992
- exitClass: "fancy-fade-out"
4993
- });
4994
- if (!mounted) return null;
5241
+ const positioned = position.x !== -9999 && position.y !== -9999;
5242
+ if (!open) return null;
4995
5243
  return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
4996
5244
  "div",
4997
5245
  {
4998
5246
  ref: (node) => {
4999
5247
  outsideRef.current = node;
5000
5248
  floatingRef.current = node;
5001
- animRef.current = node;
5002
5249
  },
5003
5250
  "data-react-fancy-popover": "",
5004
5251
  className: cn(
5005
5252
  "fixed z-50 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-700 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:shadow-zinc-950/50",
5006
- animClass,
5253
+ positioned ? "fancy-scale-in" : "invisible",
5007
5254
  className
5008
5255
  ),
5009
5256
  style: { left: position.x, top: position.y },
5257
+ onMouseEnter: hover ? onHoverEnter : void 0,
5258
+ onMouseLeave: hover ? onHoverLeave : void 0,
5010
5259
  children
5011
5260
  }
5012
5261
  ) });
@@ -5018,17 +5267,37 @@ function PopoverRoot({
5018
5267
  defaultOpen = false,
5019
5268
  onOpenChange,
5020
5269
  placement = "bottom",
5021
- offset = 8
5270
+ offset = 8,
5271
+ hover = false,
5272
+ hoverDelay = 200,
5273
+ hoverCloseDelay = 300
5022
5274
  }) {
5023
- const [open, setOpen] = useControllableState(
5024
- controlledOpen,
5025
- defaultOpen,
5026
- onOpenChange
5027
- );
5275
+ const [internalOpen, setInternalOpen] = react.useState(defaultOpen);
5276
+ const isControlled = controlledOpen !== void 0;
5277
+ const open = isControlled ? controlledOpen : internalOpen;
5028
5278
  const anchorRef = react.useRef(null);
5279
+ const floatingRef = react.useRef(null);
5280
+ const hoverTimeoutRef = react.useRef(void 0);
5281
+ const setOpen = react.useCallback(
5282
+ (next) => {
5283
+ if (!isControlled) setInternalOpen(next);
5284
+ onOpenChange?.(next);
5285
+ },
5286
+ [isControlled, onOpenChange]
5287
+ );
5288
+ const onHoverEnter = react.useCallback(() => {
5289
+ if (!hover) return;
5290
+ clearTimeout(hoverTimeoutRef.current);
5291
+ hoverTimeoutRef.current = setTimeout(() => setOpen(true), hoverDelay);
5292
+ }, [hover, hoverDelay, setOpen]);
5293
+ const onHoverLeave = react.useCallback(() => {
5294
+ if (!hover) return;
5295
+ clearTimeout(hoverTimeoutRef.current);
5296
+ hoverTimeoutRef.current = setTimeout(() => setOpen(false), hoverCloseDelay);
5297
+ }, [hover, hoverCloseDelay, setOpen]);
5029
5298
  const ctx = react.useMemo(
5030
- () => ({ open, setOpen, anchorRef, placement, offset }),
5031
- [open, setOpen, anchorRef, placement, offset]
5299
+ () => ({ open, setOpen, anchorRef, floatingRef, placement, offset, hover, onHoverEnter, onHoverLeave }),
5300
+ [open, setOpen, placement, offset, hover, onHoverEnter, onHoverLeave]
5032
5301
  );
5033
5302
  return /* @__PURE__ */ jsxRuntime.jsx(PopoverContext.Provider, { value: ctx, children });
5034
5303
  }
@@ -5056,6 +5325,36 @@ function DropdownTrigger({ children }) {
5056
5325
  });
5057
5326
  }
5058
5327
  DropdownTrigger.displayName = "DropdownTrigger";
5328
+ function useAnimation({
5329
+ open,
5330
+ enterClass,
5331
+ exitClass
5332
+ }) {
5333
+ const [mounted, setMounted] = react.useState(open);
5334
+ const [animClass, setAnimClass] = react.useState(open ? enterClass : "");
5335
+ const ref = react.useRef(null);
5336
+ react.useEffect(() => {
5337
+ if (open) {
5338
+ setMounted(true);
5339
+ setAnimClass(enterClass);
5340
+ } else if (mounted) {
5341
+ setAnimClass(exitClass);
5342
+ }
5343
+ }, [open, enterClass, exitClass, mounted]);
5344
+ const handleAnimationEnd = react.useCallback(() => {
5345
+ if (!open) {
5346
+ setMounted(false);
5347
+ setAnimClass("");
5348
+ }
5349
+ }, [open]);
5350
+ react.useEffect(() => {
5351
+ const el = ref.current;
5352
+ if (!el) return;
5353
+ el.addEventListener("animationend", handleAnimationEnd);
5354
+ return () => el.removeEventListener("animationend", handleAnimationEnd);
5355
+ }, [handleAnimationEnd, mounted]);
5356
+ return { mounted, className: animClass, ref };
5357
+ }
5059
5358
  function DropdownItems({ children, className }) {
5060
5359
  const { open, setOpen, anchorRef, activeIndex, setActiveIndex, placement, offset } = useDropdown();
5061
5360
  const floatingRef = react.useRef(null);