@vellira-ui/react-native 2.40.0 → 2.42.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 CHANGED
@@ -114,23 +114,27 @@ export function RoleSelect() {
114
114
 
115
115
  ### Dropdown Notes
116
116
 
117
- Use `Dropdown` for contextual actions, not saved form values. The `items` model
118
- is flat: use `{ type: 'group', label }` as a heading before related actions and
119
- `{ type: 'separator' }` between sections. Use `open`, `defaultOpen`, and
120
- `onOpenChange` for menu state, and `onSelect` for the selected action value.
117
+ Use `Dropdown` for contextual actions, not saved form values. Compose native
118
+ menus with `Dropdown.Trigger`, `Dropdown.Content`, `Dropdown.Item`, groups,
119
+ labels, and separators. Use `open`, `defaultOpen`, and `onOpenChange` for menu
120
+ state. Use root `color` for the semantic trigger and menu palette, and
121
+ `Dropdown.Item danger` for destructive commands.
121
122
 
122
123
  ```tsx
123
- import type { DropdownItem } from '@vellira-ui/react-native';
124
124
  import { Dropdown } from '@vellira-ui/react-native';
125
125
 
126
- const items: DropdownItem[] = [
127
- { type: 'group', label: 'File' },
128
- { label: 'Duplicate', value: 'duplicate' },
129
- { type: 'separator' },
130
- { label: 'Delete', value: 'delete', danger: true },
131
- ];
132
-
133
- <Dropdown label='Actions' items={items} onSelect={handleAction} />;
126
+ <Dropdown color='primary' label='Actions'>
127
+ <Dropdown.Content>
128
+ <Dropdown.Label>File</Dropdown.Label>
129
+ <Dropdown.Item value='duplicate' onSelect={duplicate}>
130
+ Duplicate
131
+ </Dropdown.Item>
132
+ <Dropdown.Separator />
133
+ <Dropdown.Item value='delete' danger onSelect={deleteFile}>
134
+ Delete
135
+ </Dropdown.Item>
136
+ </Dropdown.Content>
137
+ </Dropdown>;
134
138
  ```
135
139
 
136
140
  ### FormField Notes
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export type { SelectOption, SelectProps } from './components/Select';
8
8
  export { Select } from './components/Select';
9
9
  export type { TabsContentProps, TabsListProps, TabsProps, TabsTriggerProps, } from './components/Tabs';
10
10
  export { Tabs } from './components/Tabs';
11
- export type { TooltipProps } from './components/Tooltip';
11
+ export type { TooltipArrowProps, TooltipContentProps, TooltipProps, TooltipRootProps, TooltipTriggerProps, } from './components/Tooltip';
12
12
  export { Tooltip } from './components/Tooltip';
13
13
  export type { FormFieldProps } from './patterns/FormField';
14
14
  export { FormField } from './patterns/FormField';
package/dist/index.js CHANGED
@@ -1,8 +1,133 @@
1
1
  import { Children, cloneElement, createContext, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import { Check, ChevronDown, Close, Search } from "@vellira-ui/icons";
3
- import { AccessibilityInfo, ActivityIndicator, Animated, Dimensions, FlatList, Modal as Modal$1, Pressable, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle, useWindowDimensions } from "react-native";
3
+ import { AccessibilityInfo, ActivityIndicator, Animated, Dimensions, Easing, FlatList, Modal as Modal$1, Pressable, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle, useWindowDimensions } from "react-native";
4
4
  import { darkTheme, highContrastTheme, lightTheme } from "@vellira-ui/tokens";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/managers/FloatingManager/useNativeFloatingPosition.ts
7
+ const safePadding = 12;
8
+ function useNativeFloatingPosition(placement = "top", offset = 8) {
9
+ const [position, setPosition] = useState({
10
+ top: 0,
11
+ left: 0
12
+ });
13
+ const floatingSizeRef = useRef({
14
+ width: 0,
15
+ height: 0
16
+ });
17
+ const lastTriggerRef = useRef(null);
18
+ const clamp = useCallback((value, min, max) => {
19
+ return Math.min(Math.max(value, min), Math.max(min, max));
20
+ }, []);
21
+ const calculatePosition = useCallback((triggerRect, size) => {
22
+ const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
23
+ const [side, align = "center"] = placement.split("-");
24
+ const horizontalTop = side === "bottom" ? triggerRect.y + triggerRect.height + offset : triggerRect.y - size.height - offset;
25
+ const verticalTop = align === "start" ? triggerRect.y : align === "end" ? triggerRect.y + triggerRect.height - size.height : triggerRect.y + triggerRect.height / 2 - size.height / 2;
26
+ const horizontalLeft = align === "start" ? triggerRect.x : align === "end" ? triggerRect.x + triggerRect.width - size.width : triggerRect.x + triggerRect.width / 2 - size.width / 2;
27
+ const verticalLeft = side === "right" ? triggerRect.x + triggerRect.width + offset : triggerRect.x - size.width - offset;
28
+ const rawPosition = side === "left" || side === "right" ? {
29
+ top: verticalTop,
30
+ left: verticalLeft
31
+ } : {
32
+ top: horizontalTop,
33
+ left: horizontalLeft
34
+ };
35
+ return {
36
+ top: clamp(rawPosition.top, safePadding, screenHeight - size.height - safePadding),
37
+ left: clamp(rawPosition.left, safePadding, screenWidth - size.width - safePadding)
38
+ };
39
+ }, [
40
+ placement,
41
+ offset,
42
+ clamp
43
+ ]);
44
+ const updatePosition = useCallback((triggerRef, measuredSize = floatingSizeRef.current) => {
45
+ lastTriggerRef.current = triggerRef;
46
+ const node = triggerRef.current;
47
+ if (!node || typeof node.measureInWindow !== "function") {
48
+ setPosition({
49
+ top: 0,
50
+ left: 0
51
+ });
52
+ return;
53
+ }
54
+ node.measureInWindow((x, y, width, height) => {
55
+ setPosition(calculatePosition({
56
+ x,
57
+ y,
58
+ width,
59
+ height
60
+ }, measuredSize));
61
+ });
62
+ }, [calculatePosition]);
63
+ return {
64
+ position,
65
+ updatePosition,
66
+ onFloatingLayout: useCallback((event) => {
67
+ const { width, height } = event.nativeEvent.layout;
68
+ const nextSize = {
69
+ width,
70
+ height
71
+ };
72
+ floatingSizeRef.current = nextSize;
73
+ if (lastTriggerRef.current) updatePosition(lastTriggerRef.current, nextSize);
74
+ }, [updatePosition])
75
+ };
76
+ }
77
+ //#endregion
78
+ //#region src/managers/OverlayStack/NativeOverlayStack.ts
79
+ let stack = [];
80
+ const nativeOverlayStackStore = {
81
+ add(id) {
82
+ stack = stack.filter((item) => item !== id);
83
+ stack.push(id);
84
+ },
85
+ remove(id) {
86
+ stack = stack.filter((item) => item !== id);
87
+ },
88
+ isTop(id) {
89
+ return stack[stack.length - 1] === id;
90
+ }
91
+ };
92
+ //#endregion
93
+ //#region src/managers/OverlayStack/useNativeOverlayStack.ts
94
+ const useNativeOverlayStack = ({ id, visible }) => {
95
+ useEffect(() => {
96
+ if (!visible) return;
97
+ nativeOverlayStackStore.add(id);
98
+ return () => {
99
+ nativeOverlayStackStore.remove(id);
100
+ };
101
+ }, [id, visible]);
102
+ return { isTopOverlay: useCallback(() => nativeOverlayStackStore.isTop(id), [id]) };
103
+ };
104
+ //#endregion
105
+ //#region src/hooks/behavior/overlay/useOverlayStack.ts
106
+ const useOverlayStack = ({ active, id }) => useNativeOverlayStack({
107
+ id,
108
+ visible: active
109
+ });
110
+ //#endregion
111
+ //#region src/hooks/behavior/overlay/useOverlayDismiss.ts
112
+ const useOverlayDismiss = ({ active, closeOnOutsidePress = true, id, requestClose }) => {
113
+ const { isTopOverlay } = useOverlayStack({
114
+ active,
115
+ id
116
+ });
117
+ const requestTopClose = useCallback(() => {
118
+ if (!isTopOverlay()) return;
119
+ requestClose();
120
+ }, [isTopOverlay, requestClose]);
121
+ return {
122
+ isTopOverlay,
123
+ requestClose: requestTopClose,
124
+ requestOutsideClose: useCallback(() => {
125
+ if (!closeOnOutsidePress) return;
126
+ requestTopClose();
127
+ }, [closeOnOutsidePress, requestTopClose])
128
+ };
129
+ };
130
+ //#endregion
6
131
  //#region src/hooks/useControllableState.ts
7
132
  const useControllableState = ({ value, defaultValue, onChange }) => {
8
133
  const [internalValue, setInternalValue] = useState(defaultValue);
@@ -515,9 +640,10 @@ const createStyles$21 = (theme) => StyleSheet.create({
515
640
  });
516
641
  //#endregion
517
642
  //#region src/components/Dropdown/Content/DropdownContent.tsx
518
- function DropdownContent({ isOpen, children, onClose, contentStyle, accessibilityLabel, presentation, searchable = false, searchValue = "", searchPlaceholder = "Search actions...", searchAccessibilityLabel, onSearchChange }) {
643
+ function DropdownContent({ isOpen, children, onClose, color = "primary", contentStyle, accessibilityLabel, presentation, searchable = false, searchValue = "", searchPlaceholder = "Search actions...", searchAccessibilityLabel, onSearchChange }) {
519
644
  const { theme } = useTheme();
520
645
  const styles = useThemeStyles(createStyles$21);
646
+ const colorPalette = theme.components.dropdown[color];
521
647
  const isSheet = presentation === "sheet";
522
648
  const animation = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
523
649
  const [reduceMotion, setReduceMotion] = useState(false);
@@ -589,6 +715,7 @@ function DropdownContent({ isOpen, children, onClose, contentStyle, accessibilit
589
715
  style: [
590
716
  styles.menu,
591
717
  styles[`${presentation}Menu`],
718
+ { borderColor: colorPalette.content.border },
592
719
  contentStyle,
593
720
  menuAnimatedStyle
594
721
  ],
@@ -619,125 +746,6 @@ function DropdownContent({ isOpen, children, onClose, contentStyle, accessibilit
619
746
  }
620
747
  DropdownContent.displayName = "DropdownContent";
621
748
  //#endregion
622
- //#region src/managers/FloatingManager/useNativeFloatingPosition.ts
623
- const safePadding = 12;
624
- function useNativeFloatingPosition(placement = "top", offset = 8) {
625
- const [position, setPosition] = useState({
626
- top: 0,
627
- left: 0
628
- });
629
- const floatingSizeRef = useRef({
630
- width: 0,
631
- height: 0
632
- });
633
- const lastTriggerRef = useRef(null);
634
- const clamp = useCallback((value, min, max) => {
635
- return Math.min(Math.max(value, min), Math.max(min, max));
636
- }, []);
637
- const calculatePosition = useCallback((triggerRect, size) => {
638
- const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
639
- const [side, align = "center"] = placement.split("-");
640
- const horizontalTop = side === "bottom" ? triggerRect.y + triggerRect.height + offset : triggerRect.y - size.height - offset;
641
- const verticalTop = align === "start" ? triggerRect.y : align === "end" ? triggerRect.y + triggerRect.height - size.height : triggerRect.y + triggerRect.height / 2 - size.height / 2;
642
- const horizontalLeft = align === "start" ? triggerRect.x : align === "end" ? triggerRect.x + triggerRect.width - size.width : triggerRect.x + triggerRect.width / 2 - size.width / 2;
643
- const verticalLeft = side === "right" ? triggerRect.x + triggerRect.width + offset : triggerRect.x - size.width - offset;
644
- const rawPosition = side === "left" || side === "right" ? {
645
- top: verticalTop,
646
- left: verticalLeft
647
- } : {
648
- top: horizontalTop,
649
- left: horizontalLeft
650
- };
651
- return {
652
- top: clamp(rawPosition.top, safePadding, screenHeight - size.height - safePadding),
653
- left: clamp(rawPosition.left, safePadding, screenWidth - size.width - safePadding)
654
- };
655
- }, [
656
- placement,
657
- offset,
658
- clamp
659
- ]);
660
- const updatePosition = useCallback((triggerRef, measuredSize = floatingSizeRef.current) => {
661
- lastTriggerRef.current = triggerRef;
662
- const node = triggerRef.current;
663
- if (!node || typeof node.measureInWindow !== "function") {
664
- setPosition({
665
- top: 0,
666
- left: 0
667
- });
668
- return;
669
- }
670
- node.measureInWindow((x, y, width, height) => {
671
- setPosition(calculatePosition({
672
- x,
673
- y,
674
- width,
675
- height
676
- }, measuredSize));
677
- });
678
- }, [calculatePosition]);
679
- return {
680
- position,
681
- updatePosition,
682
- onFloatingLayout: useCallback((event) => {
683
- const { width, height } = event.nativeEvent.layout;
684
- const nextSize = {
685
- width,
686
- height
687
- };
688
- floatingSizeRef.current = nextSize;
689
- if (lastTriggerRef.current) updatePosition(lastTriggerRef.current, nextSize);
690
- }, [updatePosition])
691
- };
692
- }
693
- //#endregion
694
- //#region src/managers/OverlayStack/NativeOverlayStack.ts
695
- let stack = [];
696
- const nativeOverlayStackStore = {
697
- add(id) {
698
- stack = stack.filter((item) => item !== id);
699
- stack.push(id);
700
- },
701
- remove(id) {
702
- stack = stack.filter((item) => item !== id);
703
- },
704
- isTop(id) {
705
- return stack[stack.length - 1] === id;
706
- }
707
- };
708
- //#endregion
709
- //#region src/managers/OverlayStack/useNativeOverlayStack.ts
710
- const useNativeOverlayStack = ({ id, visible }) => {
711
- useEffect(() => {
712
- if (!visible) return;
713
- nativeOverlayStackStore.add(id);
714
- return () => {
715
- nativeOverlayStackStore.remove(id);
716
- };
717
- }, [id, visible]);
718
- return { isTopOverlay: useCallback(() => nativeOverlayStackStore.isTop(id), [id]) };
719
- };
720
- //#endregion
721
- //#region src/managers/OverlayStack/useNativeDismiss.ts
722
- const useNativeDismiss = ({ id, visible, closeOnOutsidePress = true, onClose }) => {
723
- const { isTopOverlay } = useNativeOverlayStack({
724
- id,
725
- visible
726
- });
727
- const requestClose = useCallback(() => {
728
- if (!isTopOverlay()) return;
729
- onClose();
730
- }, [isTopOverlay, onClose]);
731
- return {
732
- isTopOverlay,
733
- requestClose,
734
- requestOutsideClose: useCallback(() => {
735
- if (!closeOnOutsidePress) return;
736
- requestClose();
737
- }, [closeOnOutsidePress, requestClose])
738
- };
739
- };
740
- //#endregion
741
749
  //#region src/components/Dropdown/Group/DropdownGroup.styles.ts
742
750
  const createStyles$20 = (theme) => StyleSheet.create({ groupLabel: {
743
751
  paddingHorizontal: theme.tokens.spacing[4],
@@ -875,10 +883,6 @@ const createStyles$19 = (theme) => StyleSheet.create({
875
883
  backgroundColor: theme.components.dropdown.item.default.bg,
876
884
  borderRadius: theme.tokens.radius.sm
877
885
  },
878
- itemPressed: { backgroundColor: theme.components.dropdown.item.pressed.bg },
879
- itemDisabled: { backgroundColor: theme.components.dropdown.item.disabled.bg },
880
- itemDanger: { backgroundColor: theme.components.dropdown.item.danger.default.bg },
881
- itemDangerPressed: { backgroundColor: theme.components.dropdown.item.danger.active.bg },
882
886
  itemText: {
883
887
  flex: 1,
884
888
  minWidth: 0,
@@ -886,17 +890,14 @@ const createStyles$19 = (theme) => StyleSheet.create({
886
890
  fontFamily: theme.tokens.typography.family.regular,
887
891
  fontSize: theme.tokens.typography.size.md,
888
892
  lineHeight: theme.tokens.typography.lineHeight.md
889
- },
890
- itemTextPressed: { color: theme.components.dropdown.item.pressed.fg },
891
- itemTextDisabled: { color: theme.components.dropdown.item.disabled.fg },
892
- itemTextDanger: { color: theme.components.dropdown.item.danger.default.fg },
893
- itemTextDangerPressed: { color: theme.components.dropdown.item.danger.active.fg }
893
+ }
894
894
  });
895
895
  //#endregion
896
896
  //#region src/components/Dropdown/Item/DropdownItem.tsx
897
- function DropdownItem({ label, value, icon, danger = false, disabled = false, textWrap = "truncate", itemStyle, textStyle, onSelect }) {
897
+ function DropdownItem({ label, value, color = "primary", icon, danger = false, disabled = false, textWrap = "truncate", itemStyle, textStyle, onSelect }) {
898
898
  const { theme } = useTheme();
899
899
  const styles = useThemeStyles(createStyles$19);
900
+ const colorPalette = theme.components.dropdown[color];
900
901
  const renderColoredNode = (node, color) => {
901
902
  if (!isValidElement(node)) return node;
902
903
  return cloneElement(node, { color });
@@ -904,12 +905,12 @@ function DropdownItem({ label, value, icon, danger = false, disabled = false, te
904
905
  const getContentColor = (pressed) => {
905
906
  if (disabled) return theme.components.dropdown.item.disabled.fg;
906
907
  if (danger) return pressed ? theme.components.dropdown.item.danger.active.fg : theme.components.dropdown.item.danger.default.fg;
907
- return pressed ? theme.components.dropdown.item.pressed.fg : theme.components.dropdown.item.default.fg;
908
+ return pressed ? colorPalette.item.pressed.fg : theme.components.dropdown.item.default.fg;
908
909
  };
909
910
  const getBackgroundColor = (pressed) => {
910
911
  if (disabled) return theme.components.dropdown.item.disabled.bg;
911
912
  if (danger) return pressed ? theme.components.dropdown.item.danger.active.bg : theme.components.dropdown.item.danger.default.bg;
912
- return pressed ? theme.components.dropdown.item.pressed.bg : theme.components.dropdown.item.default.bg;
913
+ return pressed ? colorPalette.item.pressed.bg : theme.components.dropdown.item.default.bg;
913
914
  };
914
915
  const accessibilityLabel = typeof label === "string" ? label : value;
915
916
  const numberOfLines = textWrap === "wrap" ? void 0 : 1;
@@ -1009,10 +1010,6 @@ const createStyles$17 = (theme) => StyleSheet.create({
1009
1010
  minHeight: 52,
1010
1011
  padding: 0
1011
1012
  },
1012
- triggerPressed: {
1013
- backgroundColor: theme.components.dropdown.trigger.hover.bg,
1014
- borderColor: theme.components.dropdown.trigger.hover.border
1015
- },
1016
1013
  triggerDisabled: {
1017
1014
  backgroundColor: theme.components.dropdown.trigger.disabled.bg,
1018
1015
  borderColor: theme.components.dropdown.trigger.disabled.border,
@@ -1022,7 +1019,6 @@ const createStyles$17 = (theme) => StyleSheet.create({
1022
1019
  color: theme.components.dropdown.trigger.default.fg,
1023
1020
  fontFamily: theme.tokens.typography.family.regular
1024
1021
  },
1025
- triggerTextPressed: { color: theme.components.dropdown.trigger.hover.fg },
1026
1022
  triggerTextDisabled: { color: theme.components.dropdown.trigger.disabled.fg },
1027
1023
  textSm: {
1028
1024
  fontSize: theme.tokens.typography.size.sm,
@@ -1049,9 +1045,10 @@ const createStyles$17 = (theme) => StyleSheet.create({
1049
1045
  });
1050
1046
  //#endregion
1051
1047
  //#region src/components/Dropdown/Trigger/DropdownTrigger.tsx
1052
- function DropdownTrigger({ label, trigger, children, icon, arrowIcon, showArrow = true, size = "md", disabled = false, isOpen, triggerStyle, triggerRef, accessibilityLabel, accessibilityHint, onPress }) {
1048
+ function DropdownTrigger({ asChild = false, label, trigger, children, icon, arrowIcon, showArrow = true, color = "primary", size = "md", disabled = false, isOpen, triggerStyle, triggerRef, accessibilityLabel, accessibilityHint, onPress }) {
1053
1049
  const { theme } = useTheme();
1054
1050
  const styles = useThemeStyles(createStyles$17);
1051
+ const colorPalette = theme.components.dropdown[color];
1055
1052
  const textSizeStyle = {
1056
1053
  sm: styles.textSm,
1057
1054
  md: styles.textMd,
@@ -1091,7 +1088,7 @@ function DropdownTrigger({ label, trigger, children, icon, arrowIcon, showArrow
1091
1088
  if (!isValidElement(node)) return node;
1092
1089
  return cloneElement(node, { color });
1093
1090
  };
1094
- const contentColor = disabled ? theme.components.dropdown.trigger.disabled.fg : isPressed ? theme.components.dropdown.trigger.hover.fg : theme.components.dropdown.trigger.default.fg;
1091
+ const contentColor = disabled ? theme.components.dropdown.trigger.disabled.fg : isPressed ? colorPalette.trigger.hover.fg : colorPalette.trigger.default.fg;
1095
1092
  const arrow = arrowIcon ? renderColoredNode(arrowIcon, contentColor) : /* @__PURE__ */ jsx(ChevronDown, {
1096
1093
  width: 16,
1097
1094
  height: 16,
@@ -1099,7 +1096,7 @@ function DropdownTrigger({ label, trigger, children, icon, arrowIcon, showArrow
1099
1096
  });
1100
1097
  const renderedIcon = icon ? renderColoredNode(icon, contentColor) : null;
1101
1098
  const triggerContent = trigger ?? children;
1102
- if (isValidElement(triggerContent)) {
1099
+ if (asChild && isValidElement(triggerContent)) {
1103
1100
  const child = triggerContent;
1104
1101
  const isChildDisabled = disabled || child.props.disabled;
1105
1102
  return cloneElement(child, {
@@ -1133,9 +1130,16 @@ function DropdownTrigger({ label, trigger, children, icon, arrowIcon, showArrow
1133
1130
  style: [
1134
1131
  styles.trigger,
1135
1132
  styles[size],
1133
+ {
1134
+ backgroundColor: colorPalette.trigger.default.bg,
1135
+ borderColor: colorPalette.trigger.default.border
1136
+ },
1136
1137
  isIconOnly && styles.iconOnly,
1137
1138
  isIconOnly && iconOnlySizeStyle,
1138
- isPressed && !disabled && styles.triggerPressed,
1139
+ isPressed && !disabled && {
1140
+ backgroundColor: colorPalette.trigger.hover.bg,
1141
+ borderColor: colorPalette.trigger.hover.border
1142
+ },
1139
1143
  disabled && styles.triggerDisabled,
1140
1144
  triggerStyle
1141
1145
  ],
@@ -1178,7 +1182,7 @@ const createStyles$16 = (theme) => StyleSheet.create({
1178
1182
  });
1179
1183
  //#endregion
1180
1184
  //#region src/components/Dropdown/Dropdown.tsx
1181
- function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, showArrow = true, open, defaultOpen = false, onOpenChange, presentation = "auto", closeOnSelect = true, disabled = false, loading = false, loadingText = "Loading actions...", searchable = false, command = false, searchValue, defaultSearchValue = "", searchPlaceholder, onSearch, empty, noOptionsText, size = "md", style, triggerStyle, contentStyle, itemStyle, textStyle, accessibilityLabel, accessibilityHint }) {
1185
+ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, showArrow = true, open, defaultOpen = false, onOpenChange, presentation = "auto", closeOnSelect = true, color = "primary", disabled = false, loading = false, loadingText = "Loading actions...", searchable = false, command = false, searchValue, defaultSearchValue = "", searchPlaceholder, onSearch, empty, noOptionsText, size = "md", style, triggerStyle, contentStyle, itemStyle, textStyle, accessibilityLabel, accessibilityHint }) {
1182
1186
  const styles = useThemeStyles(createStyles$16);
1183
1187
  const overlayId = useId();
1184
1188
  const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState(defaultSearchValue);
@@ -1228,10 +1232,10 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1228
1232
  closeDropdown();
1229
1233
  requestAnimationFrame(focusTrigger);
1230
1234
  }, [closeDropdown, focusTrigger]);
1231
- const dismiss = useNativeDismiss({
1235
+ const dismiss = useOverlayDismiss({
1232
1236
  id: overlayId,
1233
- visible: isOpen,
1234
- onClose: closeAndFocusTrigger
1237
+ active: isOpen,
1238
+ requestClose: closeAndFocusTrigger
1235
1239
  });
1236
1240
  const handleSelect = useCallback((entry) => {
1237
1241
  if (entry.disabled || loading) return;
@@ -1265,6 +1269,7 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1265
1269
  return /* @__PURE__ */ jsx(DropdownItem, {
1266
1270
  label: item.props.children,
1267
1271
  value: item.props.value ?? item.id,
1272
+ color,
1268
1273
  icon: item.props.icon,
1269
1274
  danger: item.props.danger,
1270
1275
  disabled: item.props.disabled,
@@ -1274,6 +1279,7 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1274
1279
  onSelect: () => handleSelect(item)
1275
1280
  });
1276
1281
  }, [
1282
+ color,
1277
1283
  handleSelect,
1278
1284
  itemStyle,
1279
1285
  styles.emptyText,
@@ -1293,11 +1299,13 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1293
1299
  return /* @__PURE__ */ jsxs(View, {
1294
1300
  style: [styles.root, style],
1295
1301
  children: [/* @__PURE__ */ jsx(DropdownTrigger, {
1302
+ asChild: Boolean(parsed.trigger),
1296
1303
  label,
1297
1304
  trigger: trigger ?? parsed.trigger,
1298
1305
  icon,
1299
1306
  arrowIcon,
1300
1307
  showArrow,
1308
+ color,
1301
1309
  disabled: disabled || parsed.triggerProps?.disabled,
1302
1310
  isOpen,
1303
1311
  size,
@@ -1311,6 +1319,7 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1311
1319
  }), /* @__PURE__ */ jsx(DropdownContent, {
1312
1320
  isOpen,
1313
1321
  onClose: dismiss.requestClose,
1322
+ color,
1314
1323
  contentStyle: [contentStyle, contentStyleFromSlot],
1315
1324
  accessibilityLabel: menuAccessibilityLabel,
1316
1325
  presentation: presentationFromSlot ?? resolvedPresentation,
@@ -1576,11 +1585,11 @@ const ModalRoot = ({ open, defaultOpen = false, onOpenChange, closeOnOutsidePres
1576
1585
  onOpenChange,
1577
1586
  closeOnOutsidePress
1578
1587
  });
1579
- const dismiss = useNativeDismiss({
1588
+ const dismiss = useOverlayDismiss({
1580
1589
  id: modal.contentId,
1581
- visible: modal.open,
1590
+ active: modal.open,
1582
1591
  closeOnOutsidePress: modal.closeOnOutsidePress,
1583
- onClose: modal.requestClose
1592
+ requestClose: modal.requestClose
1584
1593
  });
1585
1594
  return /* @__PURE__ */ jsx(ModalContext.Provider, {
1586
1595
  value: {
@@ -3475,11 +3484,11 @@ function SelectRoot(props) {
3475
3484
  fieldDescribedBy: field?.ariaDescribedBy
3476
3485
  });
3477
3486
  const hasValue = selectedValues.length > 0;
3478
- const dismiss = useNativeDismiss({
3487
+ const dismiss = useOverlayDismiss({
3479
3488
  id: overlayId,
3480
- visible: isOpen,
3489
+ active: isOpen,
3481
3490
  closeOnOutsidePress: dismissOnBackdropPress,
3482
- onClose: closeDropdown
3491
+ requestClose: closeDropdown
3483
3492
  });
3484
3493
  const clearValue = () => {
3485
3494
  selectedFocusValueRef.current = void 0;
@@ -3699,18 +3708,216 @@ const TabsContent = ({ value, children, forceMount = false, style }) => {
3699
3708
  TabsContent.displayName = "Tabs.Content";
3700
3709
  //#endregion
3701
3710
  //#region src/components/Tabs/List/TabsIndicator.tsx
3702
- const TabsIndicator = ({ children, style }) => /* @__PURE__ */ jsx(View, {
3703
- accessibilityElementsHidden: true,
3704
- importantForAccessibility: "no-hide-descendants",
3705
- pointerEvents: "none",
3706
- style,
3707
- children
3711
+ const COLLAPSED_SIZE = 8;
3712
+ const LINE_ANIMATION_DURATION = 360;
3713
+ const SURFACE_ANIMATION_DURATION = 220;
3714
+ const easing = Easing?.bezier?.(.22, 1, .36, 1) ?? ((value) => value);
3715
+ const animateValue = (value, toValue, duration) => Animated.timing(value, {
3716
+ toValue,
3717
+ duration,
3718
+ easing,
3719
+ useNativeDriver: false
3708
3720
  });
3721
+ const TabsIndicator = ({ children, style }) => {
3722
+ const { theme } = useTheme();
3723
+ const { value, orientation, variant, color, size, indicatorVersion, getTriggerLayout } = useTabs();
3724
+ const translateX = useRef(new Animated.Value(0)).current;
3725
+ const translateY = useRef(new Animated.Value(0)).current;
3726
+ const width = useRef(new Animated.Value(0)).current;
3727
+ const height = useRef(new Animated.Value(0)).current;
3728
+ const previousLayoutRef = useRef(null);
3729
+ const previousValueRef = useRef(void 0);
3730
+ const previousOrientationRef = useRef(orientation);
3731
+ const animationRef = useRef(null);
3732
+ const [reduceMotion, setReduceMotion] = useState(false);
3733
+ const [visible, setVisible] = useState(false);
3734
+ const isVertical = orientation === "vertical";
3735
+ const palette = theme.components.tabs[color];
3736
+ useEffect(() => {
3737
+ AccessibilityInfo.isReduceMotionEnabled?.().then(setReduceMotion);
3738
+ const subscription = AccessibilityInfo.addEventListener?.("reduceMotionChanged", setReduceMotion);
3739
+ return () => {
3740
+ subscription?.remove();
3741
+ animationRef.current?.stop();
3742
+ };
3743
+ }, []);
3744
+ useEffect(() => {
3745
+ if (!value) {
3746
+ setVisible(false);
3747
+ return;
3748
+ }
3749
+ const nextLayout = getTriggerLayout(value);
3750
+ if (!nextLayout || nextLayout.width <= 0 || nextLayout.height <= 0) {
3751
+ setVisible(false);
3752
+ return;
3753
+ }
3754
+ setVisible(true);
3755
+ animationRef.current?.stop();
3756
+ const previousLayout = previousLayoutRef.current;
3757
+ if (!(!reduceMotion && previousLayout && previousValueRef.current !== value && previousOrientationRef.current === orientation)) {
3758
+ translateX.setValue(nextLayout.x);
3759
+ translateY.setValue(nextLayout.y);
3760
+ width.setValue(nextLayout.width);
3761
+ height.setValue(nextLayout.height);
3762
+ previousLayoutRef.current = nextLayout;
3763
+ previousValueRef.current = value;
3764
+ previousOrientationRef.current = orientation;
3765
+ return;
3766
+ }
3767
+ if (variant === "line" && isVertical) {
3768
+ const previousCenter = previousLayout.y + previousLayout.height / 2;
3769
+ const nextCenter = nextLayout.y + nextLayout.height / 2;
3770
+ const collapsedPreviousY = previousCenter - COLLAPSED_SIZE / 2;
3771
+ const collapsedNextY = nextCenter - COLLAPSED_SIZE / 2;
3772
+ translateY.setValue(previousLayout.y);
3773
+ height.setValue(previousLayout.height);
3774
+ animationRef.current = Animated.sequence([
3775
+ Animated.parallel([animateValue(height, COLLAPSED_SIZE, LINE_ANIMATION_DURATION * .28), animateValue(translateY, collapsedPreviousY, LINE_ANIMATION_DURATION * .28)]),
3776
+ animateValue(translateY, collapsedNextY, LINE_ANIMATION_DURATION * .36),
3777
+ Animated.parallel([animateValue(height, nextLayout.height, LINE_ANIMATION_DURATION * .36), animateValue(translateY, nextLayout.y, LINE_ANIMATION_DURATION * .36)])
3778
+ ]);
3779
+ } else if (variant === "line") {
3780
+ const previousCenter = previousLayout.x + previousLayout.width / 2;
3781
+ const nextCenter = nextLayout.x + nextLayout.width / 2;
3782
+ const collapsedPreviousX = previousCenter - COLLAPSED_SIZE / 2;
3783
+ const collapsedNextX = nextCenter - COLLAPSED_SIZE / 2;
3784
+ translateX.setValue(previousLayout.x);
3785
+ width.setValue(previousLayout.width);
3786
+ animationRef.current = Animated.sequence([
3787
+ Animated.parallel([animateValue(width, COLLAPSED_SIZE, LINE_ANIMATION_DURATION * .28), animateValue(translateX, collapsedPreviousX, LINE_ANIMATION_DURATION * .28)]),
3788
+ animateValue(translateX, collapsedNextX, LINE_ANIMATION_DURATION * .36),
3789
+ Animated.parallel([animateValue(width, nextLayout.width, LINE_ANIMATION_DURATION * .36), animateValue(translateX, nextLayout.x, LINE_ANIMATION_DURATION * .36)])
3790
+ ]);
3791
+ } else {
3792
+ translateX.setValue(previousLayout.x);
3793
+ translateY.setValue(previousLayout.y);
3794
+ width.setValue(previousLayout.width);
3795
+ height.setValue(previousLayout.height);
3796
+ animationRef.current = Animated.parallel([
3797
+ animateValue(translateX, nextLayout.x, SURFACE_ANIMATION_DURATION),
3798
+ animateValue(translateY, nextLayout.y, SURFACE_ANIMATION_DURATION),
3799
+ animateValue(width, nextLayout.width, SURFACE_ANIMATION_DURATION),
3800
+ animateValue(height, nextLayout.height, SURFACE_ANIMATION_DURATION)
3801
+ ]);
3802
+ }
3803
+ animationRef.current.start(() => {
3804
+ translateX.setValue(nextLayout.x);
3805
+ translateY.setValue(nextLayout.y);
3806
+ width.setValue(nextLayout.width);
3807
+ height.setValue(nextLayout.height);
3808
+ });
3809
+ previousLayoutRef.current = nextLayout;
3810
+ previousValueRef.current = value;
3811
+ previousOrientationRef.current = orientation;
3812
+ }, [
3813
+ getTriggerLayout,
3814
+ height,
3815
+ indicatorVersion,
3816
+ isVertical,
3817
+ orientation,
3818
+ reduceMotion,
3819
+ translateX,
3820
+ translateY,
3821
+ value,
3822
+ variant,
3823
+ width
3824
+ ]);
3825
+ const indicatorStyle = useMemo(() => {
3826
+ const baseStyle = {
3827
+ position: "absolute",
3828
+ opacity: visible ? 1 : 0,
3829
+ zIndex: 0
3830
+ };
3831
+ if (variant === "line") return [
3832
+ baseStyle,
3833
+ {
3834
+ backgroundColor: palette.indicator.bg,
3835
+ borderRadius: theme.tokens.radius.full
3836
+ },
3837
+ isVertical ? {
3838
+ top: 0,
3839
+ left: 0,
3840
+ width: 3,
3841
+ height,
3842
+ transform: [{ translateY }]
3843
+ } : {
3844
+ right: void 0,
3845
+ bottom: 0,
3846
+ left: 0,
3847
+ width,
3848
+ height: 3,
3849
+ transform: [{ translateX }]
3850
+ },
3851
+ style
3852
+ ];
3853
+ if (variant === "pills") return [
3854
+ baseStyle,
3855
+ {
3856
+ top: 0,
3857
+ left: 0,
3858
+ width,
3859
+ height,
3860
+ backgroundColor: palette.pills.active.bg,
3861
+ borderColor: palette.pills.active.border,
3862
+ borderRadius: theme.tokens.radius[size === "lg" ? "lg" : size === "sm" ? "sm" : "md"],
3863
+ borderWidth: 1,
3864
+ transform: [{ translateX }, { translateY }]
3865
+ },
3866
+ style
3867
+ ];
3868
+ return [
3869
+ baseStyle,
3870
+ {
3871
+ top: 0,
3872
+ left: 0,
3873
+ width,
3874
+ height,
3875
+ backgroundColor: palette.segmented.active.bg,
3876
+ borderColor: palette.segmented.active.border,
3877
+ borderRadius: theme.tokens.radius.lg,
3878
+ borderWidth: 1,
3879
+ shadowColor: "#181521",
3880
+ shadowOffset: {
3881
+ width: 0,
3882
+ height: 1
3883
+ },
3884
+ shadowOpacity: .06,
3885
+ shadowRadius: 2,
3886
+ transform: [{ translateX }, { translateY }]
3887
+ },
3888
+ style
3889
+ ];
3890
+ }, [
3891
+ height,
3892
+ isVertical,
3893
+ palette.indicator.bg,
3894
+ palette.pills.active.bg,
3895
+ palette.pills.active.border,
3896
+ palette.segmented.active.bg,
3897
+ palette.segmented.active.border,
3898
+ size,
3899
+ style,
3900
+ theme.tokens.radius,
3901
+ translateX,
3902
+ translateY,
3903
+ variant,
3904
+ visible,
3905
+ width
3906
+ ]);
3907
+ return /* @__PURE__ */ jsx(Animated.View, {
3908
+ accessibilityElementsHidden: true,
3909
+ importantForAccessibility: "no-hide-descendants",
3910
+ pointerEvents: "none",
3911
+ style: indicatorStyle,
3912
+ children
3913
+ });
3914
+ };
3709
3915
  TabsIndicator.displayName = "Tabs.Indicator";
3710
3916
  //#endregion
3711
3917
  //#region src/components/Tabs/List/TabsList.styles.ts
3712
3918
  const createStyles$6 = (theme) => StyleSheet.create({
3713
3919
  list: {
3920
+ position: "relative",
3714
3921
  flexDirection: "row",
3715
3922
  alignSelf: "stretch",
3716
3923
  width: "100%",
@@ -3775,31 +3982,62 @@ const createStyles$5 = (theme) => StyleSheet.create({
3775
3982
  justifyContent: "center",
3776
3983
  paddingHorizontal: theme.tokens.spacing[4],
3777
3984
  paddingVertical: theme.tokens.spacing[2],
3778
- borderWidth: 1
3985
+ borderWidth: 1,
3986
+ zIndex: 1
3987
+ },
3988
+ tabIconOnly: {
3989
+ width: 44,
3990
+ minWidth: 44,
3991
+ paddingHorizontal: 0
3779
3992
  },
3780
3993
  tabSm: {
3781
3994
  minHeight: 36,
3782
3995
  paddingHorizontal: theme.tokens.spacing[3],
3783
3996
  paddingVertical: 6
3784
3997
  },
3998
+ tabIconOnlySm: {
3999
+ width: 36,
4000
+ minWidth: 36,
4001
+ paddingHorizontal: 0
4002
+ },
3785
4003
  tabLg: {
3786
4004
  minHeight: 52,
3787
4005
  paddingHorizontal: theme.tokens.spacing[5],
3788
4006
  paddingVertical: theme.tokens.spacing[3]
3789
4007
  },
4008
+ tabIconOnlyLg: {
4009
+ width: 52,
4010
+ minWidth: 52,
4011
+ paddingHorizontal: 0
4012
+ },
3790
4013
  tabSegmented: {
3791
4014
  minHeight: 32,
3792
4015
  paddingVertical: 5,
3793
4016
  borderRadius: theme.tokens.radius.lg
3794
4017
  },
4018
+ tabSegmentedIconOnly: {
4019
+ width: 32,
4020
+ minWidth: 32,
4021
+ paddingHorizontal: 0
4022
+ },
3795
4023
  tabSegmentedSm: {
3796
4024
  minHeight: 30,
3797
4025
  paddingVertical: 4
3798
4026
  },
4027
+ tabSegmentedIconOnlySm: {
4028
+ width: 30,
4029
+ minWidth: 30,
4030
+ paddingHorizontal: 0
4031
+ },
3799
4032
  tabSegmentedLg: {
3800
4033
  minHeight: 40,
3801
4034
  paddingVertical: 8
3802
4035
  },
4036
+ tabSegmentedIconOnlyLg: {
4037
+ width: 40,
4038
+ minWidth: 40,
4039
+ paddingHorizontal: 0
4040
+ },
3803
4041
  tabVertical: {
3804
4042
  position: "relative",
3805
4043
  width: "100%",
@@ -3808,25 +4046,6 @@ const createStyles$5 = (theme) => StyleSheet.create({
3808
4046
  flexShrink: 0,
3809
4047
  justifyContent: "flex-start"
3810
4048
  },
3811
- horizontalIndicator: {
3812
- position: "absolute",
3813
- right: 0,
3814
- bottom: 0,
3815
- left: 0,
3816
- height: 3,
3817
- backgroundColor: "transparent"
3818
- },
3819
- horizontalIndicatorActive: { backgroundColor: "transparent" },
3820
- verticalIndicator: {
3821
- position: "absolute",
3822
- top: 0,
3823
- bottom: 0,
3824
- left: 0,
3825
- width: 3,
3826
- backgroundColor: "transparent"
3827
- },
3828
- verticalIndicatorActive: { backgroundColor: "transparent" },
3829
- verticalIndicatorPressed: { backgroundColor: "transparent" },
3830
4049
  tabHovered: { backgroundColor: "transparent" },
3831
4050
  tabPressed: { backgroundColor: "transparent" },
3832
4051
  tabFocused: { borderColor: theme.semantic.focus.ring.color },
@@ -3860,6 +4079,11 @@ const createStyles$5 = (theme) => StyleSheet.create({
3860
4079
  paddingVertical: theme.tokens.spacing[1],
3861
4080
  borderRadius: theme.tokens.radius.md
3862
4081
  },
4082
+ tabPillsIconOnly: {
4083
+ width: 36,
4084
+ minWidth: 36,
4085
+ paddingHorizontal: 0
4086
+ },
3863
4087
  tabPillsActive: {
3864
4088
  backgroundColor: "transparent",
3865
4089
  borderRadius: theme.tokens.radius.md
@@ -3931,7 +4155,7 @@ TabsIcon.displayName = "Tabs.Icon";
3931
4155
  const TabsTrigger = ({ value, children, icon, badge, description, disabled, style, textStyle }) => {
3932
4156
  const { theme } = useTheme();
3933
4157
  const styles = useThemeStyles(createStyles$5);
3934
- const { value: selectedValue, variant, color, size, orientation, disabled: rootDisabled, setValue, registerTrigger } = useTabs();
4158
+ const { value: selectedValue, variant, color, size, orientation, disabled: rootDisabled, setValue, registerTrigger, registerTriggerLayout } = useTabs();
3935
4159
  const isDisabled = rootDisabled || disabled;
3936
4160
  const isActive = selectedValue === value;
3937
4161
  const isPills = variant === "pills";
@@ -3940,6 +4164,7 @@ const TabsTrigger = ({ value, children, icon, badge, description, disabled, styl
3940
4164
  const isVertical = orientation === "vertical";
3941
4165
  const isSm = size === "sm";
3942
4166
  const isLg = size === "lg";
4167
+ const isOnlyIcon = icon != null && children == null && badge == null;
3943
4168
  const palette = theme.components.tabs[color];
3944
4169
  const state = isDisabled ? theme.components.tabs.disabled : isPills ? isActive ? palette.pills.active : palette.pills.default : isActive ? palette.trigger.active : palette.trigger.default;
3945
4170
  const pressedState = isDisabled ? state : isPills ? isActive ? palette.pills.active : palette.pills.hover : isActive ? palette.trigger.active : palette.trigger.hover;
@@ -3947,12 +4172,17 @@ const TabsTrigger = ({ value, children, icon, badge, description, disabled, styl
3947
4172
  registerTrigger(value, Boolean(isDisabled), true);
3948
4173
  return () => {
3949
4174
  registerTrigger(value, Boolean(isDisabled), false);
4175
+ registerTriggerLayout(value, void 0);
3950
4176
  };
3951
4177
  }, [
3952
4178
  isDisabled,
3953
4179
  registerTrigger,
4180
+ registerTriggerLayout,
3954
4181
  value
3955
4182
  ]);
4183
+ const handleLayout = (event) => {
4184
+ registerTriggerLayout(value, event.nativeEvent.layout);
4185
+ };
3956
4186
  const iconColor = isPills && isActive ? palette.pills.active.fg : isActive ? palette.trigger.active.fg : state.fg;
3957
4187
  const renderedIcon = isValidElement(icon) ? cloneElement(icon, { color: iconColor }) : icon;
3958
4188
  return /* @__PURE__ */ jsx(Pressable, {
@@ -3965,43 +4195,39 @@ const TabsTrigger = ({ value, children, icon, badge, description, disabled, styl
3965
4195
  onPress: () => {
3966
4196
  if (!isDisabled && !isActive) setValue(value);
3967
4197
  },
4198
+ onLayout: handleLayout,
3968
4199
  style: ({ pressed }) => [
3969
4200
  styles.tab,
3970
4201
  isSm && styles.tabSm,
3971
4202
  isLg && styles.tabLg,
4203
+ isOnlyIcon && styles.tabIconOnly,
4204
+ isOnlyIcon && isSm && styles.tabIconOnlySm,
4205
+ isOnlyIcon && isLg && styles.tabIconOnlyLg,
3972
4206
  isVertical && styles.tabVertical,
3973
4207
  isPills && styles.tabPills,
4208
+ isPills && isOnlyIcon && styles.tabPillsIconOnly,
3974
4209
  isSegmented && styles.tabSegmented,
3975
4210
  isSegmented && isSm && styles.tabSegmentedSm,
3976
4211
  isSegmented && isLg && styles.tabSegmentedLg,
3977
- isPills && isActive && {
3978
- borderColor: palette.pills.active.border,
3979
- backgroundColor: palette.pills.active.bg
3980
- },
4212
+ isSegmented && isOnlyIcon && styles.tabSegmentedIconOnly,
4213
+ isSegmented && isOnlyIcon && isSm && styles.tabSegmentedIconOnlySm,
4214
+ isSegmented && isOnlyIcon && isLg && styles.tabSegmentedIconOnlyLg,
3981
4215
  {
3982
- borderColor: state.border,
4216
+ borderColor: isLine ? "transparent" : state.border,
3983
4217
  backgroundColor: pressed ? pressedState.bg : state.bg
3984
4218
  },
3985
4219
  isSegmented && isActive && {
3986
4220
  borderColor: palette.segmented.active.border,
3987
- backgroundColor: palette.segmented.active.bg
4221
+ backgroundColor: "transparent"
4222
+ },
4223
+ isPills && isActive && {
4224
+ borderColor: "transparent",
4225
+ backgroundColor: "transparent"
3988
4226
  },
3989
4227
  isDisabled && styles.tabDisabled,
3990
4228
  style
3991
4229
  ],
3992
- children: ({ pressed }) => /* @__PURE__ */ jsxs(Fragment, { children: [
3993
- isLine && !isVertical && /* @__PURE__ */ jsx(View, {
3994
- pointerEvents: "none",
3995
- style: [styles.horizontalIndicator, isActive && { backgroundColor: palette.indicator.bg }]
3996
- }),
3997
- isLine && isVertical && /* @__PURE__ */ jsx(View, {
3998
- pointerEvents: "none",
3999
- style: [
4000
- styles.verticalIndicator,
4001
- isActive && { backgroundColor: palette.indicator.bg },
4002
- pressed && !isActive && !isDisabled && { backgroundColor: palette.indicator.hoverBg }
4003
- ]
4004
- }),
4230
+ children: () => /* @__PURE__ */ jsxs(Fragment, { children: [
4005
4231
  icon != null && /* @__PURE__ */ jsx(View, {
4006
4232
  style: styles.tabIcon,
4007
4233
  children: renderedIcon
@@ -4066,7 +4292,9 @@ const TabsRoot = ({ children, value: controlledValue, defaultValue, onValueChang
4066
4292
  const styles = useThemeStyles(createStyles$4);
4067
4293
  const isControlled = controlledValue !== void 0;
4068
4294
  const [version, setVersion] = useState(0);
4295
+ const [indicatorVersion, setIndicatorVersion] = useState(0);
4069
4296
  const triggersRef = useRef([]);
4297
+ const triggerLayoutsRef = useRef(/* @__PURE__ */ new Map());
4070
4298
  const { value, setValue } = useTabs$1({
4071
4299
  value: controlledValue,
4072
4300
  defaultValue,
@@ -4089,6 +4317,14 @@ const TabsRoot = ({ children, value: controlledValue, defaultValue, onValueChang
4089
4317
  else triggersRef.current.push(nextTab);
4090
4318
  setVersion((current) => current + 1);
4091
4319
  }, []);
4320
+ const registerTriggerLayout = useCallback((triggerValue, layout) => {
4321
+ if (!layout) triggerLayoutsRef.current.delete(triggerValue);
4322
+ else triggerLayoutsRef.current.set(triggerValue, layout);
4323
+ setIndicatorVersion((current) => current + 1);
4324
+ }, []);
4325
+ const getTriggerLayout = useCallback((triggerValue) => {
4326
+ return triggerLayoutsRef.current.get(triggerValue);
4327
+ }, []);
4092
4328
  useEffect(() => {
4093
4329
  const enabledTabs = triggersRef.current.filter((tab) => !tab.disabled);
4094
4330
  const selectedExists = triggersRef.current.some((tab) => tab.value === value);
@@ -4118,7 +4354,10 @@ const TabsRoot = ({ children, value: controlledValue, defaultValue, onValueChang
4118
4354
  keepMounted,
4119
4355
  lazyMount,
4120
4356
  disabled,
4121
- registerTrigger
4357
+ registerTrigger,
4358
+ indicatorVersion,
4359
+ getTriggerLayout,
4360
+ registerTriggerLayout
4122
4361
  }), [
4123
4362
  activationMode,
4124
4363
  color,
@@ -4126,6 +4365,9 @@ const TabsRoot = ({ children, value: controlledValue, defaultValue, onValueChang
4126
4365
  keepMounted,
4127
4366
  lazyMount,
4128
4367
  orientation,
4368
+ getTriggerLayout,
4369
+ indicatorVersion,
4370
+ registerTriggerLayout,
4129
4371
  registerTrigger,
4130
4372
  setValue,
4131
4373
  size,
@@ -4155,6 +4397,50 @@ const Tabs = Object.assign(TabsRoot, {
4155
4397
  });
4156
4398
  Tabs.displayName = "Tabs";
4157
4399
  //#endregion
4400
+ //#region src/components/Tooltip/internal/TooltipContext.tsx
4401
+ const TooltipContext = createContext(null);
4402
+ const TooltipProvider = TooltipContext.Provider;
4403
+ const useTooltipContext = () => {
4404
+ const context = useContext(TooltipContext);
4405
+ if (!context) throw new Error("Tooltip components must be used inside <Tooltip>.");
4406
+ return context;
4407
+ };
4408
+ TooltipContext.displayName = "TooltipContext";
4409
+ //#endregion
4410
+ //#region src/components/Tooltip/Arrow/TooltipArrow.tsx
4411
+ const TooltipArrow = ({ style }) => {
4412
+ const { theme } = useTheme();
4413
+ const tooltip = useTooltipContext();
4414
+ const size = theme.components.tooltip.arrow.size;
4415
+ const side = tooltip.placement.split("-")[0];
4416
+ const staticSide = {
4417
+ top: "bottom",
4418
+ right: "left",
4419
+ bottom: "top",
4420
+ left: "right"
4421
+ }[side];
4422
+ const crossAxisStyle = side === "left" || side === "right" ? {
4423
+ top: "50%",
4424
+ marginTop: -size / 2
4425
+ } : {
4426
+ left: "50%",
4427
+ marginLeft: -size / 2
4428
+ };
4429
+ return /* @__PURE__ */ jsx(View, {
4430
+ pointerEvents: "none",
4431
+ style: [{
4432
+ position: "absolute",
4433
+ width: size,
4434
+ height: size,
4435
+ backgroundColor: theme.components.tooltip.arrow.bg,
4436
+ transform: [{ rotate: "45deg" }],
4437
+ [staticSide]: -size / 2,
4438
+ ...crossAxisStyle
4439
+ }, style]
4440
+ });
4441
+ };
4442
+ TooltipArrow.displayName = "Tooltip.Arrow";
4443
+ //#endregion
4158
4444
  //#region src/components/Tooltip/Tooltip.styles.ts
4159
4445
  const createStyles$3 = (theme) => StyleSheet.create({
4160
4446
  root: { alignSelf: "flex-start" },
@@ -4162,13 +4448,13 @@ const createStyles$3 = (theme) => StyleSheet.create({
4162
4448
  bubble: {
4163
4449
  position: "absolute",
4164
4450
  zIndex: 1e3,
4165
- maxWidth: 280,
4166
- paddingHorizontal: theme.tokens.spacing[3],
4167
- paddingVertical: theme.tokens.spacing[2],
4451
+ maxWidth: theme.components.tooltip.content.maxWidth,
4452
+ paddingHorizontal: theme.components.tooltip.content.paddingX,
4453
+ paddingVertical: theme.components.tooltip.content.paddingY,
4168
4454
  backgroundColor: theme.components.tooltip.content.bg,
4169
4455
  borderColor: theme.components.tooltip.content.border,
4170
- borderRadius: theme.tokens.radius.sm,
4171
- borderWidth: 1,
4456
+ borderRadius: theme.components.tooltip.content.radius,
4457
+ borderWidth: theme.components.tooltip.content.borderWidth,
4172
4458
  shadowColor: theme.tokens.shadows.md.color,
4173
4459
  shadowOffset: {
4174
4460
  width: theme.tokens.shadows.md.x,
@@ -4182,83 +4468,192 @@ const createStyles$3 = (theme) => StyleSheet.create({
4182
4468
  flexShrink: 1,
4183
4469
  color: theme.components.tooltip.content.fg,
4184
4470
  fontFamily: theme.tokens.typography.family.regular,
4185
- fontSize: theme.tokens.typography.size.sm,
4186
- lineHeight: theme.tokens.typography.lineHeight.sm,
4471
+ fontSize: theme.components.tooltip.content.fontSize,
4472
+ lineHeight: theme.components.tooltip.content.lineHeight,
4187
4473
  textAlign: "center"
4188
4474
  }
4189
4475
  });
4190
4476
  //#endregion
4191
- //#region src/components/Tooltip/Tooltip.tsx
4192
- function Tooltip({ children, content, placement = "top", disabled = false, maxWidth = 240, delay, style, contentStyle, textStyle }) {
4477
+ //#region src/components/Tooltip/Content/TooltipContent.tsx
4478
+ const TooltipContent = ({ children, forceMount = false, style, textStyle }) => {
4193
4479
  const styles = useThemeStyles(createStyles$3);
4194
- const overlayId = useId();
4195
- const [visible, setVisible] = useState(false);
4480
+ const tooltip = useTooltipContext();
4481
+ const visible = tooltip.open && !tooltip.disabled;
4482
+ if (!forceMount && !visible) return null;
4483
+ const bubble = /* @__PURE__ */ jsx(View, {
4484
+ nativeID: tooltip.contentId,
4485
+ pointerEvents: "none",
4486
+ style: [
4487
+ styles.bubble,
4488
+ {
4489
+ top: tooltip.position.top,
4490
+ left: tooltip.position.left
4491
+ },
4492
+ !visible && { display: "none" },
4493
+ style
4494
+ ],
4495
+ onLayout: tooltip.onFloatingLayout,
4496
+ children: Children.map(children, (child) => typeof child === "string" || typeof child === "number" ? /* @__PURE__ */ jsx(Text, {
4497
+ style: [styles.text, textStyle],
4498
+ children: child
4499
+ }) : child)
4500
+ });
4501
+ if (!visible) return bubble;
4502
+ return /* @__PURE__ */ jsx(Modal$1, {
4503
+ visible,
4504
+ transparent: true,
4505
+ animationType: "fade",
4506
+ onRequestClose: tooltip.requestClose,
4507
+ children: /* @__PURE__ */ jsx(Pressable, {
4508
+ style: styles.overlay,
4509
+ onPress: tooltip.requestOutsideClose,
4510
+ children: bubble
4511
+ })
4512
+ });
4513
+ };
4514
+ TooltipContent.displayName = "Tooltip.Content";
4515
+ //#endregion
4516
+ //#region src/components/Tooltip/internal/useTooltipDelay.ts
4517
+ const resolveTooltipDelay = (delay) => {
4518
+ if (typeof delay === "number") return {
4519
+ open: delay,
4520
+ close: 2500
4521
+ };
4522
+ return {
4523
+ open: delay?.open ?? 0,
4524
+ close: delay?.close ?? 2500
4525
+ };
4526
+ };
4527
+ //#endregion
4528
+ //#region src/components/Tooltip/Root/TooltipRoot.tsx
4529
+ const TooltipRoot = ({ children, open: openProp, defaultOpen = false, onOpenChange, placement = "top", disabled = false, delay, offset = 8, closeOnOutsidePress = true, style }) => {
4530
+ const contentId = `${useId()}-content`;
4196
4531
  const triggerRef = useRef(null);
4197
4532
  const closeTimerRef = useRef(null);
4198
- const { position, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, 8);
4199
- const hideDelay = delay?.close ?? 2500;
4200
- const clearCloseTimer = () => {
4533
+ const resolvedDelay = useMemo(() => resolveTooltipDelay(delay), [delay]);
4534
+ const [open, setOpenState] = useControllableState({
4535
+ value: openProp,
4536
+ defaultValue: defaultOpen,
4537
+ onChange: onOpenChange
4538
+ });
4539
+ const { position, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, offset);
4540
+ const clearCloseTimer = useCallback(() => {
4201
4541
  if (closeTimerRef.current) {
4202
4542
  clearTimeout(closeTimerRef.current);
4203
4543
  closeTimerRef.current = null;
4204
4544
  }
4205
- };
4206
- const dismiss = useNativeDismiss({
4207
- id: overlayId,
4208
- visible: visible && !disabled,
4209
- onClose: () => {
4210
- clearCloseTimer();
4211
- setVisible(false);
4212
- }
4213
- });
4214
- const showTooltip = () => {
4545
+ }, []);
4546
+ const setOpen = useCallback((nextOpen) => {
4547
+ if (disabled && nextOpen) return;
4548
+ setOpenState(nextOpen);
4549
+ }, [disabled, setOpenState]);
4550
+ const hide = useCallback(() => {
4551
+ clearCloseTimer();
4552
+ setOpen(false);
4553
+ }, [clearCloseTimer, setOpen]);
4554
+ const show = useCallback(() => {
4215
4555
  if (disabled) return;
4216
4556
  clearCloseTimer();
4217
4557
  updatePosition(triggerRef);
4218
- setVisible(true);
4219
- closeTimerRef.current = setTimeout(() => {
4220
- setVisible(false);
4221
- closeTimerRef.current = null;
4222
- }, hideDelay);
4223
- };
4558
+ const openTooltip = () => {
4559
+ setOpen(true);
4560
+ closeTimerRef.current = setTimeout(() => {
4561
+ setOpen(false);
4562
+ closeTimerRef.current = null;
4563
+ }, resolvedDelay.close);
4564
+ };
4565
+ if (resolvedDelay.open > 0) {
4566
+ closeTimerRef.current = setTimeout(openTooltip, resolvedDelay.open);
4567
+ return;
4568
+ }
4569
+ openTooltip();
4570
+ }, [
4571
+ clearCloseTimer,
4572
+ disabled,
4573
+ resolvedDelay,
4574
+ setOpen,
4575
+ updatePosition
4576
+ ]);
4577
+ const dismiss = useOverlayDismiss({
4578
+ id: contentId,
4579
+ active: open && !disabled,
4580
+ closeOnOutsidePress,
4581
+ requestClose: hide
4582
+ });
4583
+ useEffect(() => {
4584
+ if (disabled && open) hide();
4585
+ }, [
4586
+ disabled,
4587
+ hide,
4588
+ open
4589
+ ]);
4224
4590
  useEffect(() => {
4225
4591
  return clearCloseTimer;
4226
- }, []);
4227
- return /* @__PURE__ */ jsxs(View, {
4228
- style: [styles.root, style],
4229
- children: [/* @__PURE__ */ jsx(Pressable, {
4230
- ref: triggerRef,
4231
- onLongPress: showTooltip,
4592
+ }, [clearCloseTimer]);
4593
+ return /* @__PURE__ */ jsx(TooltipProvider, {
4594
+ value: useMemo(() => ({
4595
+ contentId,
4596
+ open,
4597
+ disabled,
4598
+ placement,
4599
+ position,
4600
+ triggerRef,
4601
+ setOpen,
4602
+ show,
4603
+ hide,
4604
+ requestClose: dismiss.requestClose,
4605
+ requestOutsideClose: dismiss.requestOutsideClose,
4606
+ onFloatingLayout
4607
+ }), [
4608
+ contentId,
4609
+ disabled,
4610
+ dismiss.requestClose,
4611
+ dismiss.requestOutsideClose,
4612
+ hide,
4613
+ open,
4614
+ placement,
4615
+ position,
4616
+ setOpen,
4617
+ show,
4618
+ onFloatingLayout
4619
+ ]),
4620
+ children: /* @__PURE__ */ jsx(View, {
4621
+ style,
4232
4622
  children
4233
- }), /* @__PURE__ */ jsx(Modal$1, {
4234
- visible: visible && !disabled,
4235
- transparent: true,
4236
- animationType: "fade",
4237
- onRequestClose: dismiss.requestClose,
4238
- children: /* @__PURE__ */ jsx(Pressable, {
4239
- style: styles.overlay,
4240
- onPress: dismiss.requestOutsideClose,
4241
- children: /* @__PURE__ */ jsx(View, {
4242
- pointerEvents: "none",
4243
- style: [
4244
- styles.bubble,
4245
- {
4246
- maxWidth,
4247
- top: position.top,
4248
- left: position.left
4249
- },
4250
- contentStyle
4251
- ],
4252
- onLayout: onFloatingLayout,
4253
- children: typeof content === "string" ? /* @__PURE__ */ jsx(Text, {
4254
- style: [styles.text, textStyle],
4255
- children: content
4256
- }) : content
4257
- })
4258
- })
4259
- })]
4623
+ })
4260
4624
  });
4261
- }
4625
+ };
4626
+ TooltipRoot.displayName = "Tooltip.Root";
4627
+ //#endregion
4628
+ //#region src/components/Tooltip/Trigger/TooltipTrigger.tsx
4629
+ const TooltipTrigger = ({ children, disabled, onLongPress, style, ...props }) => {
4630
+ const tooltip = useTooltipContext();
4631
+ const isDisabled = tooltip.disabled || disabled;
4632
+ return /* @__PURE__ */ jsx(Pressable, {
4633
+ ...props,
4634
+ ref: tooltip.triggerRef,
4635
+ accessibilityState: {
4636
+ ...props.accessibilityState,
4637
+ disabled: isDisabled || props.accessibilityState?.disabled
4638
+ },
4639
+ disabled: isDisabled,
4640
+ onLongPress: (event) => {
4641
+ onLongPress?.(event);
4642
+ if (event.defaultPrevented) return;
4643
+ tooltip.show();
4644
+ },
4645
+ style,
4646
+ children
4647
+ });
4648
+ };
4649
+ TooltipTrigger.displayName = "Tooltip.Trigger";
4650
+ //#endregion
4651
+ //#region src/components/Tooltip/Tooltip.tsx
4652
+ const Tooltip = Object.assign(TooltipRoot, {
4653
+ Trigger: TooltipTrigger,
4654
+ Content: TooltipContent,
4655
+ Arrow: TooltipArrow
4656
+ });
4262
4657
  Tooltip.displayName = "Tooltip";
4263
4658
  //#endregion
4264
4659
  //#region src/utils/devWarning.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellira-ui/react-native",
3
- "version": "2.40.0",
3
+ "version": "2.42.0",
4
4
  "description": "React Native components for Vellira Design System",
5
5
  "author": "Roman Bakurov",
6
6
  "license": "MIT",