@ledgerhq/lumen-ui-rnative 0.1.2 → 0.1.3

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 (97) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/i18n/locales/de.json +3 -0
  3. package/dist/src/i18n/locales/en.json +3 -0
  4. package/dist/src/i18n/locales/es.json +3 -0
  5. package/dist/src/i18n/locales/fr.json +3 -0
  6. package/dist/src/i18n/locales/ja.json +3 -0
  7. package/dist/src/i18n/locales/ko.json +3 -0
  8. package/dist/src/i18n/locales/pt.json +3 -0
  9. package/dist/src/i18n/locales/ru.json +3 -0
  10. package/dist/src/i18n/locales/th.json +3 -0
  11. package/dist/src/i18n/locales/tr.json +3 -0
  12. package/dist/src/i18n/locales/zh.json +3 -0
  13. package/dist/src/lib/Animations/constants.d.ts +28 -0
  14. package/dist/src/lib/Animations/constants.d.ts.map +1 -0
  15. package/dist/src/lib/Animations/constants.js +27 -0
  16. package/dist/src/lib/Animations/index.d.ts +1 -0
  17. package/dist/src/lib/Animations/index.d.ts.map +1 -1
  18. package/dist/src/lib/Animations/index.js +1 -0
  19. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +1 -1
  20. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
  21. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +76 -5
  22. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
  23. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
  24. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +25 -2
  25. package/dist/src/lib/Components/AmountDisplay/types.d.ts +20 -25
  26. package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
  27. package/dist/src/lib/Components/AmountDisplay/types.js +1 -1
  28. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +10 -0
  29. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  30. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.js +114 -0
  31. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts +58 -0
  32. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts.map +1 -0
  33. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.js +61 -0
  34. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts +11 -0
  35. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts.map +1 -0
  36. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.js +7 -0
  37. package/dist/src/lib/Components/SegmentedControl/index.d.ts +3 -0
  38. package/dist/src/lib/Components/SegmentedControl/index.d.ts.map +1 -0
  39. package/dist/src/lib/Components/SegmentedControl/index.js +1 -0
  40. package/dist/src/lib/Components/SegmentedControl/types.d.ts +45 -0
  41. package/dist/src/lib/Components/SegmentedControl/types.d.ts.map +1 -0
  42. package/dist/src/lib/Components/SegmentedControl/types.js +1 -0
  43. package/dist/src/lib/Components/TabBar/TabBar.js +1 -0
  44. package/dist/src/lib/Components/TabBar/types.d.ts +0 -1
  45. package/dist/src/lib/Components/TabBar/types.d.ts.map +1 -1
  46. package/dist/src/lib/Components/index.d.ts +1 -0
  47. package/dist/src/lib/Components/index.d.ts.map +1 -1
  48. package/dist/src/lib/Components/index.js +1 -0
  49. package/package.json +1 -1
  50. package/src/i18n/locales/de.json +3 -0
  51. package/src/i18n/locales/en.json +3 -0
  52. package/src/i18n/locales/es.json +3 -0
  53. package/src/i18n/locales/fr.json +3 -0
  54. package/src/i18n/locales/ja.json +3 -0
  55. package/src/i18n/locales/ko.json +3 -0
  56. package/src/i18n/locales/pt.json +3 -0
  57. package/src/i18n/locales/ru.json +3 -0
  58. package/src/i18n/locales/th.json +3 -0
  59. package/src/i18n/locales/tr.json +3 -0
  60. package/src/i18n/locales/zh.json +3 -0
  61. package/src/lib/Animations/constants.ts +31 -0
  62. package/src/lib/Animations/index.ts +1 -0
  63. package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +7 -1
  64. package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +29 -2
  65. package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +101 -51
  66. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +175 -24
  67. package/src/lib/Components/AmountDisplay/types.ts +22 -25
  68. package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +159 -0
  69. package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +102 -0
  70. package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +57 -0
  71. package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +202 -0
  72. package/src/lib/Components/SegmentedControl/SegmentedControlContext.tsx +17 -0
  73. package/src/lib/Components/SegmentedControl/index.ts +2 -0
  74. package/src/lib/Components/SegmentedControl/types.ts +46 -0
  75. package/src/lib/Components/TabBar/TabBar.tsx +1 -0
  76. package/src/lib/Components/TabBar/types.ts +0 -1
  77. package/src/lib/Components/index.ts +1 -0
  78. package/dist/src/lib/Components/Banner/Banner.figma.d.ts +0 -2
  79. package/dist/src/lib/Components/Banner/Banner.figma.d.ts.map +0 -1
  80. package/dist/src/lib/Components/Banner/Banner.figma.js +0 -45
  81. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts +0 -2
  82. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts.map +0 -1
  83. package/dist/src/lib/Components/Checkbox/Checkbox.figma.js +0 -32
  84. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts +0 -2
  85. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts.map +0 -1
  86. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.js +0 -26
  87. package/dist/src/lib/Components/Switch/Switch.figma.d.ts +0 -2
  88. package/dist/src/lib/Components/Switch/Switch.figma.d.ts.map +0 -1
  89. package/dist/src/lib/Components/Switch/Switch.figma.js +0 -32
  90. package/dist/src/lib/Components/Tile/Tile.figma.d.ts +0 -2
  91. package/dist/src/lib/Components/Tile/Tile.figma.d.ts.map +0 -1
  92. package/dist/src/lib/Components/Tile/Tile.figma.js +0 -28
  93. package/src/lib/Components/Banner/Banner.figma.tsx +0 -59
  94. package/src/lib/Components/Checkbox/Checkbox.figma.tsx +0 -49
  95. package/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.tsx +0 -42
  96. package/src/lib/Components/Switch/Switch.figma.tsx +0 -47
  97. package/src/lib/Components/Tile/Tile.figma.tsx +0 -53
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "react-native",
@@ -27,7 +27,7 @@
27
27
  "./styles": "./src/styles/index.ts"
28
28
  },
29
29
  "dependencies": {
30
- "@ledgerhq/lumen-utils-shared": "0.1.0",
30
+ "@ledgerhq/lumen-utils-shared": "0.1.1",
31
31
  "i18next": "^23.7.0",
32
32
  "react-i18next": "^14.0.0"
33
33
  },
@@ -37,7 +37,7 @@
37
37
  "peerDependencies": {
38
38
  "@types/react": "^19.0.0",
39
39
  "@gorhom/bottom-sheet": "^5.0.0",
40
- "@ledgerhq/lumen-design-core": "0.1.0",
40
+ "@ledgerhq/lumen-design-core": "0.1.1",
41
41
  "react": "^19.0.0",
42
42
  "react-native": "~0.79.7",
43
43
  "react-native-reanimated": "^3.0.0",
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "QR-Code scannen"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Betrag ausgeblendet"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Benutzer-Avatar",
8
11
  "notificationAriaLabel": "Hat Benachrichtigung"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Scan QR code"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Amount hidden"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "User avatar",
8
11
  "notificationAriaLabel": "Has notification"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Escanear código QR"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Importe oculto"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Avatar de usuario",
8
11
  "notificationAriaLabel": "Tiene notificación"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Scanner le code QR"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Montant masqué"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Avatar de l'utilisateur",
8
11
  "notificationAriaLabel": "A une notification"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "QRコードをスキャン"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "金額非表示"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "ユーザーアバター",
8
11
  "notificationAriaLabel": "通知あり"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "QR 코드 스캔"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "금액 숨김"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "사용자 아바타",
8
11
  "notificationAriaLabel": "알림 있음"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Escanear QR Code"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Valor oculto"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Avatar do usuário",
8
11
  "notificationAriaLabel": "Tem notificação"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Сканировать QR-код"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Сумма скрыта"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Аватар пользователя",
8
11
  "notificationAriaLabel": "Есть уведомление"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "สแกน QR Code"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "ยอดเงินถูกซ่อน"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "อวาตาร์ผู้ใช้",
8
11
  "notificationAriaLabel": "มีการแจ้งเตือน"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "Karekodu okut"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "Tutar gizlendi"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "Kullanıcı avatarı",
8
11
  "notificationAriaLabel": "Bildirim var"
@@ -3,6 +3,9 @@
3
3
  "addressInput": {
4
4
  "qrCodeAriaLabel": "扫描二维码"
5
5
  },
6
+ "amountDisplay": {
7
+ "amountHiddenAriaLabel": "金额已隐藏"
8
+ },
6
9
  "avatar": {
7
10
  "defaultAlt": "用户头像",
8
11
  "notificationAriaLabel": "有通知"
@@ -0,0 +1,28 @@
1
+ export declare const durations: {
2
+ readonly '0': 0;
3
+ readonly '75': 75;
4
+ readonly '100': 100;
5
+ readonly '120': 120;
6
+ readonly '150': 150;
7
+ readonly '200': 200;
8
+ readonly '250': 250;
9
+ readonly '300': 300;
10
+ readonly '500': 500;
11
+ readonly '700': 700;
12
+ readonly '1000': 1000;
13
+ readonly '2000': 2000;
14
+ readonly '3000': 3000;
15
+ };
16
+ export type DurationKey = keyof typeof durations;
17
+ export declare const easingCurves: {
18
+ readonly bezier: {
19
+ readonly default: import("react-native-reanimated").EasingFunctionFactory;
20
+ readonly emphasize: import("react-native-reanimated").EasingFunctionFactory;
21
+ readonly out: import("react-native-reanimated").EasingFunctionFactory;
22
+ readonly in: import("react-native-reanimated").EasingFunctionFactory;
23
+ };
24
+ readonly quad: (t: number) => number;
25
+ readonly ease: (t: number) => number;
26
+ readonly linear: (t: number) => number;
27
+ };
28
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/lib/Animations/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;CAcZ,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,SAAS,CAAC;AAEjD,eAAO,MAAM,YAAY;;;;;;;;;;CAUf,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { Easing } from 'react-native-reanimated';
2
+ export const durations = {
3
+ '0': 0,
4
+ '75': 75,
5
+ '100': 100,
6
+ '120': 120,
7
+ '150': 150,
8
+ '200': 200,
9
+ '250': 250,
10
+ '300': 300,
11
+ '500': 500,
12
+ '700': 700,
13
+ '1000': 1000,
14
+ '2000': 2000,
15
+ '3000': 3000,
16
+ };
17
+ export const easingCurves = {
18
+ bezier: {
19
+ default: Easing.bezier(0.4, 0, 0.2, 1),
20
+ emphasize: Easing.bezier(0.05, 0.7, 0.1, 1),
21
+ out: Easing.bezier(0, 0, 0.2, 1),
22
+ in: Easing.bezier(0.4, 0, 1, 1),
23
+ },
24
+ quad: Easing.quad,
25
+ ease: Easing.ease,
26
+ linear: Easing.linear,
27
+ };
@@ -1,4 +1,5 @@
1
1
  export * from './Spin';
2
2
  export * from './Pulse';
3
3
  export * from './types';
4
+ export * from './constants';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/Animations/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/Animations/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export * from './Spin';
2
2
  export * from './Pulse';
3
3
  export * from './types';
4
+ export * from './constants';
@@ -33,7 +33,7 @@ import { AmountDisplayProps } from './types';
33
33
  * ```
34
34
  */
35
35
  export declare const AmountDisplay: {
36
- ({ value, formatter, hidden, loading, ...props }: AmountDisplayProps): import("react/jsx-runtime").JSX.Element;
36
+ ({ value, formatter, hidden, loading, animate, ...props }: AmountDisplayProps): import("react/jsx-runtime").JSX.Element;
37
37
  displayName: string;
38
38
  };
39
39
  //# sourceMappingURL=AmountDisplay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AmountDisplay.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/AmountDisplay.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAoC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa;sDAMvB,kBAAkB;;CA+BpB,CAAC"}
1
+ {"version":3,"file":"AmountDisplay.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/AmountDisplay.tsx"],"names":[],"mappings":"AAaA,OAAO,EACL,kBAAkB,EAInB,MAAM,SAAS,CAAC;AA6IjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa;+DAOvB,kBAAkB;;CA8DpB,CAAC"}
@@ -1,13 +1,53 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { View, Text } from 'react-native';
2
+ import { useSplitText, buildAriaLabel } from '@ledgerhq/lumen-utils-shared';
3
+ import { memo, useEffect } from 'react';
4
+ import { Text, View } from 'react-native';
5
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
6
+ import { useCommonTranslation } from '../../../i18n';
3
7
  import { useStyleSheet } from '../../../styles';
4
8
  import { Pulse } from '../../Animations/Pulse';
5
9
  import { Box } from '../Utility';
10
+ import { DIGITS, } from './types';
11
+ const INTEGER_DIGIT_WIDTHS = {
12
+ 0: 24.5,
13
+ 1: 15,
14
+ 2: 23,
15
+ 3: 24,
16
+ 4: 25,
17
+ 5: 23,
18
+ 6: 24.5,
19
+ 7: 21.5,
20
+ 8: 24,
21
+ 9: 24,
22
+ };
23
+ const DECIMAL_DIGIT_WIDTHS = {
24
+ 0: 17,
25
+ 1: 10.5,
26
+ 2: 16,
27
+ 3: 16.5,
28
+ 4: 17.2,
29
+ 5: 15.7,
30
+ 6: 17,
31
+ 7: 14.7,
32
+ 8: 16.5,
33
+ 9: 16.5,
34
+ };
35
+ const TIMING_CONFIG = {
36
+ duration: 600,
37
+ easing: Easing.inOut(Easing.ease),
38
+ };
6
39
  const useStyles = () => {
7
40
  return useStyleSheet((t) => ({
8
41
  container: {
9
42
  flexDirection: 'row',
10
- alignItems: 'baseline',
43
+ alignItems: 'flex-end',
44
+ },
45
+ integerPartContainer: {
46
+ flexDirection: 'row',
47
+ },
48
+ decimalPartContainer: {
49
+ flexDirection: 'row',
50
+ paddingBottom: t.spacings.s2,
11
51
  },
12
52
  integerText: {
13
53
  ...t.typographies.heading1SemiBold,
@@ -33,6 +73,35 @@ const useStyles = () => {
33
73
  },
34
74
  }), []);
35
75
  };
76
+ const DigitStrip = memo(({ value, textStyle, animate, type }) => {
77
+ const targetWidth = (type === 'integer' ? INTEGER_DIGIT_WIDTHS : DECIMAL_DIGIT_WIDTHS)[value];
78
+ const lineHeight = textStyle.lineHeight;
79
+ const translateY = useSharedValue(-value * lineHeight);
80
+ const width = useSharedValue(targetWidth);
81
+ useEffect(() => {
82
+ if (animate) {
83
+ translateY.value = withTiming(-value * lineHeight, TIMING_CONFIG);
84
+ width.value = withTiming(targetWidth, TIMING_CONFIG);
85
+ }
86
+ else {
87
+ translateY.value = -value * lineHeight;
88
+ width.value = targetWidth;
89
+ }
90
+ }, [value, lineHeight, translateY, animate, width, targetWidth]);
91
+ const animatedStyle = useAnimatedStyle(() => ({
92
+ transform: [{ translateY: translateY.value }],
93
+ }), [translateY]);
94
+ return (_jsx(Animated.View, { style: { height: lineHeight, overflow: 'hidden', width: width }, accessibilityValue: { text: String(value) }, children: _jsx(Animated.View, { style: [animatedStyle, { alignItems: 'center' }], children: DIGITS.map((d) => (_jsx(Text, { style: textStyle, children: d }, d))) }) }));
95
+ });
96
+ const DigitStripList = memo(({ items, textStyle, animate, type }) => {
97
+ return items.map((item, index) => {
98
+ const key = items.length - index;
99
+ if (item.type === 'separator') {
100
+ return (_jsx(Text, { style: textStyle, children: item.value }, key));
101
+ }
102
+ return (_jsx(DigitStrip, { value: Number(item.value), animate: animate, textStyle: textStyle, type: type }, key));
103
+ });
104
+ });
36
105
  /**
37
106
  * AmountDisplay - Renders formatted monetary amounts with flexible currency positioning and decimal formatting.
38
107
  *
@@ -66,10 +135,12 @@ const useStyles = () => {
66
135
  * <AmountDisplay value={1234.56} formatter={usdFormatter} hidden={true} />
67
136
  * ```
68
137
  */
69
- export const AmountDisplay = ({ value, formatter, hidden = false, loading = false, ...props }) => {
138
+ export const AmountDisplay = ({ value, formatter, hidden = false, loading = false, animate = true, ...props }) => {
70
139
  const styles = useStyles();
140
+ const { t } = useCommonTranslation();
71
141
  const parts = formatter(value);
72
- return (_jsx(Box, { ...props, children: _jsx(Pulse, { animate: loading, children: _jsxs(View, { style: styles.container, children: [(parts.currencyPosition === undefined ||
73
- parts.currencyPosition === 'start') && (_jsx(Text, { style: [styles.currencyStartText, styles.spacingStart], children: parts.currencyText })), _jsx(Text, { style: styles.integerText, children: hidden ? '••••' : parts.integerPart }), parts.decimalPart && !hidden && (_jsx(Text, { style: styles.decimalText, children: (parts.decimalSeparator || '.') + parts.decimalPart })), parts.currencyPosition === 'end' && (_jsx(Text, { style: [styles.currencyEndText, styles.spacingEnd], children: parts.currencyText }))] }) }) }));
142
+ const splitDigits = useSplitText(parts);
143
+ const ariaLabel = buildAriaLabel(parts, hidden, t('components.amountDisplay.amountHiddenAriaLabel'));
144
+ return (_jsx(Box, { accessibilityLabel: ariaLabel, accessibilityState: { busy: loading }, ...props, children: _jsx(Pulse, { animate: loading, children: _jsxs(View, { style: styles.container, accessibilityElementsHidden: true, importantForAccessibility: 'no-hide-descendants', children: [_jsxs(View, { style: styles.integerPartContainer, children: [parts.currencyPosition === 'start' && (_jsx(Text, { style: [styles.currencyStartText, styles.spacingStart], children: parts.currencyText })), hidden ? (_jsx(Text, { style: styles.integerText, children: "\u2022\u2022\u2022\u2022" })) : (_jsx(DigitStripList, { items: splitDigits.integerPart, textStyle: styles.integerText, animate: animate, type: 'integer' }))] }), _jsxs(View, { style: styles.decimalPartContainer, children: [!hidden && parts.decimalPart && (_jsx(Text, { style: styles.decimalText, children: parts.decimalSeparator })), parts.decimalPart && !hidden && (_jsx(DigitStripList, { items: splitDigits.decimalPart, textStyle: styles.decimalText, animate: animate, type: 'decimal' })), parts.currencyPosition === 'end' && (_jsx(Text, { style: [styles.currencyEndText, styles.spacingEnd], children: parts.currencyText }))] })] }) }) }));
74
145
  };
75
146
  AmountDisplay.displayName = 'AmountDisplay';
@@ -5,5 +5,6 @@ export default meta;
5
5
  type Story = StoryObj<typeof AmountDisplay>;
6
6
  export declare const Base: Story;
7
7
  export declare const WithHideButton: Story;
8
+ export declare const AnimationShowcase: Story;
8
9
  export declare const Loading: Story;
9
10
  //# sourceMappingURL=AmountDisplay.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AmountDisplay.stories.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAKvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuChD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,aAAa,CA4CpC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5C,eAAO,MAAM,IAAI,EAAE,KAWlB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAqB5B,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAUrB,CAAC"}
1
+ {"version":3,"file":"AmountDisplay.stories.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAKvE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuChD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,aAAa,CAiDpC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5C,eAAO,MAAM,IAAI,EAAE,KAWlB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAqB5B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,KAoB/B,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAUrB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { View } from 'react-native';
4
4
  import { Eye, EyeCross } from '../../Symbols';
5
5
  import { IconButton } from '../IconButton';
@@ -40,6 +40,7 @@ const meta = {
40
40
  args: {
41
41
  formatter: eurFormatter,
42
42
  hidden: false,
43
+ animate: true,
43
44
  },
44
45
  argTypes: {
45
46
  formatter: {
@@ -63,7 +64,11 @@ const meta = {
63
64
  control: {
64
65
  type: 'boolean',
65
66
  },
66
- description: 'When true, displays bullet points instead of the amount',
67
+ },
68
+ animate: {
69
+ control: {
70
+ type: 'boolean',
71
+ },
67
72
  },
68
73
  },
69
74
  parameters: {
@@ -97,6 +102,24 @@ export const WithHideButton = {
97
102
  return (_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 12 }, children: [_jsx(AmountDisplay, { formatter: props.formatter, value: 1234.56, hidden: hidden }), _jsx(IconButton, { appearance: 'transparent', size: 'sm', icon: hidden ? EyeCross : Eye, accessibilityLabel: hidden ? 'Show amount' : 'Hide amount', onPress: () => setHidden((v) => !v) })] }));
98
103
  },
99
104
  };
105
+ export const AnimationShowcase = {
106
+ args: {
107
+ value: 1234.56,
108
+ },
109
+ render: (props) => {
110
+ const [currentValue, setCurrentValue] = useState(props.value);
111
+ useEffect(() => {
112
+ const interval = setInterval(() => {
113
+ setCurrentValue((prev) => {
114
+ const delta = prev * (Math.random() * 0.02 - 0.01);
115
+ return Math.round((prev + delta) * 100) / 100;
116
+ });
117
+ }, 2000);
118
+ return () => clearInterval(interval);
119
+ }, []);
120
+ return _jsx(AmountDisplay, { ...props, value: currentValue });
121
+ },
122
+ };
100
123
  export const Loading = {
101
124
  render: (props) => {
102
125
  return (_jsx(AmountDisplay, { formatter: props.formatter, value: 1234.56, loading: true }));
@@ -1,30 +1,20 @@
1
- import { ViewProps } from 'react-native';
1
+ import type { FormattedValue, SplitChar } from '@ledgerhq/lumen-utils-shared';
2
+ import { ViewProps, TextStyle } from 'react-native';
2
3
  import { StyledViewProps } from '../../../styles';
3
- export type FormattedValue = {
4
- /**
5
- * The whole number portion of the amount (e.g., "1234" from 1234.56)
6
- */
7
- integerPart: string;
8
- /**
9
- * The fractional portion of the amount without the separator (e.g., "56" from 1234.56)
10
- * @optional
11
- */
12
- decimalPart?: string;
13
- /**
14
- * The currency text or symbol (e.g., "$", "USD", "€", "BTC")
15
- */
16
- currencyText: string;
17
- /**
18
- * The character which separates integer and fractional parts.
19
- */
20
- decimalSeparator: '.' | ',';
21
- /**
22
- * Position of the currency text relative to the amount.
23
- * @optional
24
- * @default 'start'
25
- */
26
- currencyPosition?: 'start' | 'end';
4
+ export type { FormattedValue };
5
+ export declare const DIGITS: readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
6
+ type IntegerDigit = (typeof DIGITS)[number];
7
+ export type DigitStripProps = {
8
+ value: IntegerDigit;
9
+ animate: boolean;
10
+ textStyle: TextStyle & {
11
+ lineHeight: number;
12
+ };
13
+ type: 'integer' | 'decimal';
27
14
  };
15
+ export type DigitStripListProps = {
16
+ items: SplitChar[];
17
+ } & Omit<DigitStripProps, 'value'>;
28
18
  /**
29
19
  * Props for the AmountDisplay component.
30
20
  */
@@ -49,5 +39,10 @@ export type AmountDisplayProps = ViewProps & {
49
39
  * @default false
50
40
  */
51
41
  loading?: boolean;
42
+ /**
43
+ * Whether the odometer animation should play on value change or not
44
+ * @default true
45
+ */
46
+ animate?: boolean;
52
47
  } & Omit<StyledViewProps, 'children'>;
53
48
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,gBAAgB,EAAE,GAAG,GAAG,GAAG,CAAC;IAC5B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;CACpC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAC3C;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,cAAc,CAAC;IAC7C;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/AmountDisplay/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,eAAO,MAAM,MAAM,yCAA0C,CAAC;AAE9D,KAAK,YAAY,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,SAAS,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,GAAG,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;AAEnC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAC3C;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,cAAc,CAAC;IAC7C;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC"}
@@ -1 +1 @@
1
- export {};
1
+ export const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -0,0 +1,10 @@
1
+ import type { SegmentedControlButtonProps, SegmentedControlProps } from './types';
2
+ export declare function SegmentedControlButton({ value, children, icon: Icon, onPress, ...props }: SegmentedControlButtonProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare namespace SegmentedControlButton {
4
+ var displayName: string;
5
+ }
6
+ export declare function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, ...props }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare namespace SegmentedControl {
8
+ var displayName: string;
9
+ }
10
+ //# sourceMappingURL=SegmentedControl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentedControl.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAIjB,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,IAAI,EAAE,IAAI,EACV,OAAO,EACP,GAAG,KAAK,EACT,EAAE,2BAA2B,2CAkC7B;yBAxCe,sBAAsB;;;AA4EtC,wBAAgB,gBAAgB,CAAC,EAC/B,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,qBAAqB,2CAwEvB;yBA9Ee,gBAAgB"}
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useEffect, useRef } from 'react';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
4
+ import { useStyleSheet } from '../../../styles';
5
+ import { durations, easingCurves } from '../../Animations/constants';
6
+ import { Box, Pressable, Text } from '../Utility';
7
+ import { SegmentedControlContextProvider, useSegmentedControlContext, } from './SegmentedControlContext';
8
+ const ICON_SIZE = 16;
9
+ export function SegmentedControlButton({ value, children, icon: Icon, onPress, ...props }) {
10
+ const styles = useButtonStyles();
11
+ const { selectedValue, onSelectedChange } = useSegmentedControlContext();
12
+ const selected = selectedValue === value;
13
+ function handlePress() {
14
+ onSelectedChange(value);
15
+ onPress?.();
16
+ }
17
+ return (_jsx(Pressable, { onPress: handlePress, accessibilityState: { selected }, style: styles.button, ...props, children: _jsxs(Box, { style: styles.content, children: [Icon && (_jsx(Box, { style: styles.iconWrap, children: _jsx(Icon, { size: ICON_SIZE }) })), _jsx(Text, { typography: selected ? 'body2SemiBold' : 'body2', lx: { color: 'base' }, style: styles.label, children: children })] }) }));
18
+ }
19
+ SegmentedControlButton.displayName = 'SegmentedControlButton';
20
+ function useButtonStyles() {
21
+ return useStyleSheet((t) => ({
22
+ button: {
23
+ flex: 1,
24
+ flexDirection: 'row',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ paddingHorizontal: t.spacings.s16,
28
+ paddingVertical: t.spacings.s8,
29
+ borderRadius: t.borderRadius.full,
30
+ zIndex: 1,
31
+ },
32
+ content: {
33
+ flexDirection: 'row',
34
+ alignItems: 'center',
35
+ justifyContent: 'center',
36
+ gap: t.spacings.s8,
37
+ },
38
+ label: {
39
+ textAlign: 'center',
40
+ includeFontPadding: false,
41
+ },
42
+ iconWrap: {
43
+ flexDirection: 'row',
44
+ alignItems: 'center',
45
+ },
46
+ }), []);
47
+ }
48
+ export function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, ...props }) {
49
+ const styles = useRootStyles();
50
+ const pillTranslateX = useSharedValue(0);
51
+ const pillWidth = useSharedValue(0);
52
+ const pillHeight = useSharedValue(0);
53
+ const hasLayoutRef = useRef(false);
54
+ const getSelectedIndex = useCallback(() => {
55
+ return React.Children.toArray(children).findIndex((child) => {
56
+ if (React.isValidElement(child) && child.props != null) {
57
+ return child.props.value === selectedValue;
58
+ }
59
+ return false;
60
+ });
61
+ }, [selectedValue, children]);
62
+ function onLayout(e) {
63
+ const { width, height } = e.nativeEvent.layout;
64
+ const count = React.Children.count(children);
65
+ const slotWidth = count > 0 ? width / count : 0;
66
+ pillWidth.value = slotWidth;
67
+ pillHeight.value = height;
68
+ if (!hasLayoutRef.current) {
69
+ hasLayoutRef.current = true;
70
+ const index = getSelectedIndex();
71
+ if (index >= 0) {
72
+ pillTranslateX.value = index * slotWidth;
73
+ }
74
+ }
75
+ }
76
+ useEffect(() => {
77
+ if (!hasLayoutRef.current)
78
+ return;
79
+ const index = getSelectedIndex();
80
+ if (index >= 0 && pillWidth.value > 0) {
81
+ pillTranslateX.value = withTiming(index * pillWidth.value, {
82
+ duration: durations['250'],
83
+ easing: easingCurves.bezier.default,
84
+ });
85
+ }
86
+ }, [pillWidth, pillTranslateX, getSelectedIndex]);
87
+ const animatedPillStyle = useAnimatedStyle(() => ({
88
+ transform: [{ translateX: pillTranslateX.value }],
89
+ width: pillWidth.value,
90
+ height: pillHeight.value,
91
+ }), [pillTranslateX, pillWidth, pillHeight]);
92
+ return (_jsx(SegmentedControlContextProvider, { value: { selectedValue, onSelectedChange }, children: _jsxs(Box, { accessibilityRole: 'radiogroup', accessibilityLabel: accessibilityLabel, onLayout: onLayout, style: styles.container, ...props, children: [children, _jsx(Animated.View, { style: [styles.pill, animatedPillStyle], pointerEvents: 'none' })] }) }));
93
+ }
94
+ SegmentedControl.displayName = 'SegmentedControl';
95
+ function useRootStyles() {
96
+ return useStyleSheet((t) => ({
97
+ container: {
98
+ flexDirection: 'row',
99
+ alignItems: 'center',
100
+ position: 'relative',
101
+ width: '100%',
102
+ borderRadius: t.borderRadius.full,
103
+ backgroundColor: t.colors.bg.baseTransparent,
104
+ },
105
+ pill: {
106
+ position: 'absolute',
107
+ top: 0,
108
+ left: 0,
109
+ borderRadius: t.borderRadius.sm,
110
+ backgroundColor: t.colors.bg.muted,
111
+ zIndex: 0,
112
+ },
113
+ }), []);
114
+ }