@flikk/ui 1.0.0-beta.7 → 1.0.0-beta.8

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 (45) hide show
  1. package/dist/components/core/Alert/Alert.js +1 -0
  2. package/dist/components/core/Alert/Alert.theme.js +3 -7
  3. package/dist/components/core/Alert/Alert.types.d.ts +2 -1
  4. package/dist/components/core/Badge/Badge.theme.js +9 -4
  5. package/dist/components/core/Badge/Badge.types.d.ts +2 -1
  6. package/dist/components/core/Button/Button.theme.js +32 -26
  7. package/dist/components/core/Button/Button.types.d.ts +2 -1
  8. package/dist/components/core/Popover/PopoverContent.js +2 -0
  9. package/dist/components/core/Progress/Progress.theme.js +8 -5
  10. package/dist/components/core/Progress/Progress.types.d.ts +2 -1
  11. package/dist/components/core/Spinner/Spinner.theme.js +3 -5
  12. package/dist/components/core/Spinner/Spinner.types.d.ts +2 -1
  13. package/dist/components/data-display/KPI/KPI.theme.js +3 -4
  14. package/dist/components/data-display/KPI/KPI.types.d.ts +2 -1
  15. package/dist/components/data-display/Metric/Metric.theme.js +4 -0
  16. package/dist/components/data-display/Metric/Metric.types.d.ts +2 -1
  17. package/dist/components/effects/CustomCursor/CustomCursor.theme.js +2 -0
  18. package/dist/components/effects/CustomCursor/CustomCursor.types.d.ts +2 -1
  19. package/dist/components/forms/Checkbox/Checkbox.d.ts +3 -4
  20. package/dist/components/forms/Checkbox/Checkbox.js +33 -9
  21. package/dist/components/forms/Checkbox/Checkbox.theme.js +6 -0
  22. package/dist/components/forms/Checkbox/Checkbox.types.d.ts +28 -2
  23. package/dist/components/forms/Checkbox/CheckboxContext.d.ts +14 -0
  24. package/dist/components/forms/Checkbox/CheckboxContext.js +12 -0
  25. package/dist/components/forms/Checkbox/CheckboxGroup.d.ts +3 -0
  26. package/dist/components/forms/Checkbox/CheckboxGroup.js +40 -0
  27. package/dist/components/forms/Checkbox/index.d.ts +1 -1
  28. package/dist/components/forms/Radio/Radio.d.ts +3 -4
  29. package/dist/components/forms/Radio/Radio.js +21 -4
  30. package/dist/components/forms/Radio/Radio.theme.js +6 -0
  31. package/dist/components/forms/Radio/Radio.types.d.ts +30 -4
  32. package/dist/components/forms/Radio/RadioContext.d.ts +14 -0
  33. package/dist/components/forms/Radio/RadioContext.js +12 -0
  34. package/dist/components/forms/Radio/RadioGroup.d.ts +3 -0
  35. package/dist/components/forms/Radio/RadioGroup.js +36 -0
  36. package/dist/components/forms/Radio/index.d.ts +1 -1
  37. package/dist/components/forms/index.d.ts +2 -2
  38. package/dist/hooks/useSelectPortal.d.ts +3 -2
  39. package/dist/hooks/useSelectPortal.js +44 -19
  40. package/dist/styles.css +1 -1
  41. package/dist/utils/colorUtils.d.ts +23 -0
  42. package/dist/utils/colorUtils.js +45 -0
  43. package/dist/utils/index.d.ts +1 -0
  44. package/package.json +1 -1
  45. package/src/styles/theme.css +19 -0
@@ -0,0 +1,40 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import React__default, { useId, useState, useMemo } from 'react';
3
+ import { cn } from '../../../utils/cn.js';
4
+ import { checkboxTheme } from './Checkbox.theme.js';
5
+ import { formLabelTheme } from '../FormLabel/FormLabel.theme.js';
6
+ import { CheckboxGroupContext } from './CheckboxContext.js';
7
+
8
+ const CheckboxGroup = React__default.forwardRef(({ value: controlledValue, defaultValue, onChange, name, size = "md", state = "default", orientation = "vertical", label, description, error, required, className, children, ...props }, ref) => {
9
+ const autoId = useId();
10
+ const groupName = name !== null && name !== void 0 ? name : `checkbox-group-${autoId}`;
11
+ const labelId = `${autoId}-label`;
12
+ const descriptionId = `${autoId}-desc`;
13
+ const errorId = `${autoId}-error`;
14
+ // Controlled vs uncontrolled
15
+ const isControlled = controlledValue !== undefined;
16
+ const [internalValue, setInternalValue] = useState(defaultValue !== null && defaultValue !== void 0 ? defaultValue : []);
17
+ const currentValue = isControlled ? controlledValue : internalValue;
18
+ const handleChange = (itemValue, checked) => {
19
+ const next = checked
20
+ ? [...currentValue, itemValue]
21
+ : currentValue.filter((v) => v !== itemValue);
22
+ if (!isControlled) {
23
+ setInternalValue(next);
24
+ }
25
+ onChange === null || onChange === void 0 ? void 0 : onChange(next);
26
+ };
27
+ const contextValue = useMemo(() => ({
28
+ name: groupName,
29
+ value: currentValue,
30
+ onChange: handleChange,
31
+ size,
32
+ state,
33
+ }), [groupName, currentValue, size, state]);
34
+ const labelState = error ? "invalid" : state === "disabled" ? "disabled" : "default";
35
+ return (jsx(CheckboxGroupContext.Provider, { value: contextValue, children: jsxs("fieldset", { ref: ref, role: "group", "aria-labelledby": label ? labelId : undefined, "aria-describedby": description ? descriptionId : undefined, "aria-required": required || undefined, "aria-invalid": error ? true : undefined, "aria-errormessage": error ? errorId : undefined, className: cn("border-0 p-0 m-0", checkboxTheme.groupStyle, className), ...props, children: [label && (jsxs("legend", { id: labelId, className: cn(formLabelTheme.baseStyle, formLabelTheme.stateStyles[labelState]), children: [label, required && (jsx("span", { className: formLabelTheme.requiredIndicatorStyle, children: "*" }))] })), description && (jsx("p", { id: descriptionId, className: checkboxTheme.groupDescriptionStyle, children: description })), jsx("div", { className: cn(checkboxTheme.groupItemsStyle, orientation === "horizontal" &&
36
+ checkboxTheme.groupHorizontalStyle), children: children }), error && (jsx("p", { id: errorId, role: "alert", className: checkboxTheme.groupErrorStyle, children: error }))] }) }));
37
+ });
38
+ CheckboxGroup.displayName = "Checkbox.Group";
39
+
40
+ export { CheckboxGroup };
@@ -1,3 +1,3 @@
1
1
  export { Checkbox } from './Checkbox';
2
- export type { CheckboxProps, CheckboxSize, CheckboxState } from './Checkbox.types';
2
+ export type { CheckboxProps, CheckboxSize, CheckboxState, CheckboxGroupProps, CheckboxGroupOrientation } from './Checkbox.types';
3
3
  export { checkboxTheme } from './Checkbox.theme';
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { RadioProps } from "./Radio.types";
3
- /**
4
- * Radio component for selecting one option from a group
5
- */
6
- export declare const Radio: React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLInputElement>>;
3
+ export declare const Radio: React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLInputElement>> & {
4
+ Group: React.ForwardRefExoticComponent<import("./Radio.types").RadioGroupProps & React.RefAttributes<HTMLFieldSetElement>>;
5
+ };
@@ -1,18 +1,34 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import React__default from 'react';
2
+ import React__default, { useId } from 'react';
3
3
  import { radioTheme } from './Radio.theme.js';
4
4
  import { formLabelTheme } from '../FormLabel/FormLabel.theme.js';
5
5
  import { cn } from '../../../utils/cn.js';
6
+ import { useRadioGroupContext } from './RadioContext.js';
7
+ import { RadioGroup } from './RadioGroup.js';
6
8
 
7
9
  /**
8
10
  * Radio component for selecting one option from a group
9
11
  */
10
- const Radio = React__default.forwardRef(({ id, name, value, checked, defaultChecked, onChange, label, description, state = "default", size = "md", className, ...restProps }, ref) => {
11
- const isControlled = checked !== undefined;
12
+ const RadioComponent = React__default.forwardRef(({ id: idProp, name: nameProp, value, checked: checkedProp, defaultChecked, onChange, label, description, state: stateProp = "default", size: sizeProp = "md", className, ...restProps }, ref) => {
13
+ var _a, _b, _c, _d;
14
+ const autoId = useId();
15
+ const groupContext = useRadioGroupContext();
16
+ // Resolve props from group context or individual props
17
+ const id = idProp !== null && idProp !== void 0 ? idProp : autoId;
18
+ const name = (_b = (_a = groupContext === null || groupContext === void 0 ? void 0 : groupContext.name) !== null && _a !== void 0 ? _a : nameProp) !== null && _b !== void 0 ? _b : autoId;
19
+ const size = (_c = groupContext === null || groupContext === void 0 ? void 0 : groupContext.size) !== null && _c !== void 0 ? _c : sizeProp;
20
+ const state = stateProp === "disabled" ? "disabled" : ((_d = groupContext === null || groupContext === void 0 ? void 0 : groupContext.state) !== null && _d !== void 0 ? _d : stateProp);
12
21
  const isDisabled = state === "disabled";
22
+ // Determine checked state
23
+ const isInGroup = groupContext !== undefined;
24
+ const checked = isInGroup ? groupContext.value === value : checkedProp;
25
+ const isControlled = checked !== undefined;
13
26
  const handleChange = (e) => {
14
27
  if (isDisabled)
15
28
  return;
29
+ if (isInGroup) {
30
+ groupContext.onChange(value);
31
+ }
16
32
  onChange === null || onChange === void 0 ? void 0 : onChange(e.target.checked, value);
17
33
  };
18
34
  const inputProps = isControlled
@@ -20,6 +36,7 @@ const Radio = React__default.forwardRef(({ id, name, value, checked, defaultChec
20
36
  : { defaultChecked };
21
37
  return (jsxs("div", { className: cn(radioTheme.baseStyle, className), ...restProps, children: [jsxs("div", { className: cn(radioTheme.inputContainerStyle, radioTheme.sizes[size]), children: [jsx("input", { ref: ref, type: "radio", id: id, name: name, value: value, ...inputProps, onChange: handleChange, disabled: isDisabled, "aria-describedby": description ? `${id}-description` : undefined, "data-state": isDisabled ? "disabled" : "default", className: cn("peer", radioTheme.inputStyle, radioTheme.sizes[size]) }), jsx("div", { className: cn(radioTheme.dotStyle, size === "sm" ? "size-2.5 top-0.25" : "size-3") })] }), (label || description) && (jsxs("label", { htmlFor: id, className: cn("cursor-pointer", isDisabled && "cursor-not-allowed"), children: [label && (jsx("span", { className: cn(formLabelTheme.baseStyle, formLabelTheme.stateStyles[state], "mb-0"), children: label })), description && (jsx("div", { id: `${id}-description`, className: radioTheme.descriptionStyle, children: description }))] }))] }));
22
38
  });
23
- Radio.displayName = "Radio";
39
+ RadioComponent.displayName = "Radio";
40
+ const Radio = Object.assign(RadioComponent, { Group: RadioGroup });
24
41
 
25
42
  export { Radio };
@@ -34,6 +34,12 @@ const radioTheme = {
34
34
  sm: "size-5 mt-0.25",
35
35
  md: "size-6",
36
36
  },
37
+ // Group styles
38
+ groupStyle: "flex flex-col gap-1.5",
39
+ groupHorizontalStyle: "flex-row flex-wrap gap-x-6 gap-y-3",
40
+ groupDescriptionStyle: "text-sm text-[var(--color-text-secondary)]/80 -mt-0.5 mb-1",
41
+ groupErrorStyle: "text-sm text-[var(--color-danger)] mt-1.5",
42
+ groupItemsStyle: "flex flex-col gap-3",
37
43
  };
38
44
 
39
45
  export { radioTheme };
@@ -7,21 +7,42 @@ export type RadioState = 'default' | 'disabled';
7
7
  * Size variants for the radio component
8
8
  */
9
9
  export type RadioSize = 'sm' | 'md';
10
+ /**
11
+ * Orientation for Radio.Group layout
12
+ */
13
+ export type RadioGroupOrientation = 'vertical' | 'horizontal';
10
14
  /**
11
15
  * Radio component props
12
16
  */
13
17
  export interface RadioProps extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'size'> {
14
- id: string;
15
- name: string;
16
- value: string;
18
+ id?: string;
19
+ name?: string;
20
+ value: string | number;
17
21
  checked?: boolean;
18
- onChange?: (checked: boolean, value: string) => void;
22
+ onChange?: (checked: boolean, value: string | number) => void;
19
23
  label?: string;
20
24
  description?: string;
21
25
  state?: RadioState;
22
26
  size?: RadioSize;
23
27
  className?: string;
24
28
  }
29
+ /**
30
+ * Radio.Group component props
31
+ */
32
+ export interface RadioGroupProps extends Omit<React.HTMLAttributes<HTMLFieldSetElement>, 'onChange'> {
33
+ value?: string | number;
34
+ defaultValue?: string | number;
35
+ onChange?: (value: string | number) => void;
36
+ name?: string;
37
+ size?: RadioSize;
38
+ state?: RadioState;
39
+ orientation?: RadioGroupOrientation;
40
+ label?: string;
41
+ description?: string;
42
+ error?: string;
43
+ required?: boolean;
44
+ className?: string;
45
+ }
25
46
  /**
26
47
  * Radio theme configuration
27
48
  */
@@ -32,4 +53,9 @@ export interface RadioTheme {
32
53
  dotStyle: string;
33
54
  descriptionStyle: string;
34
55
  sizes: Record<RadioSize, string>;
56
+ groupStyle?: string;
57
+ groupHorizontalStyle?: string;
58
+ groupDescriptionStyle?: string;
59
+ groupErrorStyle?: string;
60
+ groupItemsStyle?: string;
35
61
  }
@@ -0,0 +1,14 @@
1
+ import type { RadioSize, RadioState } from "./Radio.types";
2
+ export interface RadioGroupContextValue {
3
+ name: string;
4
+ value: string | number | undefined;
5
+ onChange: (value: string | number) => void;
6
+ size: RadioSize;
7
+ state: RadioState;
8
+ }
9
+ export declare const RadioGroupContext: import("react").Context<RadioGroupContextValue | undefined>;
10
+ /**
11
+ * Returns the group context, or undefined when used outside a Radio.Group.
12
+ * This allows standalone Radio usage without a Group wrapper.
13
+ */
14
+ export declare function useRadioGroupContext(): RadioGroupContextValue | undefined;
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ const RadioGroupContext = createContext(undefined);
4
+ /**
5
+ * Returns the group context, or undefined when used outside a Radio.Group.
6
+ * This allows standalone Radio usage without a Group wrapper.
7
+ */
8
+ function useRadioGroupContext() {
9
+ return useContext(RadioGroupContext);
10
+ }
11
+
12
+ export { RadioGroupContext, useRadioGroupContext };
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { RadioGroupProps } from "./Radio.types";
3
+ export declare const RadioGroup: React.ForwardRefExoticComponent<RadioGroupProps & React.RefAttributes<HTMLFieldSetElement>>;
@@ -0,0 +1,36 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import React__default, { useId, useState, useMemo } from 'react';
3
+ import { cn } from '../../../utils/cn.js';
4
+ import { radioTheme } from './Radio.theme.js';
5
+ import { formLabelTheme } from '../FormLabel/FormLabel.theme.js';
6
+ import { RadioGroupContext } from './RadioContext.js';
7
+
8
+ const RadioGroup = React__default.forwardRef(({ value: controlledValue, defaultValue, onChange, name, size = "md", state = "default", orientation = "vertical", label, description, error, required, className, children, ...props }, ref) => {
9
+ const autoId = useId();
10
+ const groupName = name !== null && name !== void 0 ? name : `radio-group-${autoId}`;
11
+ const labelId = `${autoId}-label`;
12
+ const descriptionId = `${autoId}-desc`;
13
+ const errorId = `${autoId}-error`;
14
+ // Controlled vs uncontrolled
15
+ const isControlled = controlledValue !== undefined;
16
+ const [internalValue, setInternalValue] = useState(defaultValue);
17
+ const currentValue = isControlled ? controlledValue : internalValue;
18
+ const handleChange = (itemValue) => {
19
+ if (!isControlled) {
20
+ setInternalValue(itemValue);
21
+ }
22
+ onChange === null || onChange === void 0 ? void 0 : onChange(itemValue);
23
+ };
24
+ const contextValue = useMemo(() => ({
25
+ name: groupName,
26
+ value: currentValue,
27
+ onChange: handleChange,
28
+ size,
29
+ state,
30
+ }), [groupName, currentValue, size, state]);
31
+ const labelState = error ? "invalid" : state === "disabled" ? "disabled" : "default";
32
+ return (jsx(RadioGroupContext.Provider, { value: contextValue, children: jsxs("fieldset", { ref: ref, role: "radiogroup", "aria-labelledby": label ? labelId : undefined, "aria-describedby": description ? descriptionId : undefined, "aria-required": required || undefined, "aria-invalid": error ? true : undefined, "aria-errormessage": error ? errorId : undefined, className: cn("border-0 p-0 m-0", radioTheme.groupStyle, className), ...props, children: [label && (jsxs("legend", { id: labelId, className: cn(formLabelTheme.baseStyle, formLabelTheme.stateStyles[labelState]), children: [label, required && (jsx("span", { className: formLabelTheme.requiredIndicatorStyle, children: "*" }))] })), description && (jsx("p", { id: descriptionId, className: radioTheme.groupDescriptionStyle, children: description })), jsx("div", { className: cn(radioTheme.groupItemsStyle, orientation === "horizontal" && radioTheme.groupHorizontalStyle), children: children }), error && (jsx("p", { id: errorId, role: "alert", className: radioTheme.groupErrorStyle, children: error }))] }) }));
33
+ });
34
+ RadioGroup.displayName = "Radio.Group";
35
+
36
+ export { RadioGroup };
@@ -1,3 +1,3 @@
1
1
  export { Radio } from './Radio';
2
- export type { RadioProps, RadioSize, RadioState } from './Radio.types';
2
+ export type { RadioProps, RadioSize, RadioState, RadioGroupProps, RadioGroupOrientation } from './Radio.types';
3
3
  export { radioTheme } from './Radio.theme';
@@ -11,10 +11,10 @@ export { FormLabel } from "./FormLabel";
11
11
  export type { FormLabelProps, FormLabelState, FormLabelTheme, } from "./FormLabel";
12
12
  export { formLabelTheme } from "./FormLabel";
13
13
  export { Radio } from "./Radio";
14
- export type { RadioProps, RadioSize, RadioState } from "./Radio";
14
+ export type { RadioProps, RadioSize, RadioState, RadioGroupProps, RadioGroupOrientation, } from "./Radio";
15
15
  export { radioTheme } from "./Radio";
16
16
  export { Checkbox } from "./Checkbox";
17
- export type { CheckboxProps, CheckboxSize, CheckboxState } from "./Checkbox";
17
+ export type { CheckboxProps, CheckboxSize, CheckboxState, CheckboxGroupProps, CheckboxGroupOrientation, } from "./Checkbox";
18
18
  export { checkboxTheme } from "./Checkbox";
19
19
  export { Switch } from "./Switch";
20
20
  export type { SwitchProps, SwitchThemeOverrides } from "./Switch";
@@ -6,6 +6,7 @@ export interface UseSimpleDropdownOptions {
6
6
  offset?: number;
7
7
  estimatedWidth?: number;
8
8
  autoWidth?: boolean;
9
+ onClose?: () => void;
9
10
  }
10
11
  export interface SimpleDropdownReturn {
11
12
  position: {
@@ -24,5 +25,5 @@ export interface SimpleDropdownReturn {
24
25
  * Uses position: fixed with pure viewport coordinates to properly follow scroll
25
26
  * Fixes critical issues: dropdown not following scroll & multiple dropdowns not working
26
27
  */
27
- export declare const useSimpleDropdown: ({ triggerRef, isOpen, placement, offset, estimatedWidth, autoWidth, }: UseSimpleDropdownOptions) => SimpleDropdownReturn;
28
- export declare const useSelectPortal: ({ triggerRef, isOpen, placement, offset, estimatedWidth, autoWidth, }: UseSimpleDropdownOptions) => SimpleDropdownReturn;
28
+ export declare const useSimpleDropdown: ({ triggerRef, isOpen, placement, offset, estimatedWidth, autoWidth, onClose, }: UseSimpleDropdownOptions) => SimpleDropdownReturn;
29
+ export declare const useSelectPortal: ({ triggerRef, isOpen, placement, offset, estimatedWidth, autoWidth, onClose, }: UseSimpleDropdownOptions) => SimpleDropdownReturn;
@@ -1,19 +1,11 @@
1
- import { useRef, useState, useCallback, useLayoutEffect, useMemo } from 'react';
1
+ import { useRef, useState, useCallback, useLayoutEffect } from 'react';
2
2
 
3
- // Simple debounce utility
4
- const debounce = (func, wait) => {
5
- let timeout;
6
- return () => {
7
- clearTimeout(timeout);
8
- timeout = setTimeout(func, wait);
9
- };
10
- };
11
3
  /**
12
4
  * Fixed dropdown positioning hook
13
5
  * Uses position: fixed with pure viewport coordinates to properly follow scroll
14
6
  * Fixes critical issues: dropdown not following scroll & multiple dropdowns not working
15
7
  */
16
- const useSimpleDropdown = ({ triggerRef, isOpen, placement = "bottom-start", offset = 4, estimatedWidth, autoWidth = false, }) => {
8
+ const useSimpleDropdown = ({ triggerRef, isOpen, placement = "bottom-start", offset = 4, estimatedWidth, autoWidth = false, onClose, }) => {
17
9
  const contentRef = useRef(null);
18
10
  const [position, setPosition] = useState({ top: -9999, left: -9999, width: 0 });
19
11
  const [cssVariables, setCssVariables] = useState({});
@@ -280,20 +272,53 @@ const useSimpleDropdown = ({ triggerRef, isOpen, placement = "bottom-start", off
280
272
  return;
281
273
  calculatePosition();
282
274
  }, [isOpen, calculatePosition]);
283
- // Debounced position updates for better performance
284
- const debouncedCalculatePosition = useMemo(() => debounce(calculatePosition, 16), // ~60fps
285
- [calculatePosition]);
286
- // Update position on scroll and resize - debounced for performance
275
+ // rAF-synced scroll handler: skips internal scrolls, detects off-screen trigger
276
+ const scrollRafRef = useRef(null);
277
+ const handleScroll = useCallback((e) => {
278
+ // Skip if scroll originated inside the popover content
279
+ if (contentRef.current && contentRef.current.contains(e.target)) {
280
+ return;
281
+ }
282
+ // Close if trigger has scrolled off-screen
283
+ if (triggerRef.current) {
284
+ const rect = triggerRef.current.getBoundingClientRect();
285
+ const isOffScreen = rect.bottom < 0 || rect.top > window.innerHeight
286
+ || rect.right < 0 || rect.left > window.innerWidth;
287
+ if (isOffScreen && onClose) {
288
+ onClose();
289
+ return;
290
+ }
291
+ }
292
+ if (scrollRafRef.current)
293
+ cancelAnimationFrame(scrollRafRef.current);
294
+ scrollRafRef.current = requestAnimationFrame(() => {
295
+ scrollRafRef.current = null;
296
+ calculatePosition();
297
+ });
298
+ }, [calculatePosition, onClose, triggerRef]);
299
+ const handleResize = useCallback(() => {
300
+ if (scrollRafRef.current)
301
+ cancelAnimationFrame(scrollRafRef.current);
302
+ scrollRafRef.current = requestAnimationFrame(() => {
303
+ scrollRafRef.current = null;
304
+ calculatePosition();
305
+ });
306
+ }, [calculatePosition]);
307
+ // Update position on scroll and resize - rAF-synced for smooth tracking
287
308
  useLayoutEffect(() => {
288
309
  if (!isOpen)
289
310
  return;
290
- window.addEventListener("scroll", debouncedCalculatePosition, { passive: true, capture: true });
291
- window.addEventListener("resize", debouncedCalculatePosition, { passive: true });
311
+ window.addEventListener("scroll", handleScroll, { passive: true, capture: true });
312
+ window.addEventListener("resize", handleResize, { passive: true });
292
313
  return () => {
293
- window.removeEventListener("scroll", debouncedCalculatePosition, true);
294
- window.removeEventListener("resize", debouncedCalculatePosition);
314
+ window.removeEventListener("scroll", handleScroll, true);
315
+ window.removeEventListener("resize", handleResize);
316
+ if (scrollRafRef.current) {
317
+ cancelAnimationFrame(scrollRafRef.current);
318
+ scrollRafRef.current = null;
319
+ }
295
320
  };
296
- }, [isOpen, debouncedCalculatePosition]);
321
+ }, [isOpen, handleScroll, handleResize]);
297
322
  // Recalculate when dropdown content resizes (e.g. search filtering)
298
323
  useLayoutEffect(() => {
299
324
  if (!isOpen)