@udixio/ui-react 2.9.14 → 2.9.15

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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +3866 -3744
  4. package/dist/lib/components/AnchorPositioner.d.ts +11 -0
  5. package/dist/lib/components/AnchorPositioner.d.ts.map +1 -0
  6. package/dist/lib/components/TextField.d.ts +1 -2
  7. package/dist/lib/components/TextField.d.ts.map +1 -1
  8. package/dist/lib/components/Tooltip.d.ts +1 -1
  9. package/dist/lib/components/Tooltip.d.ts.map +1 -1
  10. package/dist/lib/components/index.d.ts +1 -0
  11. package/dist/lib/components/index.d.ts.map +1 -1
  12. package/dist/lib/effects/smooth-scroll.effect.d.ts +14 -0
  13. package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
  14. package/dist/lib/hooks/index.d.ts +0 -1
  15. package/dist/lib/hooks/index.d.ts.map +1 -1
  16. package/dist/lib/interfaces/text-field.interface.d.ts +1 -1
  17. package/dist/lib/interfaces/text-field.interface.d.ts.map +1 -1
  18. package/dist/lib/interfaces/tooltip.interface.d.ts +2 -0
  19. package/dist/lib/interfaces/tooltip.interface.d.ts.map +1 -1
  20. package/dist/lib/styles/card.style.d.ts +2 -2
  21. package/dist/lib/styles/checkbox.style.d.ts +2 -2
  22. package/dist/lib/styles/fab.style.d.ts +2 -2
  23. package/dist/lib/styles/navigation-rail-item.style.d.ts +2 -2
  24. package/dist/lib/styles/side-sheet.style.d.ts +2 -2
  25. package/dist/lib/styles/slider.style.d.ts +2 -2
  26. package/dist/lib/styles/tab.style.d.ts +2 -2
  27. package/dist/lib/styles/text-field.style.d.ts +4 -4
  28. package/dist/lib/styles/tooltip.style.d.ts +8 -4
  29. package/dist/lib/styles/tooltip.style.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/lib/components/AnchorPositioner.tsx +132 -0
  32. package/src/lib/components/TextField.tsx +131 -19
  33. package/src/lib/components/Tooltip.tsx +13 -13
  34. package/src/lib/components/index.ts +1 -0
  35. package/src/lib/effects/smooth-scroll.effect.tsx +15 -1
  36. package/src/lib/hooks/index.ts +0 -1
  37. package/src/lib/interfaces/text-field.interface.ts +1 -1
  38. package/src/lib/interfaces/tooltip.interface.ts +2 -0
  39. package/src/lib/styles/date-picker.style.ts +1 -1
  40. package/src/lib/styles/side-sheet.style.ts +2 -2
  41. package/dist/lib/hooks/useTooltipPosition.d.ts +0 -22
  42. package/dist/lib/hooks/useTooltipPosition.d.ts.map +0 -1
  43. package/src/lib/hooks/useTooltipPosition.ts +0 -95
@@ -1,13 +1,18 @@
1
- import React, { useEffect, useId, useState } from 'react';
1
+ import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
2
2
  import { Icon } from '../icon';
3
- import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons';
3
+ import {
4
+ faCalendarDays,
5
+ faCircleExclamation,
6
+ } from '@fortawesome/free-solid-svg-icons';
4
7
  import { motion } from 'motion/react';
8
+ import { DatePicker } from './DatePicker';
9
+ import { Button } from './Button';
5
10
 
6
11
  import TextareaAutosize from 'react-textarea-autosize';
7
12
  import { useTextFieldStyle } from '../styles/text-field.style';
8
13
  import { classNames } from '../utils';
9
14
  import { ReactProps } from '../utils/component';
10
- import { TextFieldInterface } from '../interfaces/text-field.interface';
15
+ import { AnchorPositioner } from './AnchorPositioner';
11
16
 
12
17
  /**
13
18
  * Text fields let users enter text into a UI
@@ -40,6 +45,7 @@ export const TextField = ({
40
45
  showSupportingText,
41
46
  id: idProp,
42
47
  style,
48
+ ref,
43
49
  ...restProps
44
50
  }: ReactProps<TextFieldInterface>) => {
45
51
  const generatedId = useId();
@@ -53,6 +59,12 @@ export const TextField = ({
53
59
  const [isFocused, setIsFocused] = useState(false);
54
60
  const [showErrorIcon, setShowErrorIcon] = useState(!!errorText?.length);
55
61
 
62
+ const internalRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
63
+ const inputRef = (ref as any) || internalRef;
64
+
65
+ const textFieldRef = useRef<HTMLDivElement>(null);
66
+ const calendarTriggerRef = useRef<HTMLDivElement>(null);
67
+
56
68
  const hasSupportingText =
57
69
  showSupportingText ?? (!!errorText?.length || !!supportingText?.length);
58
70
 
@@ -60,8 +72,6 @@ export const TextField = ({
60
72
  setShowErrorIcon(!!errorText?.length);
61
73
  }, [errorText]);
62
74
 
63
- const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
64
-
65
75
  const focusInput = () => {
66
76
  if (inputRef.current && !isFocused) {
67
77
  inputRef.current.focus();
@@ -96,6 +106,54 @@ export const TextField = ({
96
106
  }
97
107
  };
98
108
 
109
+ // Date Picker Logic
110
+ const isDateInput = type === 'date';
111
+ const [showDatePicker, setShowDatePicker] = useState(false);
112
+ const [tempDate, setTempDate] = useState<Date | null>(null);
113
+
114
+ const initialDateValue = useMemo(() => {
115
+ const val = String(value);
116
+ if (!val) return null;
117
+ const [y, m, d] = val.split('-').map(Number);
118
+ if (y && m && d) return new Date(y, m - 1, d);
119
+ return null;
120
+ }, [value]);
121
+
122
+ const handleDatePickerOpen = () => {
123
+ if (disabled) return;
124
+ setTempDate(initialDateValue);
125
+ setShowDatePicker(true);
126
+ };
127
+
128
+ const handleDateConfirm = () => {
129
+ const newValue = tempDate ? tempDate.toLocaleDateString('en-CA') : '';
130
+
131
+ if (!isControlled) {
132
+ setInternalValue(newValue);
133
+ }
134
+
135
+ if (onChange) {
136
+ // Create a synthetic event
137
+ const event = {
138
+ target: {
139
+ value: newValue,
140
+ name,
141
+ type,
142
+ },
143
+ } as React.ChangeEvent<HTMLInputElement>;
144
+ onChange(event);
145
+ }
146
+ setShowDatePicker(false);
147
+ };
148
+
149
+ const effectiveTrailingIcon =
150
+ isDateInput && !trailingIcon ? faCalendarDays : trailingIcon;
151
+
152
+ // Enhance styles for date input
153
+ const inputDateClass = isDateInput
154
+ ? '[&::-webkit-calendar-picker-indicator]:hidden cursor-pointer'
155
+ : '';
156
+
99
157
  const styles = useTextFieldStyle({
100
158
  showSupportingText: hasSupportingText,
101
159
  isFocused,
@@ -110,7 +168,7 @@ export const TextField = ({
110
168
  supportingText,
111
169
  type,
112
170
  leadingIcon,
113
- trailingIcon,
171
+ trailingIcon: effectiveTrailingIcon,
114
172
  variant,
115
173
  errorText,
116
174
  value: String(value),
@@ -122,12 +180,14 @@ export const TextField = ({
122
180
  const textComponentProps = multiline ? {} : { type };
123
181
 
124
182
  const isFloating =
125
- isFocused || (typeof value === 'string' && value.length > 0);
183
+ isFocused ||
184
+ (typeof value === 'string' && value.length > 0) ||
185
+ type == 'date'; // Float label when picker open
126
186
  const showLegend = isFloating && variant === 'outlined';
127
187
  const showLabel = !showLegend;
128
188
 
129
189
  return (
130
- <div className={styles.textField} style={style}>
190
+ <div ref={textFieldRef} className={styles.textField} style={style}>
131
191
  <fieldset
132
192
  onClick={focusInput}
133
193
  className={styles.content}
@@ -200,11 +260,18 @@ export const TextField = ({
200
260
  ref={inputRef as any}
201
261
  value={value}
202
262
  onChange={handleChange}
203
- className={styles.input}
263
+ className={classNames(styles.input, inputDateClass)}
204
264
  id={id}
205
265
  name={name}
206
266
  placeholder={isFocused ? (placeholder ?? undefined) : ''}
207
- onFocus={handleOnFocus}
267
+ onFocus={(e) => {
268
+ handleOnFocus();
269
+ if (isDateInput) {
270
+ // Maybe open picker on focus? User preference.
271
+ // Often better on click/icon click.
272
+ // But let's stick to icon click for now or explicit open.
273
+ }
274
+ }}
208
275
  onBlur={handleBlur}
209
276
  disabled={disabled}
210
277
  autoComplete={autoComplete}
@@ -219,21 +286,31 @@ export const TextField = ({
219
286
 
220
287
  {!showErrorIcon && (
221
288
  <>
222
- {trailingIcon && (
289
+ {effectiveTrailingIcon && (
223
290
  <div
291
+ ref={isDateInput ? calendarTriggerRef : undefined}
224
292
  onClick={(event) => {
225
293
  event.stopPropagation();
294
+ if (isDateInput) handleDatePickerOpen();
226
295
  }}
227
- className={styles.trailingIcon}
228
- >
229
- {React.isValidElement(trailingIcon) ? (
230
- trailingIcon
231
- ) : (
232
- <Icon className={'h-5'} icon={trailingIcon}></Icon>
296
+ className={classNames(
297
+ styles.trailingIcon,
298
+ isDateInput && 'cursor-pointer',
233
299
  )}
300
+ >
301
+ <div className="flex items-center justify-center w-full h-full">
302
+ {React.isValidElement(effectiveTrailingIcon) ? (
303
+ effectiveTrailingIcon
304
+ ) : (
305
+ <Icon
306
+ className={'h-5'}
307
+ icon={effectiveTrailingIcon as any}
308
+ />
309
+ )}
310
+ </div>
234
311
  </div>
235
312
  )}
236
- {!trailingIcon && suffix && (
313
+ {!effectiveTrailingIcon && suffix && (
237
314
  <span className={styles.suffix}>{suffix}</span>
238
315
  )}
239
316
  </>
@@ -242,7 +319,7 @@ export const TextField = ({
242
319
  {showErrorIcon && (
243
320
  <div
244
321
  className={classNames(styles.trailingIcon, {
245
- ' absolute right-0': !trailingIcon,
322
+ ' absolute right-0': !effectiveTrailingIcon,
246
323
  })}
247
324
  >
248
325
  <Icon
@@ -252,6 +329,7 @@ export const TextField = ({
252
329
  </div>
253
330
  )}
254
331
  </fieldset>
332
+
255
333
  {hasSupportingText && (
256
334
  <p className={styles.supportingText} id={helperTextId}>
257
335
  {errorText?.length
@@ -261,6 +339,40 @@ export const TextField = ({
261
339
  : '\u00A0'}
262
340
  </p>
263
341
  )}
342
+
343
+ {isDateInput && showDatePicker && (
344
+ <>
345
+ <div
346
+ className="fixed inset-0 z-40 bg-transparent"
347
+ onClick={() => setShowDatePicker(false)}
348
+ />
349
+ <AnchorPositioner anchorRef={textFieldRef} position="bottom">
350
+ <div className="z-50 shadow-xl rounded-[28px] bg-surface-container-high overflow-hidden">
351
+ <DatePicker
352
+ className={''}
353
+ value={tempDate}
354
+ onChange={setTempDate}
355
+ />
356
+ <div className="flex justify-end gap-2 p-4 pt-0">
357
+ <Button
358
+ variant="text"
359
+ size="small"
360
+ onClick={() => setShowDatePicker(false)}
361
+ >
362
+ Cancel
363
+ </Button>
364
+ <Button
365
+ variant="filled"
366
+ size="small"
367
+ onClick={handleDateConfirm}
368
+ >
369
+ OK
370
+ </Button>
371
+ </div>
372
+ </div>
373
+ </AnchorPositioner>
374
+ </>
375
+ )}
264
376
  </div>
265
377
  );
266
378
  };
@@ -1,11 +1,11 @@
1
1
  import { cloneElement, isValidElement, useEffect, useRef } from 'react';
2
2
  import { MotionProps } from '../utils';
3
3
  import { Button } from './Button';
4
+ import { AnchorPositioner } from './AnchorPositioner';
4
5
  import { ToolTipInterface } from '../interfaces';
5
6
  import { useToolTipStyle } from '../styles';
6
7
  import { AnimatePresence, motion } from 'motion/react';
7
- import { SyncedFixedWrapper } from '../effects';
8
- import { useTooltipTrigger, useTooltipPosition } from '../hooks';
8
+ import { useTooltipTrigger } from '../hooks';
9
9
 
10
10
  /**
11
11
  * Tooltips display brief labels or messages
@@ -36,8 +36,12 @@ export const Tooltip = ({
36
36
  defaultOpen = false,
37
37
  onOpenChange,
38
38
  id,
39
+ anchorRef,
39
40
  ...props
40
41
  }: MotionProps<ToolTipInterface>) => {
42
+ const defaultPosition = variant === 'rich' ? 'bottom-right' : 'bottom';
43
+ const effectivePosition = positionProp || defaultPosition;
44
+
41
45
  transition = { duration: 0.3, ...transition };
42
46
 
43
47
  if (!children && !targetRef) {
@@ -50,6 +54,7 @@ export const Tooltip = ({
50
54
 
51
55
  const internalRef = useRef<HTMLElement | null>(null);
52
56
  const resolvedRef = targetRef || internalRef;
57
+ const positioningRef = anchorRef || resolvedRef;
53
58
 
54
59
  // Use the trigger hook for state management and accessibility
55
60
  const { triggerProps, tooltipProps, isOpen } = useTooltipTrigger({
@@ -62,14 +67,6 @@ export const Tooltip = ({
62
67
  id,
63
68
  });
64
69
 
65
- // Use the position hook for auto-positioning
66
- const { resolvedPosition } = useTooltipPosition({
67
- targetRef: resolvedRef,
68
- position: positionProp,
69
- variant,
70
- isOpen,
71
- });
72
-
73
70
  // Apply trigger props to the target element
74
71
  const enhancedChildren =
75
72
  !targetRef && isValidElement(children)
@@ -150,7 +147,7 @@ export const Tooltip = ({
150
147
  className,
151
148
  title,
152
149
  text,
153
- position: resolvedPosition,
150
+ position: effectivePosition,
154
151
  trigger,
155
152
  targetRef: targetRef as any,
156
153
  children: children as any,
@@ -172,7 +169,10 @@ export const Tooltip = ({
172
169
  {enhancedChildren}
173
170
  <AnimatePresence>
174
171
  {isOpen && (
175
- <SyncedFixedWrapper targetRef={resolvedRef}>
172
+ <AnchorPositioner
173
+ anchorRef={positioningRef}
174
+ position={effectivePosition}
175
+ >
176
176
  <motion.div
177
177
  initial={'close'}
178
178
  variants={variants}
@@ -209,7 +209,7 @@ export const Tooltip = ({
209
209
  )}
210
210
  </div>
211
211
  </motion.div>
212
- </SyncedFixedWrapper>
212
+ </AnchorPositioner>
213
213
  )}
214
214
  </AnimatePresence>
215
215
  </>
@@ -1,3 +1,4 @@
1
+ export * from './AnchorPositioner';
1
2
  export * from './Button';
2
3
  export * from './Card';
3
4
  export * from './Carousel';
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, ReactNode } from 'react';
1
+ import { ReactNode, useEffect, useRef } from 'react';
2
2
  import Lenis from 'lenis';
3
3
 
4
4
  export type SmoothScrollProps = {
@@ -36,6 +36,20 @@ export type SmoothScrollProps = {
36
36
  /**
37
37
  * SmoothScroll component using Lenis for smooth scrolling.
38
38
  * This component enables smooth scrolling at the document level.
39
+ *
40
+ * @warning **Use with caution.** Overriding native scroll behavior can cause:
41
+ * - **Accessibility issues**: Screen readers, keyboard navigation, and assistive technologies
42
+ * may not work correctly with custom scroll implementations.
43
+ * - **Anchor links broken**: `scrollIntoView()`, hash navigation (`#section`), and
44
+ * `window.scrollTo()` may behave unexpectedly or be ignored.
45
+ * - **Third-party library conflicts**: Libraries relying on native scroll events
46
+ * (infinite scroll, lazy loading, scroll-triggered animations) may malfunction.
47
+ * - **Browser features disabled**: Find-in-page (Ctrl+F), autoscroll, and native
48
+ * momentum scrolling on trackpads may not work as expected.
49
+ * - **Performance overhead**: The RAF loop runs continuously, which may impact
50
+ * battery life and performance on low-end devices.
51
+ * - **Mobile issues**: Touch gestures, pull-to-refresh, and overscroll behaviors
52
+ * can conflict with smooth scroll implementations.
39
53
  */
40
54
  export const SmoothScroll = ({
41
55
  transition = 1.2,
@@ -4,7 +4,6 @@ export type {
4
4
  UseTooltipTriggerReturn,
5
5
  } from './useTooltipTrigger';
6
6
 
7
- export { useTooltipPosition } from './useTooltipPosition';
8
7
  export type {
9
8
  UseTooltipPositionOptions,
10
9
  UseTooltipPositionReturn,
@@ -22,7 +22,7 @@ type Props = {
22
22
  id?: string;
23
23
  style?: React.CSSProperties;
24
24
  variant?: TextFieldVariant;
25
- type?: 'text' | 'password' | 'number';
25
+ type?: 'text' | 'password' | 'number' | 'date';
26
26
  autoComplete?: 'on' | 'off' | string;
27
27
  multiline?: boolean;
28
28
  };
@@ -38,6 +38,8 @@ export type ToolTipInterface<T extends HTMLElement = any> = {
38
38
  onOpenChange?: (open: boolean) => void;
39
39
  /** Custom ID for accessibility linking. Auto-generated if not provided. */
40
40
  id?: string;
41
+ /** Custom anchor for positioning. Defaults to the trigger element. */
42
+ anchorRef?: RefObject<HTMLElement>;
41
43
  } & (
42
44
  | {
43
45
  children?: never;
@@ -10,7 +10,7 @@ const datePickerConfig: ClassNameComponent<DatePickerInterface> = ({
10
10
  hasSelected,
11
11
  }) => ({
12
12
  datePicker: classNames(
13
- 'inline-flex flex-col bg-surface-container-high rounded-[28px] p-3 shadow-sm select-none', // Using shadow-sm as placeholder for elevation
13
+ 'inline-flex flex-col bg-surface-container-high rounded-[28px] p-3 select-none', // Using shadow-sm as placeholder for elevation
14
14
  'min-w-[320px]',
15
15
  ),
16
16
  header: classNames('flex items-center justify-between h-12 mb-2 px-2'),
@@ -25,9 +25,9 @@ export const sideSheetConfig: ClassNameComponent<SideSheetInterface> = ({
25
25
  },
26
26
  ],
27
27
  ),
28
- container: classNames('w-full overflow-hidden', {}),
29
- content: classNames('w-fit '),
28
+ container: classNames('w-full overflow-hidden flex flex-col', {}),
30
29
  header: classNames('p-4 flex items-center gap-2'),
30
+ content: classNames('flex-1 overflow-y-auto'),
31
31
  title: classNames('text-on-surface-variant text-title-large'),
32
32
  closeButton: classNames('ml-auto'),
33
33
  divider: classNames({ hidden: variant == 'modal' }),
@@ -1,22 +0,0 @@
1
- import { RefObject } from 'react';
2
- type Position = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
3
- type Variant = 'plain' | 'rich';
4
- export interface UseTooltipPositionOptions {
5
- targetRef: RefObject<HTMLElement | null>;
6
- position?: Position;
7
- variant?: Variant;
8
- isOpen: boolean;
9
- }
10
- export interface UseTooltipPositionReturn {
11
- resolvedPosition: Position;
12
- }
13
- /**
14
- * Hook to calculate tooltip position using useLayoutEffect.
15
- * Auto-flips position if not enough viewport space.
16
- *
17
- * For plain variant: prefers left/right, falls back to top/bottom
18
- * For rich variant: uses corner positions (top-left, top-right, bottom-left, bottom-right)
19
- */
20
- export declare function useTooltipPosition({ targetRef, position: positionProp, variant, isOpen, }: UseTooltipPositionOptions): UseTooltipPositionReturn;
21
- export {};
22
- //# sourceMappingURL=useTooltipPosition.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useTooltipPosition.d.ts","sourceRoot":"","sources":["../../../src/lib/hooks/useTooltipPosition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA6B,MAAM,OAAO,CAAC;AAE7D,KAAK,QAAQ,GACT,KAAK,GACL,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,WAAW,GACX,aAAa,GACb,cAAc,CAAC;AAEnB,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhC,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,QAAQ,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,EAAE,YAAY,EACtB,OAAiB,EACjB,MAAM,GACP,EAAE,yBAAyB,GAAG,wBAAwB,CAyDtD"}
@@ -1,95 +0,0 @@
1
- import { RefObject, useLayoutEffect, useState } from 'react';
2
-
3
- type Position =
4
- | 'top'
5
- | 'bottom'
6
- | 'left'
7
- | 'right'
8
- | 'top-left'
9
- | 'top-right'
10
- | 'bottom-left'
11
- | 'bottom-right';
12
-
13
- type Variant = 'plain' | 'rich';
14
-
15
- export interface UseTooltipPositionOptions {
16
- targetRef: RefObject<HTMLElement | null>;
17
- position?: Position;
18
- variant?: Variant;
19
- isOpen: boolean;
20
- }
21
-
22
- export interface UseTooltipPositionReturn {
23
- resolvedPosition: Position;
24
- }
25
-
26
- /**
27
- * Hook to calculate tooltip position using useLayoutEffect.
28
- * Auto-flips position if not enough viewport space.
29
- *
30
- * For plain variant: prefers left/right, falls back to top/bottom
31
- * For rich variant: uses corner positions (top-left, top-right, bottom-left, bottom-right)
32
- */
33
- export function useTooltipPosition({
34
- targetRef,
35
- position: positionProp,
36
- variant = 'plain',
37
- isOpen,
38
- }: UseTooltipPositionOptions): UseTooltipPositionReturn {
39
- const [resolvedPosition, setResolvedPosition] = useState<Position>(
40
- positionProp ?? 'bottom',
41
- );
42
-
43
- useLayoutEffect(() => {
44
- // If position is explicitly set, use it
45
- if (positionProp) {
46
- setResolvedPosition(positionProp);
47
- return;
48
- }
49
-
50
- // Only calculate if open and we have a target
51
- if (!isOpen || !targetRef.current || typeof window === 'undefined') {
52
- return;
53
- }
54
-
55
- const targetElement = targetRef.current;
56
- const rect = targetElement.getBoundingClientRect();
57
-
58
- const viewportWidth = window.innerWidth;
59
- const viewportHeight = window.innerHeight;
60
-
61
- // Normalized position (0-1 range)
62
- const x = rect.left / viewportWidth;
63
- const y = rect.top / viewportHeight;
64
-
65
- let newPosition: Position;
66
-
67
- if (variant === 'plain') {
68
- // Plain variant: prefer horizontal positioning, fall back to vertical
69
- if (x < 1 / 3) {
70
- newPosition = 'right';
71
- } else if (x > 2 / 3) {
72
- newPosition = 'left';
73
- } else {
74
- newPosition = y > 0.5 ? 'top' : 'bottom';
75
- }
76
- } else {
77
- // Rich variant: use corner positions
78
- if (x < 0.5 && y < 0.5) {
79
- newPosition = 'bottom-right';
80
- } else if (x >= 0.5 && y < 0.5) {
81
- newPosition = 'bottom-left';
82
- } else if (x >= 0.5 && y >= 0.5) {
83
- newPosition = 'top-left';
84
- } else {
85
- newPosition = 'top-right';
86
- }
87
- }
88
-
89
- setResolvedPosition(newPosition);
90
- }, [isOpen, targetRef, positionProp, variant]);
91
-
92
- return {
93
- resolvedPosition,
94
- };
95
- }