@retray-dev/ui-kit 1.5.0 → 1.6.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/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference
1
+ # @retray-dev/ui-kit — Component Reference (v1.6.0)
2
2
 
3
3
  This file is the AI reference for this package. It is shipped inside the npm package so consuming projects can import it into their `CLAUDE.md` with:
4
4
 
@@ -74,7 +74,7 @@ function MyComponent() {
74
74
 
75
75
  ## Theme Tokens
76
76
 
77
- All 18 tokens are available via `useTheme().colors`.
77
+ All 20 tokens are available via `useTheme().colors`.
78
78
 
79
79
  | Token | Light | Dark | Semantic Role |
80
80
  |-------|-------|------|---------------|
@@ -95,6 +95,8 @@ All 18 tokens are available via `useTheme().colors`.
95
95
  | `border` | `#e5e5e5` | `#2a2a2a` | Borders and dividers |
96
96
  | `input` | `#e5e5e5` | `#2a2a2a` | Input field border color |
97
97
  | `ring` | `#a3a3a3` | `#d4d4d4` | Focus ring color |
98
+ | `success` | `#16a34a` | `#22c55e` | Success state (Toast success variant) |
99
+ | `successForeground` | `#ffffff` | `#ffffff` | Text on success background |
98
100
 
99
101
  ---
100
102
 
@@ -145,6 +147,8 @@ All 18 tokens are available via `useTheme().colors`.
145
147
  | loading | `boolean` | `false` | Replaces label with a spinner and forces disabled state |
146
148
  | fullWidth | `boolean` | `false` | Stretches to container width (`alignSelf: 'stretch'`) |
147
149
  | disabled | `boolean` | — | Reduces opacity to 0.45 |
150
+ | icon | `React.ReactNode` | — | Icon rendered alongside the label |
151
+ | iconPosition | `'left' \| 'right'` | `'left'` | Side the icon appears on |
148
152
 
149
153
  **Variants:**
150
154
  - `primary`: filled with `primary` token — main actions
@@ -160,6 +164,7 @@ All 18 tokens are available via `useTheme().colors`.
160
164
  <Button label="Cancel" variant="ghost" onPress={onCancel} />
161
165
  <Button label="Delete" variant="outline" size="sm" />
162
166
  <Button label="Submitting..." loading fullWidth />
167
+ <Button label="Share" icon={<ShareIcon size={16} />} iconPosition="right" />
163
168
  ```
164
169
 
165
170
  ---
@@ -187,6 +192,50 @@ All 18 tokens are available via `useTheme().colors`.
187
192
 
188
193
  ---
189
194
 
195
+ ### CurrencyInput
196
+
197
+ **Import:** `import { CurrencyInput } from '@retray-dev/ui-kit'`
198
+ **When to use:** Monetary or numeric inputs that need thousands formatting while typing. Wraps `Input` — shares the same visual design, label, error, and hint behavior.
199
+
200
+ | Prop | Type | Default | Notes |
201
+ |------|------|---------|-------|
202
+ | value | `string` | — | Controlled display value (includes prefix, e.g. `'$1,234'`) |
203
+ | onChangeText | `(formatted: string) => void` | — | Called with the formatted display string |
204
+ | onChangeValue | `(raw: number) => void` | — | Called with the parsed number (no separators, no prefix) |
205
+ | prefix | `string` | `'$'` | Symbol prepended to the formatted value |
206
+ | thousandsSeparator | `'.' \| ','` | `'.'` | Character used to separate groups of three digits |
207
+ | label | `string` | — | Label above the input |
208
+ | error | `string` | — | Error text below; turns border red |
209
+ | hint | `string` | — | Helper text below (hidden when `error` is set) |
210
+ | placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
211
+ | editable | `boolean` | — | Pass `false` to disable editing |
212
+ | containerStyle | `ViewStyle` | — | Style for the outer container |
213
+
214
+ **Example:**
215
+ ```tsx
216
+ const [display, setDisplay] = useState('')
217
+ const [amount, setAmount] = useState(0)
218
+
219
+ <CurrencyInput
220
+ label="Amount"
221
+ value={display}
222
+ onChangeText={setDisplay}
223
+ onChangeValue={setAmount}
224
+ hint={`Parsed: ${amount}`}
225
+ />
226
+
227
+ // European format (dot as thousands separator)
228
+ <CurrencyInput
229
+ prefix="€"
230
+ thousandsSeparator="."
231
+ value={display}
232
+ onChangeText={setDisplay}
233
+ onChangeValue={setAmount}
234
+ />
235
+ ```
236
+
237
+ ---
238
+
190
239
  ### Textarea
191
240
 
192
241
  **Import:** `import { Textarea } from '@retray-dev/ui-kit'`
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A personal React Native / Expo UI component library with a built-in design system, dark mode support, haptic feedback, and smooth animations.
4
4
 
5
- - 23 components across 5 categories
5
+ - 24 components across 5 categories
6
6
  - Light/dark theme with 18 color tokens and full customization
7
7
  - Apple HIG–compliant touch targets and haptic feedback
8
8
  - Animated interactions: spring press, sliding tabs, accordion easing, animated progress
@@ -101,7 +101,7 @@ const { colors, colorScheme } = useTheme()
101
101
  | ----------- | ----------------------------------------------------------------------------------------------- |
102
102
  | Display | `Text`, `Badge`, `Avatar`, `Separator`, `Spinner`, `Skeleton`, `Progress` |
103
103
  | Surfaces | `Card`, `Alert`, `EmptyState` |
104
- | Form | `Button`, `Input`, `Textarea`, `Checkbox`, `Switch`, `Toggle`, `RadioGroup`, `Select`, `Slider` |
104
+ | Form | `Button`, `Input`, `CurrencyInput`, `Textarea`, `Checkbox`, `Switch`, `Toggle`, `RadioGroup`, `Select`, `Slider` |
105
105
  | Composition | `Tabs`, `Accordion` |
106
106
  | Overlays | `Sheet` |
107
107
  | Feedback | `Toast` / `ToastProvider` / `useToast` |
package/dist/index.d.mts CHANGED
@@ -367,4 +367,23 @@ interface ToastProviderProps {
367
367
  }
368
368
  declare function ToastProvider({ children }: ToastProviderProps): React.JSX.Element;
369
369
 
370
- export { Accordion, type AccordionItem, type AccordionProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ColorScheme, EmptyState, type EmptyStateProps, Input, type InputProps, Progress, type ProgressProps, RadioGroup, type RadioGroupProps, type RadioOption, Select, type SelectOption, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, Switch, type SwitchProps, type TabItem, Tabs, TabsContent, type TabsContentProps, type TabsProps, Text, type TextProps, type TextVariant, Textarea, type TextareaProps, type Theme, type ThemeColors, ThemeProvider, type ThemeProviderProps, type ToastItem, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, type ToggleVariant, defaultDark, defaultLight, useTheme, useToast };
370
+ interface CurrencyInputProps {
371
+ value?: string;
372
+ onChangeText?: (formatted: string) => void;
373
+ /** Called with the parsed numeric value (no separators, no prefix). */
374
+ onChangeValue?: (raw: number) => void;
375
+ /** Symbol prepended to the formatted value. Defaults to `'$'`. */
376
+ prefix?: string;
377
+ /** Character used to separate groups of three digits. Defaults to `'.'`. */
378
+ thousandsSeparator?: '.' | ',';
379
+ label?: string;
380
+ /** Red helper text; also changes input border to destructive color. */
381
+ error?: string;
382
+ hint?: string;
383
+ placeholder?: string;
384
+ editable?: boolean;
385
+ containerStyle?: ViewStyle;
386
+ }
387
+ declare function CurrencyInput({ value, onChangeText, onChangeValue, prefix, thousandsSeparator, label, error, hint, placeholder, editable, containerStyle, }: CurrencyInputProps): React.JSX.Element;
388
+
389
+ export { Accordion, type AccordionItem, type AccordionProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ColorScheme, CurrencyInput, type CurrencyInputProps, EmptyState, type EmptyStateProps, Input, type InputProps, Progress, type ProgressProps, RadioGroup, type RadioGroupProps, type RadioOption, Select, type SelectOption, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, Switch, type SwitchProps, type TabItem, Tabs, TabsContent, type TabsContentProps, type TabsProps, Text, type TextProps, type TextVariant, Textarea, type TextareaProps, type Theme, type ThemeColors, ThemeProvider, type ThemeProviderProps, type ToastItem, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, type ToggleVariant, defaultDark, defaultLight, useTheme, useToast };
package/dist/index.d.ts CHANGED
@@ -367,4 +367,23 @@ interface ToastProviderProps {
367
367
  }
368
368
  declare function ToastProvider({ children }: ToastProviderProps): React.JSX.Element;
369
369
 
370
- export { Accordion, type AccordionItem, type AccordionProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ColorScheme, EmptyState, type EmptyStateProps, Input, type InputProps, Progress, type ProgressProps, RadioGroup, type RadioGroupProps, type RadioOption, Select, type SelectOption, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, Switch, type SwitchProps, type TabItem, Tabs, TabsContent, type TabsContentProps, type TabsProps, Text, type TextProps, type TextVariant, Textarea, type TextareaProps, type Theme, type ThemeColors, ThemeProvider, type ThemeProviderProps, type ToastItem, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, type ToggleVariant, defaultDark, defaultLight, useTheme, useToast };
370
+ interface CurrencyInputProps {
371
+ value?: string;
372
+ onChangeText?: (formatted: string) => void;
373
+ /** Called with the parsed numeric value (no separators, no prefix). */
374
+ onChangeValue?: (raw: number) => void;
375
+ /** Symbol prepended to the formatted value. Defaults to `'$'`. */
376
+ prefix?: string;
377
+ /** Character used to separate groups of three digits. Defaults to `'.'`. */
378
+ thousandsSeparator?: '.' | ',';
379
+ label?: string;
380
+ /** Red helper text; also changes input border to destructive color. */
381
+ error?: string;
382
+ hint?: string;
383
+ placeholder?: string;
384
+ editable?: boolean;
385
+ containerStyle?: ViewStyle;
386
+ }
387
+ declare function CurrencyInput({ value, onChangeText, onChangeValue, prefix, thousandsSeparator, label, error, hint, placeholder, editable, containerStyle, }: CurrencyInputProps): React.JSX.Element;
388
+
389
+ export { Accordion, type AccordionItem, type AccordionProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ColorScheme, CurrencyInput, type CurrencyInputProps, EmptyState, type EmptyStateProps, Input, type InputProps, Progress, type ProgressProps, RadioGroup, type RadioGroupProps, type RadioOption, Select, type SelectOption, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, Switch, type SwitchProps, type TabItem, Tabs, TabsContent, type TabsContentProps, type TabsProps, Text, type TextProps, type TextVariant, Textarea, type TextareaProps, type Theme, type ThemeColors, ThemeProvider, type ThemeProviderProps, type ToastItem, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, type ToggleVariant, defaultDark, defaultLight, useTheme, useToast };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ var Haptics11 = require('expo-haptics');
6
6
  var expoLinearGradient = require('expo-linear-gradient');
7
7
  var ReanimatedAnimated = require('react-native-reanimated');
8
8
  var bottomSheet = require('@gorhom/bottom-sheet');
9
+ var reactNativeWorklets = require('react-native-worklets');
9
10
  var reactNativeGestureHandler = require('react-native-gesture-handler');
10
11
  var reactNativeSafeAreaContext = require('react-native-safe-area-context');
11
12
 
@@ -1617,7 +1618,7 @@ function ToastNotification({ item, onDismiss }) {
1617
1618
  const timer = setTimeout(() => {
1618
1619
  translateY.value = ReanimatedAnimated.withTiming(-80, { duration: 200 });
1619
1620
  opacity.value = ReanimatedAnimated.withTiming(0, { duration: 200 }, (done) => {
1620
- if (done) ReanimatedAnimated.runOnJS(onDismiss)();
1621
+ if (done) reactNativeWorklets.scheduleOnRN(onDismiss);
1621
1622
  });
1622
1623
  }, item.duration ?? 3e3);
1623
1624
  return () => clearTimeout(timer);
@@ -1629,7 +1630,7 @@ function ToastNotification({ item, onDismiss }) {
1629
1630
  if (shouldDismiss) {
1630
1631
  const direction = translateX.value > 0 ? 1 : -1;
1631
1632
  translateX.value = ReanimatedAnimated.withTiming(direction * 500, { duration: 200 }, (done) => {
1632
- if (done) ReanimatedAnimated.runOnJS(onDismiss)();
1633
+ if (done) reactNativeWorklets.scheduleOnRN(onDismiss);
1633
1634
  });
1634
1635
  opacity.value = ReanimatedAnimated.withTiming(0, { duration: 150 });
1635
1636
  } else {
@@ -1710,6 +1711,48 @@ var styles21 = reactNative.StyleSheet.create({
1710
1711
  fontSize: 12
1711
1712
  }
1712
1713
  });
1714
+ function formatCurrency(raw, separator) {
1715
+ const digits = raw.replace(/\D/g, "");
1716
+ if (!digits) return "";
1717
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
1718
+ }
1719
+ function CurrencyInput({
1720
+ value,
1721
+ onChangeText,
1722
+ onChangeValue,
1723
+ prefix = "$",
1724
+ thousandsSeparator = ".",
1725
+ label,
1726
+ error,
1727
+ hint,
1728
+ placeholder,
1729
+ editable,
1730
+ containerStyle
1731
+ }) {
1732
+ const handleChange = (text) => {
1733
+ const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text;
1734
+ const formatted = formatCurrency(withoutPrefix, thousandsSeparator);
1735
+ const display = formatted ? `${prefix}${formatted}` : "";
1736
+ onChangeText?.(display);
1737
+ const separatorRegex = new RegExp(`\\${thousandsSeparator}`, "g");
1738
+ const raw = parseFloat(formatted.replace(separatorRegex, "") || "0");
1739
+ onChangeValue?.(isNaN(raw) ? 0 : raw);
1740
+ };
1741
+ return /* @__PURE__ */ React23__default.default.createElement(
1742
+ Input,
1743
+ {
1744
+ value,
1745
+ onChangeText: handleChange,
1746
+ keyboardType: "numeric",
1747
+ label,
1748
+ error,
1749
+ hint,
1750
+ placeholder: placeholder ?? `${prefix}0`,
1751
+ editable,
1752
+ containerStyle
1753
+ }
1754
+ );
1755
+ }
1713
1756
 
1714
1757
  Object.defineProperty(exports, "BottomSheetModalProvider", {
1715
1758
  enumerable: true,
@@ -1727,6 +1770,7 @@ exports.CardFooter = CardFooter;
1727
1770
  exports.CardHeader = CardHeader;
1728
1771
  exports.CardTitle = CardTitle;
1729
1772
  exports.Checkbox = Checkbox;
1773
+ exports.CurrencyInput = CurrencyInput;
1730
1774
  exports.EmptyState = EmptyState;
1731
1775
  exports.Input = Input;
1732
1776
  exports.Progress = Progress;
package/dist/index.mjs CHANGED
@@ -2,9 +2,10 @@ import React23, { createContext, useMemo, useContext, useRef, useState, useEffec
2
2
  import { StyleSheet, useColorScheme, Animated, TouchableOpacity, ActivityIndicator, Text, View, TextInput, Image, PanResponder } from 'react-native';
3
3
  import * as Haptics11 from 'expo-haptics';
4
4
  import { LinearGradient } from 'expo-linear-gradient';
5
- import ReanimatedAnimated, { useSharedValue, useAnimatedStyle, withTiming, Easing, runOnJS, withSpring } from 'react-native-reanimated';
5
+ import ReanimatedAnimated, { useSharedValue, useAnimatedStyle, withTiming, Easing, withSpring } from 'react-native-reanimated';
6
6
  import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
7
7
  export { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
8
+ import { scheduleOnRN } from 'react-native-worklets';
8
9
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
9
10
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
11
 
@@ -1592,7 +1593,7 @@ function ToastNotification({ item, onDismiss }) {
1592
1593
  const timer = setTimeout(() => {
1593
1594
  translateY.value = withTiming(-80, { duration: 200 });
1594
1595
  opacity.value = withTiming(0, { duration: 200 }, (done) => {
1595
- if (done) runOnJS(onDismiss)();
1596
+ if (done) scheduleOnRN(onDismiss);
1596
1597
  });
1597
1598
  }, item.duration ?? 3e3);
1598
1599
  return () => clearTimeout(timer);
@@ -1604,7 +1605,7 @@ function ToastNotification({ item, onDismiss }) {
1604
1605
  if (shouldDismiss) {
1605
1606
  const direction = translateX.value > 0 ? 1 : -1;
1606
1607
  translateX.value = withTiming(direction * 500, { duration: 200 }, (done) => {
1607
- if (done) runOnJS(onDismiss)();
1608
+ if (done) scheduleOnRN(onDismiss);
1608
1609
  });
1609
1610
  opacity.value = withTiming(0, { duration: 150 });
1610
1611
  } else {
@@ -1685,5 +1686,47 @@ var styles21 = StyleSheet.create({
1685
1686
  fontSize: 12
1686
1687
  }
1687
1688
  });
1689
+ function formatCurrency(raw, separator) {
1690
+ const digits = raw.replace(/\D/g, "");
1691
+ if (!digits) return "";
1692
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
1693
+ }
1694
+ function CurrencyInput({
1695
+ value,
1696
+ onChangeText,
1697
+ onChangeValue,
1698
+ prefix = "$",
1699
+ thousandsSeparator = ".",
1700
+ label,
1701
+ error,
1702
+ hint,
1703
+ placeholder,
1704
+ editable,
1705
+ containerStyle
1706
+ }) {
1707
+ const handleChange = (text) => {
1708
+ const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text;
1709
+ const formatted = formatCurrency(withoutPrefix, thousandsSeparator);
1710
+ const display = formatted ? `${prefix}${formatted}` : "";
1711
+ onChangeText?.(display);
1712
+ const separatorRegex = new RegExp(`\\${thousandsSeparator}`, "g");
1713
+ const raw = parseFloat(formatted.replace(separatorRegex, "") || "0");
1714
+ onChangeValue?.(isNaN(raw) ? 0 : raw);
1715
+ };
1716
+ return /* @__PURE__ */ React23.createElement(
1717
+ Input,
1718
+ {
1719
+ value,
1720
+ onChangeText: handleChange,
1721
+ keyboardType: "numeric",
1722
+ label,
1723
+ error,
1724
+ hint,
1725
+ placeholder: placeholder ?? `${prefix}0`,
1726
+ editable,
1727
+ containerStyle
1728
+ }
1729
+ );
1730
+ }
1688
1731
 
1689
- export { Accordion, Alert, Avatar, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, EmptyState, Input, Progress, RadioGroup, Select, Separator, Sheet, Skeleton, Slider, Spinner, Switch, Tabs, TabsContent, Text2 as Text, Textarea, ThemeProvider, ToastProvider, Toggle, defaultDark, defaultLight, useTheme, useToast };
1732
+ export { Accordion, Alert, Avatar, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, CurrencyInput, EmptyState, Input, Progress, RadioGroup, Select, Separator, Sheet, Skeleton, Slider, Spinner, Switch, Tabs, TabsContent, Text2 as Text, Textarea, ThemeProvider, ToastProvider, Toggle, defaultDark, defaultLight, useTheme, useToast };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -44,7 +44,7 @@
44
44
  "@gorhom/bottom-sheet": ">=5.0.0",
45
45
  "react-native-reanimated": ">=4.0.0",
46
46
  "react-native-gesture-handler": ">=2.0.0",
47
- "react-native-worklets": ">=0.5.0",
47
+ "react-native-worklets": ">=0.8.0",
48
48
  "react-native-safe-area-context": ">=4.0.0"
49
49
  },
50
50
  "pnpm": {
@@ -52,7 +52,7 @@
52
52
  "fast-xml-parser": "^5.5.7",
53
53
  "react": "19.1.0",
54
54
  "react-native": "0.81.5",
55
- "react-native-worklets": "0.5.1"
55
+ "react-native-worklets": "0.8.1"
56
56
  },
57
57
  "onlyBuiltDependencies": [
58
58
  "esbuild"
@@ -65,9 +65,9 @@
65
65
  "expo-linear-gradient": "~14.1.5",
66
66
  "react": "18.2.0",
67
67
  "react-native": "0.74.0",
68
- "react-native-gesture-handler": "~2.28.0",
69
- "react-native-reanimated": "~4.1.1",
70
- "react-native-worklets": "~0.5.0",
68
+ "react-native-gesture-handler": "~2.30.0",
69
+ "react-native-reanimated": "~4.3.0",
70
+ "react-native-worklets": "~0.8.1",
71
71
  "react-native-safe-area-context": "~5.6.2",
72
72
  "eslint": "^9.0.0",
73
73
  "@eslint/js": "^9.0.0",
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+ import { ViewStyle } from 'react-native'
3
+ import { Input } from '../Input'
4
+
5
+ export interface CurrencyInputProps {
6
+ value?: string
7
+ onChangeText?: (formatted: string) => void
8
+ /** Called with the parsed numeric value (no separators, no prefix). */
9
+ onChangeValue?: (raw: number) => void
10
+ /** Symbol prepended to the formatted value. Defaults to `'$'`. */
11
+ prefix?: string
12
+ /** Character used to separate groups of three digits. Defaults to `'.'`. */
13
+ thousandsSeparator?: '.' | ','
14
+ label?: string
15
+ /** Red helper text; also changes input border to destructive color. */
16
+ error?: string
17
+ hint?: string
18
+ placeholder?: string
19
+ editable?: boolean
20
+ containerStyle?: ViewStyle
21
+ }
22
+
23
+ function formatCurrency(raw: string, separator: '.' | ','): string {
24
+ const digits = raw.replace(/\D/g, '')
25
+ if (!digits) return ''
26
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator)
27
+ }
28
+
29
+ export function CurrencyInput({
30
+ value,
31
+ onChangeText,
32
+ onChangeValue,
33
+ prefix = '$',
34
+ thousandsSeparator = '.',
35
+ label,
36
+ error,
37
+ hint,
38
+ placeholder,
39
+ editable,
40
+ containerStyle,
41
+ }: CurrencyInputProps) {
42
+ const handleChange = (text: string) => {
43
+ const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
44
+ const formatted = formatCurrency(withoutPrefix, thousandsSeparator)
45
+ const display = formatted ? `${prefix}${formatted}` : ''
46
+ onChangeText?.(display)
47
+ const separatorRegex = new RegExp(`\\${thousandsSeparator}`, 'g')
48
+ const raw = parseFloat(formatted.replace(separatorRegex, '') || '0')
49
+ onChangeValue?.(isNaN(raw) ? 0 : raw)
50
+ }
51
+
52
+ return (
53
+ <Input
54
+ value={value}
55
+ onChangeText={handleChange}
56
+ keyboardType="numeric"
57
+ label={label}
58
+ error={error}
59
+ hint={hint}
60
+ placeholder={placeholder ?? `${prefix}0`}
61
+ editable={editable}
62
+ containerStyle={containerStyle}
63
+ />
64
+ )
65
+ }
@@ -0,0 +1,2 @@
1
+ export { CurrencyInput } from './CurrencyInput'
2
+ export type { CurrencyInputProps } from './CurrencyInput'
@@ -5,9 +5,9 @@ import Animated, {
5
5
  useAnimatedStyle,
6
6
  withSpring,
7
7
  withTiming,
8
- runOnJS,
9
8
  Easing,
10
9
  } from 'react-native-reanimated'
10
+ import { scheduleOnRN } from 'react-native-worklets'
11
11
  import { Gesture, GestureDetector } from 'react-native-gesture-handler'
12
12
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
13
13
  import * as Haptics from 'expo-haptics'
@@ -54,7 +54,7 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
54
54
  const timer = setTimeout(() => {
55
55
  translateY.value = withTiming(-80, { duration: 200 })
56
56
  opacity.value = withTiming(0, { duration: 200 }, (done) => {
57
- if (done) runOnJS(onDismiss)()
57
+ if (done) scheduleOnRN(onDismiss)
58
58
  })
59
59
  }, item.duration ?? 3000)
60
60
 
@@ -72,7 +72,7 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
72
72
  if (shouldDismiss) {
73
73
  const direction = translateX.value > 0 ? 1 : -1
74
74
  translateX.value = withTiming(direction * 500, { duration: 200 }, (done) => {
75
- if (done) runOnJS(onDismiss)()
75
+ if (done) scheduleOnRN(onDismiss)
76
76
  })
77
77
  opacity.value = withTiming(0, { duration: 150 })
78
78
  } else {
package/src/index.ts CHANGED
@@ -27,3 +27,4 @@ export * from './components/Slider'
27
27
  export * from './components/Sheet'
28
28
  export * from './components/Select'
29
29
  export * from './components/Toast'
30
+ export * from './components/CurrencyInput'