jfs-components 0.1.25 → 0.1.30

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/CHANGELOG.md +53 -0
  2. package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
  3. package/lib/commonjs/components/Carousel/Carousel.js +112 -19
  4. package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
  5. package/lib/commonjs/components/ContentSheet/ContentSheet.js +104 -10
  6. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  7. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  8. package/lib/commonjs/components/Grid/Grid.js +206 -0
  9. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  10. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  11. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  12. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  13. package/lib/commonjs/components/Table/Table.js +27 -7
  14. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  15. package/lib/commonjs/components/index.js +42 -0
  16. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  17. package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
  18. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  19. package/lib/commonjs/icons/components/IconImage.js +19 -0
  20. package/lib/commonjs/icons/components/IconMic.js +19 -0
  21. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  22. package/lib/commonjs/icons/components/IconPause.js +19 -0
  23. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  24. package/lib/commonjs/icons/components/index.js +66 -0
  25. package/lib/commonjs/icons/registry.js +2 -2
  26. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  27. package/lib/module/components/Carousel/Carousel.js +111 -19
  28. package/lib/module/components/CompareTable/CompareTable.js +191 -26
  29. package/lib/module/components/ContentSheet/ContentSheet.js +108 -14
  30. package/lib/module/components/FormUpload/Additem.js +162 -0
  31. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  32. package/lib/module/components/Grid/Grid.js +201 -0
  33. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  34. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  35. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  36. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  37. package/lib/module/components/Table/Table.js +27 -7
  38. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  39. package/lib/module/components/index.js +7 -1
  40. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  41. package/lib/module/design-tokens/figma-modes.generated.js +21 -10
  42. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  43. package/lib/module/icons/components/IconImage.js +12 -0
  44. package/lib/module/icons/components/IconMic.js +12 -0
  45. package/lib/module/icons/components/IconMicoff.js +12 -0
  46. package/lib/module/icons/components/IconPause.js +12 -0
  47. package/lib/module/icons/components/IconPlay.js +12 -0
  48. package/lib/module/icons/components/index.js +6 -0
  49. package/lib/module/icons/registry.js +2 -2
  50. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  51. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  52. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
  53. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  54. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  55. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  56. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  57. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  58. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  59. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  60. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  61. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  62. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  63. package/lib/typescript/src/components/index.d.ts +34 -28
  64. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  65. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  66. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  67. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  72. package/lib/typescript/src/icons/registry.d.ts +1 -1
  73. package/package.json +1 -1
  74. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  75. package/src/components/Carousel/Carousel.tsx +133 -23
  76. package/src/components/CompareTable/CompareTable.tsx +224 -30
  77. package/src/components/ContentSheet/ContentSheet.tsx +138 -8
  78. package/src/components/FormUpload/Additem.tsx +262 -0
  79. package/src/components/FormUpload/FormUpload.tsx +313 -0
  80. package/src/components/Grid/Grid.tsx +368 -0
  81. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  82. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  83. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  84. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  85. package/src/components/Table/Table.tsx +29 -4
  86. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  87. package/src/components/index.ts +329 -182
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +21 -10
  90. package/src/icons/components/IconBrokenimage.tsx +11 -0
  91. package/src/icons/components/IconImage.tsx +11 -0
  92. package/src/icons/components/IconMic.tsx +11 -0
  93. package/src/icons/components/IconMicoff.tsx +11 -0
  94. package/src/icons/components/IconPause.tsx +11 -0
  95. package/src/icons/components/IconPlay.tsx +11 -0
  96. package/src/icons/components/index.ts +6 -0
  97. package/src/icons/registry.ts +25 -1
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
 
3
- import React, { useContext, useMemo } from 'react';
4
- import { Platform, View } from 'react-native';
5
- import Animated, { useAnimatedKeyboard, useAnimatedStyle } from 'react-native-reanimated';
3
+ import React, { useContext, useEffect, useMemo } from 'react';
4
+ import { Platform, Text, View, useWindowDimensions } from 'react-native';
5
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
6
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
8
8
  import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
9
- import { jsx as _jsx } from "react/jsx-runtime";
9
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  const IS_WEB = Platform.OS === 'web';
11
+
12
+ // Spring config — mirrors the Drawer's bottom pop-up spring so the two
13
+ // bottom-sheet surfaces share the exact same entrance feel (slight bounce,
14
+ // snappy). Keep in sync with `Drawer.tsx` SPRING_CONFIG.
15
+ const SPRING_CONFIG = {
16
+ damping: 32,
17
+ stiffness: 300,
18
+ mass: 1,
19
+ overshootClamping: false,
20
+ restDisplacementThreshold: 0.1,
21
+ restSpeedThreshold: 0.1
22
+ };
11
23
  function resolveContentSheetStyle(modes) {
12
24
  const backgroundColor = getVariableByName('contentSheet/bg', modes);
13
25
  const gap = getVariableByName('contentSheet/gap', modes);
@@ -16,6 +28,18 @@ function resolveContentSheetStyle(modes) {
16
28
  const paddingBottom = getVariableByName('contentSheet/padding/bottom', modes);
17
29
  const radiusTop = getVariableByName('contentSheet/padding/radius/top', modes);
18
30
  const radiusBottom = getVariableByName('contentSheet/padding/radius/bottom', modes);
31
+
32
+ // Title tokens mirror the Drawer title so the two bottom-sheet surfaces stay
33
+ // visually aligned.
34
+ const titleColor = getVariableByName('drawer/title/color', modes);
35
+ const titleSize = getVariableByName('drawer/title/fontSize', modes);
36
+ const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes);
37
+ const titleFontFamily = getVariableByName('drawer/title/fontFamily', modes);
38
+ const titleFontWeightRaw = getVariableByName('drawer/title/fontWeight', modes);
39
+ const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
40
+ const titleWrapPaddingTop = getVariableByName('drawer/titleWrap/padding/top', modes);
41
+ const titleWrapPaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes);
42
+ const titleWrapPaddingHorizontal = getVariableByName('drawer/titleWrap/padding/horizontal', modes);
19
43
  return {
20
44
  container: {
21
45
  backgroundColor,
@@ -31,7 +55,21 @@ function resolveContentSheetStyle(modes) {
31
55
  alignItems: 'stretch',
32
56
  overflow: 'hidden'
33
57
  },
34
- paddingBottom
58
+ paddingBottom,
59
+ headerStyle: {
60
+ paddingTop: titleWrapPaddingTop,
61
+ paddingBottom: titleWrapPaddingBottom,
62
+ paddingHorizontal: titleWrapPaddingHorizontal,
63
+ alignItems: 'center'
64
+ },
65
+ titleStyle: {
66
+ color: titleColor,
67
+ fontSize: titleSize,
68
+ lineHeight: titleLineHeight,
69
+ fontFamily: titleFontFamily,
70
+ fontWeight: titleFontWeight,
71
+ textAlign: 'center'
72
+ }
35
73
  };
36
74
  }
37
75
  const pinnedStyle = {
@@ -60,10 +98,12 @@ const pinnedStyle = {
60
98
  function ContentSheet({
61
99
  children,
62
100
  modes = EMPTY_MODES,
101
+ title,
63
102
  avoidKeyboard = true,
64
103
  keyboardSpacing = 0,
65
104
  safeAreaBottom = true,
66
105
  pinToBottom = true,
106
+ visible = true,
67
107
  style,
68
108
  ...rest
69
109
  }) {
@@ -79,46 +119,100 @@ function ContentSheet({
79
119
  paddingBottom
80
120
  }, style], [pinToBottom, resolved.container, paddingBottom, style]);
81
121
 
122
+ // Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
123
+ // sheet is bottom-anchored, so a positive `translateY` equal to the window
124
+ // height parks it fully below the bottom edge. Springing it back to `0`
125
+ // produces the same bottom pop-up the Drawer uses. The shared value is
126
+ // initialised off-screen so the very first mount pops in (no flash).
127
+ const {
128
+ height: windowHeight
129
+ } = useWindowDimensions();
130
+ const entrance = useSharedValue(windowHeight);
131
+ useEffect(() => {
132
+ entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG);
133
+ }, [visible, windowHeight, entrance]);
134
+
82
135
  // Switching between the keyboard-aware and plain implementation is keyed off
83
136
  // `avoidKeyboard` (and platform). Because they are distinct component types,
84
137
  // React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
85
- // inside `KeyboardAwareSheet` always runs in a stable hook order.
138
+ // inside `KeyboardAwareSheet` always runs in a stable hook order. Both
139
+ // branches receive the `entrance` shared value so the bottom pop-up plays in
140
+ // every configuration.
141
+ const header = title ? /*#__PURE__*/_jsx(View, {
142
+ style: resolved.headerStyle,
143
+ children: /*#__PURE__*/_jsx(Text, {
144
+ style: resolved.titleStyle,
145
+ children: title
146
+ })
147
+ }) : null;
86
148
  if (avoidKeyboard && !IS_WEB) {
87
- return /*#__PURE__*/_jsx(KeyboardAwareSheet, {
149
+ return /*#__PURE__*/_jsxs(KeyboardAwareSheet, {
88
150
  style: containerStyle,
89
151
  spacing: keyboardSpacing,
152
+ entrance: entrance,
90
153
  ...rest,
91
- children: processedChildren
154
+ children: [header, processedChildren]
92
155
  });
93
156
  }
94
- return /*#__PURE__*/_jsx(View, {
157
+ return /*#__PURE__*/_jsxs(AnimatedSheet, {
95
158
  style: containerStyle,
159
+ entrance: entrance,
96
160
  ...rest,
97
- children: processedChildren
161
+ children: [header, processedChildren]
98
162
  });
99
163
  }
100
164
  /**
101
165
  * Native-only wrapper that lifts the sheet by the live keyboard height. The
102
166
  * translation is computed on the UI thread from `useAnimatedKeyboard`, so the
103
- * sheet tracks the keyboard frame-for-frame without any JS work.
167
+ * sheet tracks the keyboard frame-for-frame without any JS work. The keyboard
168
+ * offset is summed with the entrance `translateY` (the bottom pop-up) inside a
169
+ * single `useAnimatedStyle` so both motions stay on the UI thread and never
170
+ * fight each other.
104
171
  */
105
172
  function KeyboardAwareSheet({
106
173
  style,
107
174
  spacing,
175
+ entrance,
108
176
  children,
109
177
  ...rest
110
178
  }) {
111
179
  const keyboard = useAnimatedKeyboard();
112
- const keyboardStyle = useAnimatedStyle(() => {
180
+ const animatedStyle = useAnimatedStyle(() => {
113
181
  const height = keyboard.height.value;
182
+ const keyboardOffset = height > 0 ? -(height + spacing) : 0;
183
+ return {
184
+ transform: [{
185
+ translateY: entrance.value + keyboardOffset
186
+ }]
187
+ };
188
+ });
189
+ return /*#__PURE__*/_jsx(Animated.View, {
190
+ style: [style, animatedStyle],
191
+ ...rest,
192
+ children: children
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Entrance-only animated sheet (web, or when keyboard avoidance is disabled).
198
+ * Applies the same bottom pop-up `translateY` as `KeyboardAwareSheet`, minus
199
+ * the keyboard offset.
200
+ */
201
+ function AnimatedSheet({
202
+ style,
203
+ entrance,
204
+ children,
205
+ ...rest
206
+ }) {
207
+ const animatedStyle = useAnimatedStyle(() => {
114
208
  return {
115
209
  transform: [{
116
- translateY: height > 0 ? -(height + spacing) : 0
210
+ translateY: entrance.value
117
211
  }]
118
212
  };
119
213
  });
120
214
  return /*#__PURE__*/_jsx(Animated.View, {
121
- style: [style, keyboardStyle],
215
+ style: [style, animatedStyle],
122
216
  ...rest,
123
217
  children: children
124
218
  });
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Pressable, View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import Image from '../Image/Image';
9
+ import IconCapsule from '../IconCapsule/IconCapsule';
10
+
11
+ /**
12
+ * Callback that opens a platform picker and resolves with the selected assets.
13
+ * This is the integration point for libraries like `react-native-image-picker`,
14
+ * `expo-image-picker`, or `react-native-document-picker`.
15
+ */
16
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
17
+ function toNumber(value, fallback) {
18
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
19
+ if (typeof value === 'string') {
20
+ const parsed = parseFloat(value);
21
+ if (Number.isFinite(parsed)) return parsed;
22
+ }
23
+ return fallback;
24
+ }
25
+ function resolveAdditemTokens(modes) {
26
+ const backgroundColor = getVariableByName('compareCardItem/background', modes);
27
+ const borderRadius = toNumber(getVariableByName('compareCardItem/radius', modes), 8);
28
+ const paddingHorizontal = toNumber(getVariableByName('compareCardItem/padding/horizontal', modes), 6);
29
+ const paddingVertical = toNumber(getVariableByName('compareCardItem/padding/vertical', modes), 8);
30
+ // The Figma design uses a fixed 44 × 44 dp cell. There is no
31
+ // `compareCardItem/size` token in the variables JSON, so we keep the size
32
+ // constant to match the design exactly.
33
+ const size = 44;
34
+ return {
35
+ backgroundColor,
36
+ borderRadius,
37
+ paddingHorizontal,
38
+ paddingVertical,
39
+ size
40
+ };
41
+ }
42
+
43
+ /** Figma defaults for the inner `IconCapsule` icons. Overridable via `modes`. */
44
+ const ADDITEM_DEFAULT_MODES = Object.freeze({
45
+ AppearanceBrand: 'Neutral',
46
+ Emphasis: 'Low',
47
+ 'Icon Capsule Size': 'S'
48
+ });
49
+
50
+ /**
51
+ * `Additem` — a small, reusable upload/preview cell used inside form uploaders.
52
+ *
53
+ * In its default `'empty'` state it shows a centered add/upload icon. In
54
+ * `'preview'` state it renders a thumbnail with a removable overlay. The
55
+ * component is intentionally unopinionated about *how* files are picked: pass a
56
+ * `picker` callback that wraps `react-native-image-picker`,
57
+ * `expo-image-picker`, `react-native-document-picker`, or any custom selector,
58
+ * and the cell will invoke it, surface the result via `onAssetsPicked`, and
59
+ * handle cancellation/errors gracefully.
60
+ */
61
+ function Additem({
62
+ state = 'empty',
63
+ imageSource,
64
+ onPress,
65
+ onRemove,
66
+ picker,
67
+ onAssetsPicked,
68
+ modes: propModes = EMPTY_MODES,
69
+ accessibilityLabel,
70
+ isDisabled = false,
71
+ testID
72
+ }) {
73
+ const {
74
+ modes: globalModes
75
+ } = useTokens();
76
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? ADDITEM_DEFAULT_MODES : {
77
+ ...ADDITEM_DEFAULT_MODES,
78
+ ...globalModes,
79
+ ...propModes
80
+ }, [globalModes, propModes]);
81
+ const tokens = useMemo(() => resolveAdditemTokens(modes), [modes]);
82
+ const handlePress = React.useCallback(async () => {
83
+ if (isDisabled) return;
84
+ if (state === 'empty' && picker) {
85
+ try {
86
+ const result = await picker();
87
+ if (result.didCancel) return;
88
+ if (result.error) {
89
+ if (__DEV__) {
90
+ console.warn('[Additem] picker error:', result.error);
91
+ }
92
+ return;
93
+ }
94
+ if (result.assets.length > 0) {
95
+ onAssetsPicked?.(result.assets);
96
+ }
97
+ } catch (err) {
98
+ if (__DEV__) {
99
+ console.warn('[Additem] unexpected picker error:', err);
100
+ }
101
+ }
102
+ return;
103
+ }
104
+ onPress?.();
105
+ }, [isDisabled, onPress, onAssetsPicked, picker, state]);
106
+ const handleRemove = React.useCallback(() => {
107
+ if (!isDisabled) {
108
+ onRemove?.();
109
+ }
110
+ }, [isDisabled, onRemove]);
111
+ const resolvedAccessibilityLabel = accessibilityLabel ?? (state === 'empty' ? 'Add attachment' : 'Attachment preview');
112
+ const containerStyle = useMemo(() => ({
113
+ width: tokens.size,
114
+ height: tokens.size,
115
+ borderRadius: tokens.borderRadius,
116
+ backgroundColor: tokens.backgroundColor,
117
+ paddingHorizontal: tokens.paddingHorizontal,
118
+ paddingVertical: tokens.paddingVertical,
119
+ alignItems: 'center',
120
+ justifyContent: 'center',
121
+ overflow: 'hidden',
122
+ opacity: isDisabled ? 0.5 : 1
123
+ }), [tokens, isDisabled]);
124
+ const isPreview = state === 'preview';
125
+ return /*#__PURE__*/_jsx(Pressable, {
126
+ onPress: handlePress,
127
+ disabled: isDisabled,
128
+ accessibilityRole: "button",
129
+ accessibilityLabel: resolvedAccessibilityLabel,
130
+ accessibilityState: {
131
+ disabled: isDisabled
132
+ },
133
+ testID: testID,
134
+ style: containerStyle,
135
+ children: isPreview ? /*#__PURE__*/_jsxs(_Fragment, {
136
+ children: [/*#__PURE__*/_jsx(Image, {
137
+ imageSource: imageSource,
138
+ width: "100%",
139
+ height: tokens.size - tokens.paddingVertical * 2,
140
+ resizeMode: "cover",
141
+ borderRadius: tokens.borderRadius - 2
142
+ }), onRemove != null && /*#__PURE__*/_jsx(View, {
143
+ style: {
144
+ position: 'absolute',
145
+ top: 2,
146
+ right: 2
147
+ },
148
+ children: /*#__PURE__*/_jsx(IconCapsule, {
149
+ iconName: "ic_close",
150
+ modes: modes,
151
+ onTouchEnd: handleRemove,
152
+ accessibilityRole: "button",
153
+ accessibilityLabel: "Remove attachment"
154
+ })
155
+ })]
156
+ }) : /*#__PURE__*/_jsx(IconCapsule, {
157
+ iconName: "ic_add",
158
+ modes: modes
159
+ })
160
+ });
161
+ }
162
+ export default /*#__PURE__*/React.memo(Additem);
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo, useCallback } from 'react';
4
+ import { View, Text } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
8
+ import { useFormContext } from '../Form/Form';
9
+ import SupportText from '../SupportText/SupportText';
10
+ import Additem from './Additem';
11
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
+ function toNumber(value, fallback) {
13
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
14
+ if (typeof value === 'string') {
15
+ const parsed = parseFloat(value);
16
+ if (Number.isFinite(parsed)) return parsed;
17
+ }
18
+ return fallback;
19
+ }
20
+ function toFontWeight(value, fallback) {
21
+ if (typeof value === 'number') return value.toString();
22
+ if (typeof value === 'string' && value.length > 0) return value;
23
+ return fallback;
24
+ }
25
+ function resolveFormUploadTokens(modes) {
26
+ const gap = toNumber(getVariableByName('formField/input/gap', modes), 8);
27
+ const rowGap = toNumber(getVariableByName('formField/gap', modes), 8);
28
+ const labelColor = getVariableByName('formField/label/color', modes);
29
+ const labelFontFamily = getVariableByName('formField/label/fontFamily', modes);
30
+ const labelFontSize = toNumber(getVariableByName('formField/label/fontSize', modes), 14);
31
+ const labelLineHeight = toNumber(getVariableByName('formField/label/lineHeight', modes), 17);
32
+ const labelFontWeight = toFontWeight(getVariableByName('formField/label/fontWeight', modes), '500');
33
+ return {
34
+ gap,
35
+ rowGap,
36
+ labelColor,
37
+ labelFontFamily,
38
+ labelFontSize,
39
+ labelLineHeight,
40
+ labelFontWeight
41
+ };
42
+ }
43
+ function firstError(error) {
44
+ if (!error) return undefined;
45
+ if (Array.isArray(error)) return error[0];
46
+ return error;
47
+ }
48
+
49
+ /** Figma defaults for the remove `IconCapsule` on filled preview cells. Overridable via `modes`. */
50
+ const FORM_UPLOAD_PREVIEW_ICON_CAPSULE_MODES = {
51
+ AppearanceBrand: 'Neutral',
52
+ Emphasis: 'Medium',
53
+ 'Icon Capsule Size': 'XS'
54
+ };
55
+
56
+ /**
57
+ * `FormUpload` — a form-aware upload row built from reusable `Additem` cells.
58
+ *
59
+ * The component mirrors the Figma "FormUpload" design: a label, a horizontal
60
+ * row of upload/preview cells, and support text. It integrates with the
61
+ * existing `<Form>` context so server-side validation errors automatically
62
+ * show below the row, and it clears those errors when attachments change.
63
+ *
64
+ * Upload behaviour is provided through the `picker` prop so the library stays
65
+ * free of native image-picker dependencies. Pass any picker that returns the
66
+ * `AdditemPickerResult` shape and the empty cell becomes a real upload trigger
67
+ * on both iOS and Android.
68
+ */
69
+ function FormUpload({
70
+ label,
71
+ supportText,
72
+ name,
73
+ attachments = [],
74
+ maxCount,
75
+ onAttachmentsChange,
76
+ picker,
77
+ errorMessage,
78
+ isInvalid = false,
79
+ isDisabled = false,
80
+ children,
81
+ modes: propModes = EMPTY_MODES,
82
+ style,
83
+ rowStyle,
84
+ accessibilityLabel,
85
+ testID
86
+ }) {
87
+ const formCtx = useFormContext();
88
+ const formError = name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined;
89
+ const resolvedIsInvalid = isInvalid || Boolean(formError);
90
+ const resolvedErrorMessage = errorMessage ?? formError;
91
+ const {
92
+ modes: globalModes
93
+ } = useTokens();
94
+ const baseModes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
95
+ ...globalModes,
96
+ ...propModes
97
+ }, [globalModes, propModes]);
98
+ const modes = useMemo(() => ({
99
+ ...baseModes,
100
+ 'FormField States': resolvedIsInvalid ? 'Error' : 'Idle',
101
+ Status: resolvedIsInvalid ? 'Error' : 'Auto'
102
+ }), [baseModes, resolvedIsInvalid]);
103
+
104
+ // Empty cells rely on `Additem`'s built-in Neutral / Low / S defaults for the
105
+ // add icon. Preview cells use the tighter remove-icon treatment from Figma.
106
+ const previewAdditemModes = useMemo(() => ({
107
+ ...FORM_UPLOAD_PREVIEW_ICON_CAPSULE_MODES,
108
+ ...modes
109
+ }), [modes]);
110
+ const tokens = useMemo(() => resolveFormUploadTokens(modes), [modes]);
111
+ const handleAssetsPicked = useCallback(assets => {
112
+ if (isDisabled) return;
113
+ const next = [...attachments, ...assets];
114
+ if (maxCount != null && next.length > maxCount) {
115
+ next.length = maxCount;
116
+ }
117
+ onAttachmentsChange?.(next);
118
+ if (name && formCtx) {
119
+ formCtx.onFieldChange(name);
120
+ }
121
+ }, [attachments, maxCount, onAttachmentsChange, isDisabled, name, formCtx]);
122
+ const handleRemove = useCallback(index => {
123
+ if (isDisabled) return;
124
+ const next = attachments.filter((_, i) => i !== index);
125
+ onAttachmentsChange?.(next);
126
+ if (name && formCtx) {
127
+ formCtx.onFieldChange(name);
128
+ }
129
+ }, [attachments, onAttachmentsChange, isDisabled, name, formCtx]);
130
+ const containerStyle = useMemo(() => ({
131
+ gap: tokens.gap,
132
+ opacity: isDisabled ? 0.5 : 1
133
+ }), [tokens.gap, isDisabled]);
134
+ const rowStyleResolved = useMemo(() => ({
135
+ flexDirection: 'row',
136
+ flexWrap: 'wrap',
137
+ gap: tokens.rowGap,
138
+ alignItems: 'flex-start'
139
+ }), [tokens.rowGap]);
140
+ const labelStyle = useMemo(() => ({
141
+ color: tokens.labelColor,
142
+ fontFamily: tokens.labelFontFamily,
143
+ fontSize: tokens.labelFontSize,
144
+ lineHeight: tokens.labelLineHeight,
145
+ fontWeight: tokens.labelFontWeight
146
+ }), [tokens]);
147
+ const resolvedA11yLabel = accessibilityLabel ?? label ?? 'Attachment upload';
148
+ const canAddMore = maxCount == null || attachments.length < maxCount;
149
+ const renderContent = () => {
150
+ if (children) {
151
+ return cloneChildrenWithModes(children, modes);
152
+ }
153
+ return /*#__PURE__*/_jsxs(_Fragment, {
154
+ children: [attachments.map((attachment, index) => /*#__PURE__*/_jsx(Additem, {
155
+ state: "preview",
156
+ imageSource: attachment.uri,
157
+ onRemove: () => handleRemove(index),
158
+ modes: previewAdditemModes,
159
+ isDisabled: isDisabled,
160
+ accessibilityLabel: attachment.name ?? `Attachment ${index + 1}`,
161
+ testID: `${testID}-item-${index}`
162
+ }, `${attachment.uri}-${index}`)), canAddMore && /*#__PURE__*/_jsx(Additem, {
163
+ state: "empty",
164
+ picker: picker,
165
+ onAssetsPicked: handleAssetsPicked,
166
+ modes: modes,
167
+ isDisabled: isDisabled,
168
+ testID: `${testID}-add`
169
+ })]
170
+ });
171
+ };
172
+ const supportLabel = resolvedIsInvalid && resolvedErrorMessage ? resolvedErrorMessage : supportText;
173
+ const supportStatus = resolvedIsInvalid ? 'Error' : 'Neutral';
174
+ return /*#__PURE__*/_jsxs(View, {
175
+ style: [containerStyle, style],
176
+ accessibilityRole: "none",
177
+ accessibilityLabel: resolvedA11yLabel,
178
+ testID: testID,
179
+ children: [label != null && label !== '' && /*#__PURE__*/_jsx(Text, {
180
+ style: labelStyle,
181
+ children: label
182
+ }), /*#__PURE__*/_jsx(View, {
183
+ style: [rowStyleResolved, rowStyle],
184
+ children: renderContent()
185
+ }), supportLabel != null && supportLabel !== '' && /*#__PURE__*/_jsx(SupportText, {
186
+ label: supportLabel,
187
+ status: supportStatus,
188
+ modes: modes
189
+ })]
190
+ });
191
+ }
192
+ export default /*#__PURE__*/React.memo(FormUpload);