@idealyst/components 1.0.82 → 1.0.84

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 (316) hide show
  1. package/CLAUDE.md +199 -232
  2. package/README.md +5 -5
  3. package/package.json +25 -7
  4. package/plugin/README.md +272 -0
  5. package/plugin/test-cases.jsx +112 -0
  6. package/plugin/web-legacy.js +320 -0
  7. package/plugin/web.js +422 -124
  8. package/src/Accordion/Accordion.native.tsx +182 -0
  9. package/src/Accordion/Accordion.styles.tsx +260 -0
  10. package/src/Accordion/Accordion.web.tsx +147 -0
  11. package/src/Accordion/index.native.tsx +3 -0
  12. package/src/Accordion/index.ts +3 -0
  13. package/src/Accordion/index.web.tsx +3 -0
  14. package/src/Accordion/types.ts +23 -0
  15. package/src/ActivityIndicator/ActivityIndicator.native.tsx +17 -12
  16. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +83 -109
  17. package/src/ActivityIndicator/ActivityIndicator.web.tsx +23 -17
  18. package/src/ActivityIndicator/index.ts +5 -2
  19. package/src/ActivityIndicator/index.web.ts +5 -2
  20. package/src/ActivityIndicator/types.ts +15 -10
  21. package/src/Alert/Alert.native.tsx +113 -0
  22. package/src/Alert/Alert.styles.tsx +304 -0
  23. package/src/Alert/Alert.web.tsx +123 -0
  24. package/src/Alert/index.native.ts +5 -0
  25. package/src/Alert/index.ts +5 -0
  26. package/src/Alert/index.web.ts +5 -0
  27. package/src/Alert/types.ts +21 -0
  28. package/src/Avatar/Avatar.native.tsx +8 -6
  29. package/src/Avatar/Avatar.styles.tsx +64 -58
  30. package/src/Avatar/Avatar.web.tsx +13 -8
  31. package/src/Avatar/index.ts +5 -2
  32. package/src/Avatar/index.web.ts +5 -2
  33. package/src/Avatar/types.ts +19 -13
  34. package/src/Badge/Badge.native.tsx +59 -14
  35. package/src/Badge/Badge.styles.tsx +125 -139
  36. package/src/Badge/Badge.web.tsx +72 -16
  37. package/src/Badge/index.ts +5 -2
  38. package/src/Badge/index.web.ts +5 -2
  39. package/src/Badge/types.ts +23 -11
  40. package/src/Breadcrumb/Breadcrumb.native.tsx +225 -0
  41. package/src/Breadcrumb/Breadcrumb.styles.tsx +234 -0
  42. package/src/Breadcrumb/Breadcrumb.web.tsx +268 -0
  43. package/src/Breadcrumb/index.native.ts +5 -0
  44. package/src/Breadcrumb/index.ts +5 -0
  45. package/src/Breadcrumb/index.web.ts +5 -0
  46. package/src/Breadcrumb/types.ts +56 -0
  47. package/src/Button/Button.native.tsx +75 -24
  48. package/src/Button/Button.styles.tsx +248 -205
  49. package/src/Button/Button.web.tsx +82 -25
  50. package/src/Button/index.ts +5 -5
  51. package/src/Button/index.web.ts +5 -3
  52. package/src/Button/types.ts +32 -15
  53. package/src/Card/Card.native.tsx +14 -11
  54. package/src/Card/Card.styles.tsx +146 -220
  55. package/src/Card/Card.web.tsx +20 -21
  56. package/src/Card/index.ts +5 -5
  57. package/src/Card/index.web.ts +5 -3
  58. package/src/Card/types.ts +24 -17
  59. package/src/Checkbox/Checkbox.native.tsx +24 -34
  60. package/src/Checkbox/Checkbox.styles.tsx +223 -275
  61. package/src/Checkbox/Checkbox.web.tsx +30 -37
  62. package/src/Checkbox/index.ts +5 -5
  63. package/src/Checkbox/index.web.ts +5 -3
  64. package/src/Checkbox/types.ts +26 -20
  65. package/src/Chip/Chip.native.tsx +126 -0
  66. package/src/Chip/Chip.styles.tsx +138 -0
  67. package/src/Chip/Chip.web.tsx +154 -0
  68. package/src/Chip/index.native.ts +5 -0
  69. package/src/Chip/index.ts +5 -0
  70. package/src/Chip/index.web.ts +5 -0
  71. package/src/Chip/types.ts +51 -0
  72. package/src/Dialog/Dialog.native.tsx +65 -12
  73. package/src/Dialog/Dialog.styles.tsx +154 -136
  74. package/src/Dialog/Dialog.web.tsx +16 -11
  75. package/src/Dialog/index.ts +5 -2
  76. package/src/Dialog/index.web.ts +5 -2
  77. package/src/Dialog/types.ts +22 -16
  78. package/src/Divider/Divider.native.tsx +19 -14
  79. package/src/Divider/Divider.styles.tsx +273 -595
  80. package/src/Divider/Divider.web.tsx +19 -12
  81. package/src/Divider/index.ts +5 -5
  82. package/src/Divider/index.web.ts +5 -3
  83. package/src/Divider/types.ts +28 -19
  84. package/src/Icon/Icon.native.tsx +17 -24
  85. package/src/Icon/Icon.styles.tsx +64 -48
  86. package/src/Icon/Icon.web.tsx +14 -11
  87. package/src/Icon/IconSvg/IconSvg.native.tsx +42 -0
  88. package/src/Icon/IconSvg/IconSvg.web.tsx +40 -0
  89. package/src/Icon/IconSvg/index.native.ts +1 -0
  90. package/src/Icon/IconSvg/index.ts +1 -0
  91. package/src/Icon/icon-resolver.native.ts +27 -0
  92. package/src/Icon/icon-resolver.ts +70 -0
  93. package/src/Icon/index.ts +5 -5
  94. package/src/Icon/index.web.ts +5 -3
  95. package/src/Icon/types.ts +17 -11
  96. package/src/Image/Image.native.tsx +86 -0
  97. package/src/Image/Image.styles.tsx +57 -0
  98. package/src/Image/Image.web.tsx +92 -0
  99. package/src/Image/index.native.ts +5 -0
  100. package/src/Image/index.ts +5 -0
  101. package/src/Image/types.ts +21 -0
  102. package/src/Input/Input.native.tsx +103 -26
  103. package/src/Input/Input.styles.tsx +240 -177
  104. package/src/Input/Input.web.tsx +141 -38
  105. package/src/Input/index.ts +5 -5
  106. package/src/Input/index.web.ts +5 -3
  107. package/src/Input/types.ts +43 -20
  108. package/src/List/List.native.tsx +56 -0
  109. package/src/List/List.styles.tsx +257 -0
  110. package/src/List/List.web.tsx +43 -0
  111. package/src/List/ListContext.tsx +16 -0
  112. package/src/List/ListItem.native.tsx +111 -0
  113. package/src/List/ListItem.web.tsx +110 -0
  114. package/src/List/ListSection.native.tsx +31 -0
  115. package/src/List/ListSection.web.tsx +33 -0
  116. package/src/List/index.native.tsx +5 -0
  117. package/src/List/index.ts +5 -0
  118. package/src/List/index.web.tsx +5 -0
  119. package/src/List/types.ts +42 -0
  120. package/src/Menu/Menu.native.tsx +150 -0
  121. package/src/Menu/Menu.styles.tsx +185 -0
  122. package/src/Menu/Menu.web.tsx +99 -0
  123. package/src/Menu/MenuItem.native.tsx +66 -0
  124. package/src/Menu/MenuItem.styles.tsx +119 -0
  125. package/src/Menu/MenuItem.web.tsx +67 -0
  126. package/src/Menu/index.native.ts +3 -0
  127. package/src/Menu/index.ts +3 -0
  128. package/src/Menu/index.web.ts +3 -0
  129. package/src/Menu/types.ts +30 -0
  130. package/src/Popover/Popover.native.tsx +102 -32
  131. package/src/Popover/Popover.styles.tsx +100 -67
  132. package/src/Popover/Popover.web.tsx +36 -260
  133. package/src/Popover/index.ts +5 -2
  134. package/src/Popover/index.web.ts +5 -2
  135. package/src/Popover/types.ts +14 -13
  136. package/src/Pressable/Pressable.native.tsx +7 -6
  137. package/src/Pressable/Pressable.web.tsx +8 -6
  138. package/src/Pressable/index.ts +5 -2
  139. package/src/Pressable/index.web.ts +5 -2
  140. package/src/Pressable/types.ts +11 -10
  141. package/src/Progress/Progress.native.tsx +179 -0
  142. package/src/Progress/Progress.styles.tsx +164 -0
  143. package/src/Progress/Progress.web.tsx +144 -0
  144. package/src/Progress/index.native.ts +1 -0
  145. package/src/Progress/index.ts +5 -0
  146. package/src/Progress/index.web.ts +5 -0
  147. package/src/Progress/types.ts +21 -0
  148. package/src/RadioButton/RadioButton.native.tsx +88 -0
  149. package/src/RadioButton/RadioButton.styles.tsx +163 -0
  150. package/src/RadioButton/RadioButton.web.tsx +85 -0
  151. package/src/RadioButton/RadioGroup.native.tsx +43 -0
  152. package/src/RadioButton/RadioGroup.web.tsx +49 -0
  153. package/src/RadioButton/index.native.ts +2 -0
  154. package/src/RadioButton/index.ts +2 -0
  155. package/src/RadioButton/index.web.ts +2 -0
  156. package/src/RadioButton/types.ts +29 -0
  157. package/src/SVGImage/SVGImage.native.tsx +9 -7
  158. package/src/SVGImage/SVGImage.styles.tsx +63 -55
  159. package/src/SVGImage/SVGImage.web.tsx +16 -13
  160. package/src/SVGImage/index.ts +5 -5
  161. package/src/SVGImage/index.web.ts +5 -2
  162. package/src/SVGImage/types.ts +7 -3
  163. package/src/Screen/Screen.native.tsx +43 -17
  164. package/src/Screen/Screen.styles.tsx +58 -54
  165. package/src/Screen/Screen.web.tsx +11 -5
  166. package/src/Screen/index.ts +5 -2
  167. package/src/Screen/index.web.ts +5 -2
  168. package/src/Screen/types.ts +23 -9
  169. package/src/Select/Select.native.tsx +347 -0
  170. package/src/Select/Select.styles.tsx +335 -0
  171. package/src/Select/Select.web.tsx +276 -0
  172. package/src/Select/index.native.ts +2 -0
  173. package/src/Select/index.ts +5 -0
  174. package/src/Select/index.web.ts +5 -0
  175. package/src/Select/types.ts +124 -0
  176. package/src/Skeleton/Skeleton.native.tsx +139 -0
  177. package/src/Skeleton/Skeleton.styles.tsx +59 -0
  178. package/src/Skeleton/Skeleton.web.tsx +112 -0
  179. package/src/Skeleton/index.native.ts +4 -0
  180. package/src/Skeleton/index.ts +5 -0
  181. package/src/Skeleton/index.web.ts +5 -0
  182. package/src/Skeleton/types.ts +75 -0
  183. package/src/Slider/Slider.native.tsx +248 -0
  184. package/src/Slider/Slider.styles.tsx +241 -0
  185. package/src/Slider/Slider.web.tsx +226 -0
  186. package/src/Slider/index.native.ts +3 -0
  187. package/src/Slider/index.ts +5 -0
  188. package/src/Slider/index.web.ts +5 -0
  189. package/src/Slider/types.ts +31 -0
  190. package/src/Switch/Switch.native.tsx +131 -0
  191. package/src/Switch/Switch.styles.tsx +169 -0
  192. package/src/Switch/Switch.web.tsx +121 -0
  193. package/src/Switch/index.native.ts +3 -0
  194. package/src/Switch/index.ts +5 -0
  195. package/src/Switch/index.web.ts +5 -0
  196. package/src/Switch/types.ts +21 -0
  197. package/src/TabBar/TabBar.native.tsx +142 -0
  198. package/src/TabBar/TabBar.styles.tsx +399 -0
  199. package/src/TabBar/TabBar.web.tsx +205 -0
  200. package/src/TabBar/index.native.tsx +3 -0
  201. package/src/TabBar/index.ts +3 -0
  202. package/src/TabBar/index.web.tsx +3 -0
  203. package/src/TabBar/types.ts +26 -0
  204. package/src/Table/Table.native.tsx +122 -0
  205. package/src/Table/Table.styles.tsx +283 -0
  206. package/src/Table/Table.web.tsx +112 -0
  207. package/src/Table/index.native.tsx +3 -0
  208. package/src/Table/index.ts +3 -0
  209. package/src/Table/index.web.tsx +3 -0
  210. package/src/Table/types.ts +28 -0
  211. package/src/Text/Text.native.tsx +12 -11
  212. package/src/Text/Text.styles.tsx +76 -64
  213. package/src/Text/Text.web.tsx +14 -9
  214. package/src/Text/index.ts +5 -5
  215. package/src/Text/index.web.ts +5 -3
  216. package/src/Text/types.ts +20 -13
  217. package/src/TextArea/TextArea.native.tsx +134 -0
  218. package/src/TextArea/TextArea.styles.tsx +175 -0
  219. package/src/TextArea/TextArea.web.tsx +156 -0
  220. package/src/TextArea/index.native.ts +3 -0
  221. package/src/TextArea/index.ts +3 -0
  222. package/src/TextArea/index.web.ts +3 -0
  223. package/src/TextArea/types.ts +30 -0
  224. package/src/Tooltip/Tooltip.native.tsx +165 -0
  225. package/src/Tooltip/Tooltip.styles.tsx +73 -0
  226. package/src/Tooltip/Tooltip.web.tsx +87 -0
  227. package/src/Tooltip/index.native.ts +3 -0
  228. package/src/Tooltip/index.ts +3 -0
  229. package/src/Tooltip/types.ts +18 -0
  230. package/src/Video/Video.native.tsx +105 -0
  231. package/src/Video/Video.styles.tsx +39 -0
  232. package/src/Video/Video.web.tsx +115 -0
  233. package/src/Video/index.native.ts +5 -0
  234. package/src/Video/index.ts +5 -0
  235. package/src/Video/types.ts +29 -0
  236. package/src/View/View.native.tsx +9 -14
  237. package/src/View/View.styles.tsx +101 -93
  238. package/src/View/View.web.tsx +16 -17
  239. package/src/View/index.ts +5 -5
  240. package/src/View/index.web.ts +5 -3
  241. package/src/View/types.ts +29 -21
  242. package/src/examples/AccordionExamples.tsx +126 -0
  243. package/src/examples/AlertExamples.tsx +280 -0
  244. package/src/examples/AvatarExamples.tsx +23 -23
  245. package/src/examples/BadgeExamples.tsx +109 -41
  246. package/src/examples/BreadcrumbExamples.tsx +312 -0
  247. package/src/examples/ButtonExamples.tsx +160 -33
  248. package/src/examples/CardExamples.tsx +40 -40
  249. package/src/examples/CheckboxExamples.tsx +12 -12
  250. package/src/examples/ChipExamples.tsx +197 -0
  251. package/src/examples/DialogExamples.tsx +22 -22
  252. package/src/examples/DividerExamples.tsx +49 -49
  253. package/src/examples/IconExamples.tsx +270 -54
  254. package/src/examples/ImageExamples.tsx +174 -0
  255. package/src/examples/InputExamples.tsx +75 -17
  256. package/src/examples/ListExamples.tsx +288 -0
  257. package/src/examples/MenuExamples.tsx +144 -0
  258. package/src/examples/PopoverExamples.tsx +69 -73
  259. package/src/examples/ProgressExamples.tsx +137 -0
  260. package/src/examples/RadioButtonExamples.tsx +161 -0
  261. package/src/examples/SVGImageExamples.tsx +19 -17
  262. package/src/examples/ScreenExamples.tsx +31 -31
  263. package/src/examples/SelectExamples.tsx +423 -0
  264. package/src/examples/SkeletonExamples.tsx +206 -0
  265. package/src/examples/SliderExamples.tsx +200 -0
  266. package/src/examples/SwitchExamples.tsx +182 -0
  267. package/src/examples/TabBarExamples.tsx +143 -0
  268. package/src/examples/TableExamples.tsx +280 -0
  269. package/src/examples/TextAreaExamples.tsx +173 -0
  270. package/src/examples/TextExamples.tsx +28 -32
  271. package/src/examples/ThemeExtensionExamples.tsx +10 -10
  272. package/src/examples/TooltipExamples.tsx +126 -0
  273. package/src/examples/VideoExamples.tsx +144 -0
  274. package/src/examples/ViewExamples.tsx +64 -56
  275. package/src/examples/index.ts +18 -3
  276. package/src/hooks/useMergeRefs.ts +16 -0
  277. package/src/hooks/useSmartPosition.native.ts +169 -0
  278. package/src/index.native.ts +80 -9
  279. package/src/index.ts +75 -1
  280. package/src/internal/BoundedModalContent.native.tsx +58 -0
  281. package/src/internal/PositionedPortal.tsx +254 -0
  282. package/src/internal/SafeAreaDebugOverlay.native.tsx +173 -0
  283. package/src/unistyles.d.ts +6 -0
  284. package/src/utils/buildSizeVariants.ts +16 -0
  285. package/src/utils/deepMerge.ts +43 -0
  286. package/src/utils/positionUtils.native.ts +280 -0
  287. package/src/utils/styleHelpers.ts +48 -0
  288. package/LLM-ACCESS-GUIDE.md +0 -143
  289. package/src/ActivityIndicator/README.md +0 -132
  290. package/src/Avatar/README.md +0 -139
  291. package/src/Badge/README.md +0 -170
  292. package/src/Button/Button.types.ts +0 -12
  293. package/src/Button/README.md +0 -262
  294. package/src/Card/README.md +0 -258
  295. package/src/Checkbox/README.md +0 -102
  296. package/src/Dialog/README.md +0 -210
  297. package/src/Divider/README.md +0 -108
  298. package/src/Icon/README.md +0 -81
  299. package/src/Input/README.md +0 -100
  300. package/src/SVGImage/README.md +0 -209
  301. package/src/Screen/README.md +0 -86
  302. package/src/Text/README.md +0 -94
  303. package/src/View/README.md +0 -107
  304. package/src/examples/AllExamples.tsx +0 -84
  305. package/src/examples/README.md +0 -136
  306. package/src/examples/ValidationExamples.tsx +0 -95
  307. package/src/examples/extendedTheme.ts +0 -329
  308. package/src/theme/breakpoints.ts +0 -8
  309. package/src/theme/colorResolver.ts +0 -218
  310. package/src/theme/colors.ts +0 -315
  311. package/src/theme/defaultThemes.ts +0 -326
  312. package/src/theme/index.ts +0 -188
  313. package/src/theme/themeBuilder.ts +0 -602
  314. package/src/theme/unistyles.d.ts +0 -6
  315. package/src/theme/variantHelpers.ts +0 -584
  316. package/src/theme/variants.ts +0 -56
@@ -0,0 +1,248 @@
1
+ import React, { useState, useCallback, forwardRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { GestureDetector, Gesture } from 'react-native-gesture-handler';
4
+ import Animated, { useSharedValue, useAnimatedStyle, runOnJS, withSpring } from 'react-native-reanimated';
5
+ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
6
+ import { sliderStyles } from './Slider.styles';
7
+ import Text from '../Text';
8
+ import type { SliderProps } from './types';
9
+ import { isIconName } from '../Icon/icon-resolver';
10
+
11
+ const Slider = forwardRef<View, SliderProps>(({
12
+ value: controlledValue,
13
+ defaultValue = 0,
14
+ min = 0,
15
+ max = 100,
16
+ step = 1,
17
+ disabled = false,
18
+ showValue = false,
19
+ showMinMax = false,
20
+ marks = [],
21
+ intent = 'primary',
22
+ size = 'md',
23
+ icon,
24
+ onValueChange,
25
+ onValueCommit,
26
+ style,
27
+ testID,
28
+ }, ref) => {
29
+ const [internalValue, setInternalValue] = useState(defaultValue);
30
+ const [trackWidthState, setTrackWidthState] = useState(0);
31
+ const trackWidth = useSharedValue(0);
32
+ const translateX = useSharedValue(0);
33
+ const isDragging = useSharedValue(false);
34
+ const scale = useSharedValue(1);
35
+
36
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
37
+
38
+ sliderStyles.useVariants({
39
+ size,
40
+ disabled,
41
+ });
42
+
43
+ const clampValue = (val: number) => {
44
+ 'worklet';
45
+ const clampedValue = Math.min(Math.max(val, min), max);
46
+ const steppedValue = Math.round(clampedValue / step) * step;
47
+ return Math.min(Math.max(steppedValue, min), max);
48
+ };
49
+
50
+ const calculateValueFromPosition = (position: number) => {
51
+ 'worklet';
52
+ if (trackWidth.value === 0) return value;
53
+ const percentage = Math.max(0, Math.min(1, position / trackWidth.value));
54
+ const rawValue = min + percentage * (max - min);
55
+ return clampValue(rawValue);
56
+ };
57
+
58
+ const updateValue = useCallback((newValue: number) => {
59
+ if (controlledValue === undefined) {
60
+ setInternalValue(newValue);
61
+ }
62
+
63
+ onValueChange?.(newValue);
64
+ }, [controlledValue, onValueChange]);
65
+
66
+ const commitValue = useCallback((finalValue: number) => {
67
+ onValueCommit?.(finalValue);
68
+ }, [onValueCommit]);
69
+
70
+ // Update translateX when value changes externally
71
+ React.useEffect(() => {
72
+ if (!isDragging.value && trackWidth.value > 0) {
73
+ const percentage = (value - min) / (max - min);
74
+ translateX.value = percentage * trackWidth.value;
75
+ }
76
+ }, [value, min, max, trackWidth, translateX, isDragging]);
77
+
78
+ const panGesture = Gesture.Pan()
79
+ .enabled(!disabled)
80
+ .activeOffsetX([-5, 5])
81
+ .failOffsetY([-5, 5])
82
+ .onStart(() => {
83
+ isDragging.value = true;
84
+ scale.value = withSpring(1.15, { damping: 15, stiffness: 200 });
85
+ })
86
+ .onUpdate((event) => {
87
+ const newPosition = Math.max(0, Math.min(trackWidth.value, event.x));
88
+ translateX.value = newPosition;
89
+ const newValue = calculateValueFromPosition(newPosition);
90
+ runOnJS(updateValue)(newValue);
91
+ })
92
+ .onEnd(() => {
93
+ isDragging.value = false;
94
+ scale.value = withSpring(1, { damping: 15, stiffness: 200 });
95
+ const newPosition = Math.max(0, Math.min(trackWidth.value, translateX.value));
96
+ const finalValue = calculateValueFromPosition(newPosition);
97
+ runOnJS(commitValue)(finalValue);
98
+ });
99
+
100
+ const tapGesture = Gesture.Tap()
101
+ .enabled(!disabled)
102
+ .onBegin(() => {
103
+ scale.value = withSpring(1.15, { damping: 15, stiffness: 200 });
104
+ })
105
+ .onEnd((event) => {
106
+ const newPosition = Math.max(0, Math.min(trackWidth.value, event.x));
107
+ translateX.value = newPosition;
108
+ const newValue = calculateValueFromPosition(newPosition);
109
+ runOnJS(updateValue)(newValue);
110
+ runOnJS(commitValue)(newValue);
111
+ scale.value = withSpring(1, { damping: 15, stiffness: 200 });
112
+ })
113
+ .onFinalize(() => {
114
+ scale.value = withSpring(1, { damping: 15, stiffness: 200 });
115
+ });
116
+
117
+ const composedGesture = Gesture.Race(panGesture, tapGesture);
118
+
119
+ const getThumbSize = () => {
120
+ if (size === 'sm') return 16;
121
+ if (size === 'lg') return 24;
122
+ return 20;
123
+ };
124
+
125
+ const thumbSize = getThumbSize();
126
+
127
+ const thumbAnimatedStyle = useAnimatedStyle(() => {
128
+ return {
129
+ transform: [
130
+ { translateX: translateX.value - thumbSize / 2 },
131
+ { scale: scale.value },
132
+ ],
133
+ } as any;
134
+ });
135
+
136
+ const filledTrackAnimatedStyle = useAnimatedStyle(() => {
137
+ return {
138
+ width: translateX.value,
139
+ };
140
+ });
141
+
142
+ const percentage = ((value - min) / (max - min)) * 100;
143
+
144
+ const renderIcon = () => {
145
+ if (!icon) return null;
146
+
147
+ if (typeof icon === 'string' && isIconName(icon)) {
148
+ const iconStyle = sliderStyles.thumbIcon({ intent });
149
+ return (
150
+ <MaterialCommunityIcons
151
+ name={icon}
152
+ size={iconStyle.width || 16}
153
+ color={iconStyle.color}
154
+ />
155
+ );
156
+ }
157
+
158
+ return icon;
159
+ };
160
+
161
+ return (
162
+ <View ref={ref} style={[sliderStyles.container, style]} testID={testID}>
163
+ {showValue && (
164
+ <View style={sliderStyles.valueLabel as any}>
165
+ <Text>{value}</Text>
166
+ </View>
167
+ )}
168
+
169
+ <View style={sliderStyles.sliderWrapper}>
170
+ <GestureDetector gesture={composedGesture}>
171
+ <View
172
+ style={sliderStyles.track}
173
+ onLayout={(e) => {
174
+ const width = e.nativeEvent.layout.width;
175
+ trackWidth.value = width;
176
+ setTrackWidthState(width);
177
+ }}
178
+ >
179
+ {/* Filled track */}
180
+ <Animated.View
181
+ style={[sliderStyles.filledTrack({ intent }), filledTrackAnimatedStyle]}
182
+ />
183
+
184
+ {/* Marks */}
185
+ {marks.length > 0 && trackWidthState > 0 && (
186
+ <View style={sliderStyles.marks}>
187
+ {marks.map((mark) => {
188
+ const markPercentage = ((mark.value - min) / (max - min)) * 100;
189
+ const markPosition = (markPercentage / 100) * trackWidthState;
190
+ return (
191
+ <View key={mark.value}>
192
+ <View
193
+ style={[
194
+ sliderStyles.mark,
195
+ { left: markPosition },
196
+ ]}
197
+ />
198
+ {mark.label && (
199
+ <View
200
+ style={[
201
+ sliderStyles.markLabel,
202
+ { left: markPosition },
203
+ ]}
204
+ >
205
+ <Text size="sm">{mark.label}</Text>
206
+ </View>
207
+ )}
208
+ </View>
209
+ );
210
+ })}
211
+ </View>
212
+ )}
213
+
214
+ {/* Thumb */}
215
+ <Animated.View
216
+ style={[
217
+ sliderStyles.thumb({ intent, disabled }),
218
+ {
219
+ // Manual positioning/sizing for native layout
220
+ position: 'absolute',
221
+ top: '50%',
222
+ marginTop: -thumbSize / 2,
223
+ width: thumbSize,
224
+ height: thumbSize,
225
+ borderRadius: thumbSize / 2,
226
+ },
227
+ thumbAnimatedStyle,
228
+ ]}
229
+ >
230
+ {renderIcon()}
231
+ </Animated.View>
232
+ </View>
233
+ </GestureDetector>
234
+ </View>
235
+
236
+ {showMinMax && (
237
+ <View style={sliderStyles.minMaxLabels}>
238
+ <Text style={sliderStyles.minMaxLabel} size="sm">{min}</Text>
239
+ <Text style={sliderStyles.minMaxLabel} size="sm">{max}</Text>
240
+ </View>
241
+ )}
242
+ </View>
243
+ );
244
+ });
245
+
246
+ Slider.displayName = 'Slider';
247
+
248
+ export default Slider;
@@ -0,0 +1,241 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, Size, Styles} from '@idealyst/theme';
3
+ import { buildSizeVariants } from '../utils/buildSizeVariants';
4
+ import { SliderIntentVariant } from './types';
5
+
6
+ /**
7
+ * Create size variants for track
8
+ */
9
+ function createTrackSizeVariants(theme: Theme) {
10
+ const variants = {} as Record<Size, Styles>;
11
+ for (const sizeKey in theme.sizes.slider) {
12
+ const size = sizeKey as Size;
13
+ variants[size] = {
14
+ height: theme.sizes.slider[size].trackHeight,
15
+ };
16
+ }
17
+ return variants;
18
+ }
19
+
20
+ /**
21
+ * Get filled track color based on intent
22
+ */
23
+ function getFilledTrackColor(theme: Theme, intent: SliderIntentVariant) {
24
+ return theme.intents[intent].primary;
25
+ }
26
+
27
+ /**
28
+ * Create size variants for thumb
29
+ */
30
+ function createThumbSizeVariants(theme: Theme) {
31
+ return buildSizeVariants(theme, 'slider', (size) => ({
32
+ width: size.thumbSize,
33
+ height: size.thumbSize,
34
+ }));
35
+ }
36
+
37
+ /**
38
+ * Get thumb border color based on intent
39
+ */
40
+ function getThumbBorderColor(theme: Theme, intent: SliderIntentVariant) {
41
+ return theme.intents[intent].primary;
42
+ }
43
+
44
+ /**
45
+ * Create size variants for thumb icon
46
+ */
47
+ function createThumbIconSizeVariants(theme: Theme) {
48
+ return buildSizeVariants(theme, 'slider', (size) => ({
49
+ width: size.thumbIconSize,
50
+ height: size.thumbIconSize,
51
+ minWidth: size.thumbIconSize,
52
+ maxWidth: size.thumbIconSize,
53
+ minHeight: size.thumbIconSize,
54
+ maxHeight: size.thumbIconSize,
55
+ }));
56
+ }
57
+
58
+ /**
59
+ * Get thumb icon color based on intent
60
+ */
61
+ function getThumbIconColor(theme: Theme, intent: SliderIntentVariant){
62
+ return theme.intents[intent].primary;
63
+ }
64
+
65
+ /**
66
+ * Create size variants for mark
67
+ */
68
+ function createMarkSizeVariants(theme: Theme) {
69
+ return buildSizeVariants(theme, 'slider', (size) => ({
70
+ height: size.markHeight,
71
+ }));
72
+ }
73
+
74
+ const createFilledTrackStyles = (theme: Theme) => {
75
+ return ({ intent }: { intent: SliderIntentVariant }) => {
76
+ return {
77
+ position: 'absolute',
78
+ height: '100%',
79
+ borderRadius: 9999,
80
+ top: 0,
81
+ left: 0,
82
+ backgroundColor: getFilledTrackColor(theme, intent),
83
+ } as const;
84
+ }
85
+ }
86
+
87
+ const createThumbStyles = (theme: Theme) => {
88
+ return ({ intent, disabled }: { intent: SliderIntentVariant, disabled: boolean }) => {
89
+ return {
90
+ position: 'absolute',
91
+ backgroundColor: theme.colors.surface.primary,
92
+ borderRadius: 9999,
93
+ borderWidth: 2,
94
+ borderStyle: 'solid',
95
+ borderColor: getThumbBorderColor(theme, intent),
96
+ top: '50%',
97
+ display: 'flex',
98
+ alignItems: 'center',
99
+ justifyContent: 'center',
100
+ shadowColor: '#000',
101
+ shadowOffset: { width: 0, height: 2 },
102
+ shadowOpacity: 0.2,
103
+ shadowRadius: 4,
104
+ elevation: 2,
105
+ variants: {
106
+ size: createThumbSizeVariants(theme),
107
+ disabled: {
108
+ true: {
109
+ opacity: 0.6,
110
+ _web: {
111
+ cursor: 'not-allowed',
112
+ },
113
+ },
114
+ false: {
115
+ opacity: 1,
116
+ _web: {
117
+ cursor: 'grab',
118
+ _hover: {
119
+ transform: 'translate(-50%, -50%) scale(1.05)',
120
+ },
121
+ _active: {
122
+ cursor: 'grabbing',
123
+ transform: 'translate(-50%, -50%) scale(1.1)',
124
+ },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ _web: {
130
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
131
+ transform: 'translate(-50%, -50%)',
132
+ transition: 'transform 0.15s ease, box-shadow 0.2s ease',
133
+ },
134
+ } as const;
135
+ }
136
+ }
137
+
138
+ const createThumbIconStyles = (theme: Theme) => {
139
+ return ({ intent }: { intent: SliderIntentVariant }) => {
140
+ return {
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ flexShrink: 0,
145
+ color: getThumbIconColor(theme, intent),
146
+ variants: {
147
+ size: createThumbIconSizeVariants(theme),
148
+ },
149
+ } as const;
150
+ }
151
+ }
152
+
153
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel transform on native cannot resolve function calls to extract variant structures.
154
+ // @ts-ignore - TS language server needs restart to pick up theme structure changes
155
+ export const sliderStyles = StyleSheet.create((theme: Theme) => {
156
+ return {
157
+ container: {
158
+ gap: 4,
159
+ paddingVertical: 8,
160
+ },
161
+ sliderWrapper: {
162
+ position: 'relative',
163
+ paddingVertical: 4,
164
+ },
165
+ track: {
166
+ backgroundColor: theme.colors.surface.tertiary,
167
+ borderRadius: 9999,
168
+ position: 'relative',
169
+ variants: {
170
+ size: createTrackSizeVariants(theme),
171
+ disabled: {
172
+ true: {
173
+ opacity: 0.5,
174
+ _web: {
175
+ cursor: 'not-allowed',
176
+ },
177
+ },
178
+ false: {
179
+ opacity: 1,
180
+ _web: {
181
+ cursor: 'pointer',
182
+ },
183
+ },
184
+ },
185
+ } as const,
186
+ } as const,
187
+ filledTrack: createFilledTrackStyles(theme),
188
+ thumb: createThumbStyles(theme),
189
+ thumbActive: {
190
+ _web: {
191
+ transform: 'translate(-50%, -50%) scale(1.1)',
192
+ },
193
+ },
194
+ thumbIcon: createThumbIconStyles(theme),
195
+ valueLabel: {
196
+ fontSize: 12,
197
+ fontWeight: '600',
198
+ color: theme.colors.text.primary,
199
+ textAlign: 'center',
200
+ },
201
+ minMaxLabels: {
202
+ flexDirection: 'row',
203
+ justifyContent: 'space-between',
204
+ marginTop: 4,
205
+ },
206
+ minMaxLabel: {
207
+ fontSize: 12,
208
+ color: theme.colors.text.secondary,
209
+ },
210
+ mark: {
211
+ position: 'absolute',
212
+ width: 2,
213
+ backgroundColor: theme.colors.border.secondary,
214
+ top: '50%',
215
+ variants: {
216
+ size: createMarkSizeVariants(theme),
217
+ },
218
+ _web: {
219
+ transform: 'translate(-50%, -50%)',
220
+ },
221
+ },
222
+ marks: {
223
+ position: 'absolute',
224
+ width: '100%',
225
+ height: '100%',
226
+ top: 0,
227
+ left: 0,
228
+ },
229
+ markLabel: {
230
+ position: 'absolute',
231
+ fontSize: 10,
232
+ color: theme.colors.text.secondary,
233
+ top: '100%',
234
+ marginTop: 4,
235
+ _web: {
236
+ transform: 'translateX(-50%)',
237
+ whiteSpace: 'nowrap',
238
+ },
239
+ },
240
+ };
241
+ });
@@ -0,0 +1,226 @@
1
+ import React, { useState, useRef, useCallback, isValidElement, forwardRef } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { sliderStyles } from './Slider.styles';
4
+ import type { SliderProps } from './types';
5
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
+ import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
7
+ import useMergeRefs from '../hooks/useMergeRefs';
8
+
9
+ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
10
+ value: controlledValue,
11
+ defaultValue = 0,
12
+ min = 0,
13
+ max = 100,
14
+ step = 1,
15
+ disabled = false,
16
+ showValue = false,
17
+ showMinMax = false,
18
+ marks = [],
19
+ intent = 'primary',
20
+ size = 'md',
21
+ icon,
22
+ onValueChange,
23
+ onValueCommit,
24
+ style,
25
+ testID,
26
+ }, ref) => {
27
+ const [internalValue, setInternalValue] = useState(defaultValue);
28
+ const [isDragging, setIsDragging] = useState(false);
29
+ const trackRef = useRef<HTMLDivElement>(null);
30
+ const thumbRef = useRef<HTMLDivElement>(null);
31
+ const hasMoved = useRef(false);
32
+
33
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
34
+
35
+ // Apply variants
36
+ sliderStyles.useVariants({
37
+ size,
38
+ disabled,
39
+ });
40
+
41
+ const containerProps = getWebProps([sliderStyles.container, style as any]);
42
+ const wrapperProps = getWebProps([sliderStyles.sliderWrapper]);
43
+ const trackProps = getWebProps([sliderStyles.track]);
44
+ const thumbIconProps = getWebProps([sliderStyles.thumbIcon({ intent })]);
45
+ const valueLabelProps = getWebProps([sliderStyles.valueLabel]);
46
+ const minMaxLabelsProps = getWebProps([sliderStyles.minMaxLabels]);
47
+ const minMaxLabelProps = getWebProps([sliderStyles.minMaxLabel]);
48
+ const marksProps = getWebProps([sliderStyles.marks]);
49
+
50
+ const clampValue = useCallback((val: number) => {
51
+ const clampedValue = Math.min(Math.max(val, min), max);
52
+ const steppedValue = Math.round(clampedValue / step) * step;
53
+ return Math.min(Math.max(steppedValue, min), max);
54
+ }, [min, max, step]);
55
+
56
+ const calculateValueFromPosition = useCallback((clientX: number) => {
57
+ if (!trackRef.current) return value;
58
+
59
+ const rect = trackRef.current.getBoundingClientRect();
60
+ const percentage = (clientX - rect.left) / rect.width;
61
+ const rawValue = min + percentage * (max - min);
62
+ return clampValue(rawValue);
63
+ }, [min, max, value, clampValue]);
64
+
65
+ const updateValue = useCallback((newValue: number) => {
66
+ const clampedValue = clampValue(newValue);
67
+
68
+ if (controlledValue === undefined) {
69
+ setInternalValue(clampedValue);
70
+ }
71
+
72
+ onValueChange?.(clampedValue);
73
+ }, [controlledValue, clampValue, onValueChange]);
74
+
75
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
76
+ if (disabled) return;
77
+
78
+ e.preventDefault();
79
+
80
+ // Check if click is on the thumb
81
+ const isThumbClick = thumbRef.current && thumbRef.current.contains(e.target as Node);
82
+
83
+ if (isThumbClick) {
84
+ // Clicking on thumb: only start dragging, don't update value yet
85
+ setIsDragging(true);
86
+ hasMoved.current = false;
87
+ } else {
88
+ // Clicking on track: immediately update value
89
+ setIsDragging(true);
90
+ hasMoved.current = true;
91
+ const newValue = calculateValueFromPosition(e.clientX);
92
+ updateValue(newValue);
93
+ }
94
+ }, [disabled, calculateValueFromPosition, updateValue]);
95
+
96
+ const handleMouseMove = useCallback((e: MouseEvent) => {
97
+ if (!isDragging || disabled) return;
98
+
99
+ hasMoved.current = true;
100
+ const newValue = calculateValueFromPosition(e.clientX);
101
+ updateValue(newValue);
102
+ }, [isDragging, disabled, calculateValueFromPosition, updateValue]);
103
+
104
+ const handleMouseUp = useCallback(() => {
105
+ if (isDragging) {
106
+ setIsDragging(false);
107
+ if (hasMoved.current) {
108
+ onValueCommit?.(value);
109
+ }
110
+ hasMoved.current = false;
111
+ }
112
+ }, [isDragging, value, onValueCommit]);
113
+
114
+ React.useEffect(() => {
115
+ if (isDragging) {
116
+ document.addEventListener('mousemove', handleMouseMove);
117
+ document.addEventListener('mouseup', handleMouseUp);
118
+
119
+ return () => {
120
+ document.removeEventListener('mousemove', handleMouseMove);
121
+ document.removeEventListener('mouseup', handleMouseUp);
122
+ };
123
+ }
124
+ }, [isDragging, handleMouseMove, handleMouseUp]);
125
+
126
+ const percentage = ((value - min) / (max - min)) * 100;
127
+
128
+ // Dynamic styles with percentage
129
+ const filledTrackProps = getWebProps([sliderStyles.filledTrack({ intent }), { width: `${percentage}%` }]);
130
+ const thumbProps = getWebProps([
131
+ sliderStyles.thumb({ intent, disabled }),
132
+ isDragging && sliderStyles.thumbActive,
133
+ { left: `${percentage}%` }
134
+ ]);
135
+
136
+ // Helper to render icon
137
+ const renderIcon = () => {
138
+ if (!icon) return null;
139
+
140
+ if (isIconName(icon)) {
141
+ // Resolve icon name to path and render with IconSvg
142
+ const iconPath = resolveIconPath(icon);
143
+ return (
144
+ <IconSvg
145
+ path={iconPath}
146
+ {...thumbIconProps}
147
+ aria-label={icon}
148
+ />
149
+ );
150
+ } else if (isValidElement(icon)) {
151
+ // Render custom component as-is
152
+ return <span {...thumbIconProps}>{icon}</span>;
153
+ }
154
+
155
+ return null;
156
+ };
157
+
158
+ const mergedRef = useMergeRefs(ref, containerProps.ref);
159
+
160
+ return (
161
+ <div {...containerProps} ref={mergedRef} data-testid={testID}>
162
+ {showValue && (
163
+ <div {...valueLabelProps}>
164
+ {value}
165
+ </div>
166
+ )}
167
+
168
+ <div {...wrapperProps}>
169
+ <div
170
+ {...trackProps}
171
+ ref={trackRef}
172
+ onMouseDown={handleMouseDown}
173
+ role="slider"
174
+ aria-valuenow={value}
175
+ aria-valuemin={min}
176
+ aria-valuemax={max}
177
+ aria-disabled={disabled}
178
+ tabIndex={disabled ? -1 : 0}
179
+ >
180
+ {/* Filled track */}
181
+ <div {...filledTrackProps} />
182
+
183
+ {/* Marks */}
184
+ {marks.length > 0 && (
185
+ <div {...marksProps}>
186
+ {marks.map((mark) => {
187
+ const markPercentage = ((mark.value - min) / (max - min)) * 100;
188
+ const markProps = getWebProps([sliderStyles.mark, { left: `${markPercentage}%` }]);
189
+ const markLabelProps = getWebProps([sliderStyles.markLabel, { left: `${markPercentage}%` }]);
190
+ return (
191
+ <div key={mark.value}>
192
+ <div {...markProps} />
193
+ {mark.label && (
194
+ <div {...markLabelProps}>
195
+ {mark.label}
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ })}
201
+ </div>
202
+ )}
203
+
204
+ {/* Thumb */}
205
+ <div
206
+ ref={thumbRef}
207
+ {...thumbProps}
208
+ >
209
+ {renderIcon()}
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ {showMinMax && (
215
+ <div {...minMaxLabelsProps}>
216
+ <span {...minMaxLabelProps}>{min}</span>
217
+ <span {...minMaxLabelProps}>{max}</span>
218
+ </div>
219
+ )}
220
+ </div>
221
+ );
222
+ });
223
+
224
+ Slider.displayName = 'Slider';
225
+
226
+ export default Slider;