jfs-components 0.1.25 → 0.1.28
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/CHANGELOG.md +40 -0
- package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
- package/lib/commonjs/components/Carousel/Carousel.js +112 -19
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +39 -5
- package/lib/commonjs/components/FormUpload/Additem.js +168 -0
- package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
- package/lib/commonjs/components/Grid/Grid.js +206 -0
- package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
- package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/commonjs/components/index.js +42 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
- package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
- package/lib/commonjs/icons/components/IconImage.js +19 -0
- package/lib/commonjs/icons/components/IconMic.js +19 -0
- package/lib/commonjs/icons/components/IconMicoff.js +19 -0
- package/lib/commonjs/icons/components/IconPause.js +19 -0
- package/lib/commonjs/icons/components/IconPlay.js +19 -0
- package/lib/commonjs/icons/components/index.js +66 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
- package/lib/module/components/Carousel/Carousel.js +111 -19
- package/lib/module/components/ContentSheet/ContentSheet.js +41 -7
- package/lib/module/components/FormUpload/Additem.js +162 -0
- package/lib/module/components/FormUpload/FormUpload.js +192 -0
- package/lib/module/components/Grid/Grid.js +201 -0
- package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
- package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
- package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/module/components/index.js +7 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +21 -10
- package/lib/module/icons/components/IconBrokenimage.js +12 -0
- package/lib/module/icons/components/IconImage.js +12 -0
- package/lib/module/icons/components/IconMic.js +12 -0
- package/lib/module/icons/components/IconMicoff.js +12 -0
- package/lib/module/icons/components/IconPause.js +12 -0
- package/lib/module/icons/components/IconPlay.js +12 -0
- package/lib/module/icons/components/index.js +6 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
- package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
- package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
- package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
- package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
- package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
- package/lib/typescript/src/components/index.d.ts +34 -28
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
- package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +6 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
- package/src/components/Carousel/Carousel.tsx +133 -23
- package/src/components/ContentSheet/ContentSheet.tsx +50 -0
- package/src/components/FormUpload/Additem.tsx +262 -0
- package/src/components/FormUpload/FormUpload.tsx +313 -0
- package/src/components/Grid/Grid.tsx +368 -0
- package/src/components/NumberPagination/NumberPagination.tsx +170 -0
- package/src/components/ProfileCard/ProfileCard.tsx +268 -0
- package/src/components/UpiHandle/UpiHandle.tsx +35 -24
- package/src/components/index.ts +329 -182
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +21 -10
- package/src/icons/components/IconBrokenimage.tsx +11 -0
- package/src/icons/components/IconImage.tsx +11 -0
- package/src/icons/components/IconMic.tsx +11 -0
- package/src/icons/components/IconMicoff.tsx +11 -0
- package/src/icons/components/IconPause.tsx +11 -0
- package/src/icons/components/IconPlay.tsx +11 -0
- package/src/icons/components/index.ts +6 -0
- package/src/icons/registry.ts +25 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { Pressable } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import Icon from '../../icons/Icon';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
function resolveAutoplayControlTokens(modes, disabled) {
|
|
9
|
+
const width = getVariableByName('autoplayControl/width', modes) ?? 36;
|
|
10
|
+
const height = getVariableByName('autoplayControl/height', modes) ?? 36;
|
|
11
|
+
const radiusRaw = getVariableByName('autoplayControl/radius', modes) ?? 9999;
|
|
12
|
+
const backgroundColor = getVariableByName('autoplayControl/background/color', modes) ?? '#ffffff';
|
|
13
|
+
const iconColor = getVariableByName('autoplayControl/icon/color', modes) ?? '#000000';
|
|
14
|
+
const borderRadius = radiusRaw === 9999 ? width / 2 : radiusRaw;
|
|
15
|
+
return {
|
|
16
|
+
containerStyle: {
|
|
17
|
+
width,
|
|
18
|
+
height,
|
|
19
|
+
borderRadius,
|
|
20
|
+
backgroundColor,
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
opacity: disabled ? 0.5 : 1
|
|
24
|
+
},
|
|
25
|
+
iconColor
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AutoplayControl is a button component that toggles between play and pause states.
|
|
31
|
+
* All styling values are resolved from Figma design tokens.
|
|
32
|
+
*/
|
|
33
|
+
export default function AutoplayControl({
|
|
34
|
+
state = 'pause',
|
|
35
|
+
modes,
|
|
36
|
+
onPress,
|
|
37
|
+
disabled = false,
|
|
38
|
+
style
|
|
39
|
+
}) {
|
|
40
|
+
const tokens = useMemo(() => resolveAutoplayControlTokens(modes ?? {}, disabled), [modes, disabled]);
|
|
41
|
+
const iconName = state === 'play' ? 'ic_play' : 'ic_pause';
|
|
42
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
43
|
+
onPress: disabled ? undefined : onPress,
|
|
44
|
+
disabled: disabled,
|
|
45
|
+
style: ({
|
|
46
|
+
pressed
|
|
47
|
+
}) => [tokens.containerStyle, pressed && {
|
|
48
|
+
opacity: 0.7
|
|
49
|
+
}, style],
|
|
50
|
+
accessibilityRole: "button",
|
|
51
|
+
accessibilityState: {
|
|
52
|
+
disabled
|
|
53
|
+
},
|
|
54
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
55
|
+
name: iconName,
|
|
56
|
+
size: 24,
|
|
57
|
+
color: tokens.iconColor
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -4,6 +4,8 @@ import React, { createContext, useContext, useRef, useState, useEffect, useCallb
|
|
|
4
4
|
import { View, ScrollView, Animated, Pressable } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import AutoplayControl from '../AutoplayControl/AutoplayControl';
|
|
8
|
+
import NumberPagination from '../NumberPagination/NumberPagination';
|
|
7
9
|
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// Context
|
|
@@ -37,9 +39,11 @@ export function Carousel({
|
|
|
37
39
|
itemWidth: itemWidthProp,
|
|
38
40
|
paddingHorizontal: paddingHorizontalProp,
|
|
39
41
|
paddingVertical: paddingVerticalProp,
|
|
42
|
+
type = 'Default',
|
|
40
43
|
onIndexChange,
|
|
41
44
|
style
|
|
42
45
|
}) {
|
|
46
|
+
const isNumbered = type === 'Numbered';
|
|
43
47
|
// ---- Token resolution ----
|
|
44
48
|
const tokenGap = parseFloat(getVariableByName('carousel/gap', modes));
|
|
45
49
|
const gap = gapProp ?? tokenGap;
|
|
@@ -47,27 +51,43 @@ export function Carousel({
|
|
|
47
51
|
const containerPaddingV = parseFloat(getVariableByName('carousel/padding/vertical', modes));
|
|
48
52
|
// Outer container max height per Figma (`carousel/maxHeight`).
|
|
49
53
|
const maxHeight = parseFloat(getVariableByName('carousel/maxHeight', modes));
|
|
54
|
+
// Gap between AutoplayControl and NumberPagination in the Numbered overlay.
|
|
55
|
+
const controlGap = parseFloat(getVariableByName('carouselControl/gap', modes));
|
|
56
|
+
|
|
50
57
|
// Spacing between the cards row and the pagination dots uses `carousel/gap`.
|
|
51
58
|
const paginationOffset = gap;
|
|
52
59
|
|
|
53
60
|
// ---- Refs & state ----
|
|
54
61
|
const scrollRef = useRef(null);
|
|
55
62
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
63
|
+
const activeIndexRef = useRef(0);
|
|
56
64
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
57
65
|
const autoPlayTimer = useRef(null);
|
|
58
66
|
const userInteracting = useRef(false);
|
|
67
|
+
// Guard: suppress handleScroll index updates during programmatic scrollTo
|
|
68
|
+
// animations so intermediate offsets don't flicker activeIndex back.
|
|
69
|
+
const programmaticScroll = useRef(false);
|
|
70
|
+
const programmaticTimer = useRef(null);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
activeIndexRef.current = activeIndex;
|
|
73
|
+
}, [activeIndex]);
|
|
74
|
+
|
|
75
|
+
// Interactive play/pause state for the Numbered variant.
|
|
76
|
+
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
|
59
77
|
|
|
60
78
|
// Flatten children so we can count items
|
|
61
79
|
const items = useMemo(() => React.Children.toArray(children).filter(React.isValidElement), [children]);
|
|
62
80
|
const totalItems = items.length;
|
|
63
81
|
|
|
64
|
-
// Effective item width: provided prop, or full container width minus peek offsets
|
|
82
|
+
// Effective item width: provided prop, or full container width minus peek offsets.
|
|
83
|
+
// For the Numbered variant the items are full-bleed (no peek padding).
|
|
65
84
|
const effectiveItemWidth = useMemo(() => {
|
|
66
85
|
if (itemWidthProp != null) return itemWidthProp;
|
|
67
86
|
if (containerWidth === 0) return 0;
|
|
87
|
+
if (isNumbered) return containerWidth;
|
|
68
88
|
// Full-width minus peekOffset on each side
|
|
69
89
|
return containerWidth - containerPaddingH * 4;
|
|
70
|
-
}, [itemWidthProp, containerWidth, containerPaddingH]);
|
|
90
|
+
}, [itemWidthProp, containerWidth, containerPaddingH, isNumbered]);
|
|
71
91
|
|
|
72
92
|
// Snap interval = item width + gap
|
|
73
93
|
const snapInterval = effectiveItemWidth + gap;
|
|
@@ -81,6 +101,17 @@ export function Carousel({
|
|
|
81
101
|
} else {
|
|
82
102
|
targetIndex = Math.max(0, Math.min(index, totalItems - 1));
|
|
83
103
|
}
|
|
104
|
+
// Lock out handleScroll during the animated scroll so intermediate
|
|
105
|
+
// offsets don't flicker activeIndex back to the old value.
|
|
106
|
+
if (animated) {
|
|
107
|
+
programmaticScroll.current = true;
|
|
108
|
+
if (programmaticTimer.current != null) {
|
|
109
|
+
clearTimeout(programmaticTimer.current);
|
|
110
|
+
}
|
|
111
|
+
programmaticTimer.current = setTimeout(() => {
|
|
112
|
+
programmaticScroll.current = false;
|
|
113
|
+
}, 500);
|
|
114
|
+
}
|
|
84
115
|
scrollRef.current?.scrollTo({
|
|
85
116
|
x: targetIndex * snapInterval,
|
|
86
117
|
animated
|
|
@@ -95,6 +126,9 @@ export function Carousel({
|
|
|
95
126
|
// ---- Scroll event handler ----
|
|
96
127
|
const handleScroll = useCallback(event => {
|
|
97
128
|
if (snapInterval === 0) return;
|
|
129
|
+
// Skip index recalculation during programmatic scrollTo animations
|
|
130
|
+
// to prevent the active indicator from flickering back to the old page.
|
|
131
|
+
if (programmaticScroll.current) return;
|
|
98
132
|
const offsetX = event.nativeEvent.contentOffset.x;
|
|
99
133
|
const newIndex = Math.round(offsetX / snapInterval);
|
|
100
134
|
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
|
|
@@ -112,22 +146,19 @@ export function Carousel({
|
|
|
112
146
|
}
|
|
113
147
|
}, []);
|
|
114
148
|
const startAutoPlay = useCallback(() => {
|
|
115
|
-
if (!autoPlay || totalItems <= 1) return;
|
|
149
|
+
if (!autoPlay && !isPlaying || totalItems <= 1) return;
|
|
150
|
+
if (!isPlaying) return;
|
|
116
151
|
clearAutoPlay();
|
|
117
152
|
autoPlayTimer.current = setInterval(() => {
|
|
118
153
|
if (!userInteracting.current) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
onIndexChange?.(next);
|
|
126
|
-
return next;
|
|
127
|
-
});
|
|
154
|
+
const prev = activeIndexRef.current;
|
|
155
|
+
const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
|
|
156
|
+
// Route through scrollToIndex so the programmaticScroll guard
|
|
157
|
+
// suppresses intermediate handleScroll offsets during the animation.
|
|
158
|
+
scrollToIndex(next, true);
|
|
128
159
|
}
|
|
129
160
|
}, autoPlayInterval);
|
|
130
|
-
}, [autoPlay, totalItems, loop,
|
|
161
|
+
}, [autoPlay, isPlaying, totalItems, loop, autoPlayInterval, clearAutoPlay, scrollToIndex]);
|
|
131
162
|
useEffect(() => {
|
|
132
163
|
startAutoPlay();
|
|
133
164
|
return clearAutoPlay;
|
|
@@ -138,8 +169,23 @@ export function Carousel({
|
|
|
138
169
|
const handleScrollEndDrag = useCallback(() => {
|
|
139
170
|
userInteracting.current = false;
|
|
140
171
|
// Reset autoplay timer after interaction
|
|
141
|
-
if (autoPlay) startAutoPlay();
|
|
142
|
-
}, [autoPlay, startAutoPlay]);
|
|
172
|
+
if (autoPlay || isPlaying) startAutoPlay();
|
|
173
|
+
}, [autoPlay, isPlaying, startAutoPlay]);
|
|
174
|
+
const finishProgrammaticScroll = useCallback(event => {
|
|
175
|
+
programmaticScroll.current = false;
|
|
176
|
+
if (programmaticTimer.current != null) {
|
|
177
|
+
clearTimeout(programmaticTimer.current);
|
|
178
|
+
programmaticTimer.current = null;
|
|
179
|
+
}
|
|
180
|
+
if (snapInterval === 0) return;
|
|
181
|
+
const offsetX = event.nativeEvent.contentOffset.x;
|
|
182
|
+
const newIndex = Math.round(offsetX / snapInterval);
|
|
183
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
|
|
184
|
+
if (clampedIndex !== activeIndexRef.current) {
|
|
185
|
+
setActiveIndex(clampedIndex);
|
|
186
|
+
onIndexChange?.(clampedIndex);
|
|
187
|
+
}
|
|
188
|
+
}, [snapInterval, totalItems, onIndexChange]);
|
|
143
189
|
|
|
144
190
|
// ---- Layout ----
|
|
145
191
|
const handleLayout = useCallback(e => {
|
|
@@ -156,14 +202,37 @@ export function Carousel({
|
|
|
156
202
|
goPrev
|
|
157
203
|
}), [modes, activeIndex, totalItems, goTo, goNext, goPrev]);
|
|
158
204
|
|
|
205
|
+
// ---- Toggle handler for Numbered variant ----
|
|
206
|
+
const handleToggleAutoplay = useCallback(() => {
|
|
207
|
+
setIsPlaying(prev => {
|
|
208
|
+
if (prev) {
|
|
209
|
+
// Pausing
|
|
210
|
+
clearAutoPlay();
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
// Resuming — autoplay will restart via the effect
|
|
214
|
+
return true;
|
|
215
|
+
});
|
|
216
|
+
}, [clearAutoPlay]);
|
|
217
|
+
|
|
218
|
+
// Restart autoplay whenever isPlaying flips to true
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (isPlaying) startAutoPlay();else clearAutoPlay();
|
|
221
|
+
}, [isPlaying, startAutoPlay, clearAutoPlay]);
|
|
222
|
+
|
|
159
223
|
// ---- Render ----
|
|
160
224
|
// Explicit padding props win verbatim; otherwise fall back to the
|
|
161
225
|
// token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
|
|
162
|
-
|
|
163
|
-
const
|
|
226
|
+
// Numbered variant is full-bleed: zero padding.
|
|
227
|
+
const effectivePaddingH = isNumbered ? 0 : paddingHorizontalProp ?? containerPaddingH;
|
|
228
|
+
const effectivePaddingV = isNumbered ? 0 : paddingVerticalProp ?? containerPaddingV;
|
|
164
229
|
const outerStyle = {
|
|
165
230
|
paddingVertical: effectivePaddingV,
|
|
166
|
-
maxHeight
|
|
231
|
+
maxHeight: isNumbered ? undefined : maxHeight,
|
|
232
|
+
// Numbered needs relative positioning for the floating controls overlay.
|
|
233
|
+
...(isNumbered ? {
|
|
234
|
+
overflow: 'hidden'
|
|
235
|
+
} : {})
|
|
167
236
|
};
|
|
168
237
|
const contentContainerStyle = {
|
|
169
238
|
paddingHorizontal: effectivePaddingH,
|
|
@@ -189,6 +258,7 @@ export function Carousel({
|
|
|
189
258
|
scrollEventThrottle: 16,
|
|
190
259
|
onScrollBeginDrag: handleScrollBeginDrag,
|
|
191
260
|
onScrollEndDrag: handleScrollEndDrag,
|
|
261
|
+
onMomentumScrollEnd: finishProgrammaticScroll,
|
|
192
262
|
children: items.map((child, index) => {
|
|
193
263
|
// Strict slot box: width must be honored; never grow or shrink with
|
|
194
264
|
// content, and clip anything that misbehaves (e.g. a child whose
|
|
@@ -225,11 +295,33 @@ export function Carousel({
|
|
|
225
295
|
children: childWithModes
|
|
226
296
|
}, index);
|
|
227
297
|
})
|
|
228
|
-
}), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
|
|
298
|
+
}), !isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
|
|
229
299
|
modes: modes,
|
|
230
300
|
style: {
|
|
231
301
|
marginTop: paginationOffset
|
|
232
302
|
}
|
|
303
|
+
}), isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsxs(View, {
|
|
304
|
+
style: {
|
|
305
|
+
position: 'absolute',
|
|
306
|
+
bottom: 12,
|
|
307
|
+
left: 0,
|
|
308
|
+
right: 0,
|
|
309
|
+
flexDirection: 'row',
|
|
310
|
+
justifyContent: 'center',
|
|
311
|
+
alignItems: 'center',
|
|
312
|
+
gap: controlGap
|
|
313
|
+
},
|
|
314
|
+
pointerEvents: "box-none",
|
|
315
|
+
children: [/*#__PURE__*/_jsx(AutoplayControl, {
|
|
316
|
+
state: isPlaying ? 'pause' : 'play',
|
|
317
|
+
onPress: handleToggleAutoplay,
|
|
318
|
+
modes: modes
|
|
319
|
+
}), /*#__PURE__*/_jsx(NumberPagination, {
|
|
320
|
+
activePage: activeIndex + 1,
|
|
321
|
+
totalPages: totalItems,
|
|
322
|
+
onPageChange: page => goTo(page - 1),
|
|
323
|
+
modes: modes
|
|
324
|
+
})]
|
|
233
325
|
})]
|
|
234
326
|
})
|
|
235
327
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useMemo } from 'react';
|
|
4
|
-
import { Platform, View } from 'react-native';
|
|
4
|
+
import { Platform, Text, View } from 'react-native';
|
|
5
5
|
import Animated, { useAnimatedKeyboard, useAnimatedStyle } 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
11
|
function resolveContentSheetStyle(modes) {
|
|
12
12
|
const backgroundColor = getVariableByName('contentSheet/bg', modes);
|
|
@@ -16,6 +16,18 @@ function resolveContentSheetStyle(modes) {
|
|
|
16
16
|
const paddingBottom = getVariableByName('contentSheet/padding/bottom', modes);
|
|
17
17
|
const radiusTop = getVariableByName('contentSheet/padding/radius/top', modes);
|
|
18
18
|
const radiusBottom = getVariableByName('contentSheet/padding/radius/bottom', modes);
|
|
19
|
+
|
|
20
|
+
// Title tokens mirror the Drawer title so the two bottom-sheet surfaces stay
|
|
21
|
+
// visually aligned.
|
|
22
|
+
const titleColor = getVariableByName('drawer/title/color', modes);
|
|
23
|
+
const titleSize = getVariableByName('drawer/title/fontSize', modes);
|
|
24
|
+
const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes);
|
|
25
|
+
const titleFontFamily = getVariableByName('drawer/title/fontFamily', modes);
|
|
26
|
+
const titleFontWeightRaw = getVariableByName('drawer/title/fontWeight', modes);
|
|
27
|
+
const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
|
|
28
|
+
const titleWrapPaddingTop = getVariableByName('drawer/titleWrap/padding/top', modes);
|
|
29
|
+
const titleWrapPaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes);
|
|
30
|
+
const titleWrapPaddingHorizontal = getVariableByName('drawer/titleWrap/padding/horizontal', modes);
|
|
19
31
|
return {
|
|
20
32
|
container: {
|
|
21
33
|
backgroundColor,
|
|
@@ -31,7 +43,21 @@ function resolveContentSheetStyle(modes) {
|
|
|
31
43
|
alignItems: 'stretch',
|
|
32
44
|
overflow: 'hidden'
|
|
33
45
|
},
|
|
34
|
-
paddingBottom
|
|
46
|
+
paddingBottom,
|
|
47
|
+
headerStyle: {
|
|
48
|
+
paddingTop: titleWrapPaddingTop,
|
|
49
|
+
paddingBottom: titleWrapPaddingBottom,
|
|
50
|
+
paddingHorizontal: titleWrapPaddingHorizontal,
|
|
51
|
+
alignItems: 'center'
|
|
52
|
+
},
|
|
53
|
+
titleStyle: {
|
|
54
|
+
color: titleColor,
|
|
55
|
+
fontSize: titleSize,
|
|
56
|
+
lineHeight: titleLineHeight,
|
|
57
|
+
fontFamily: titleFontFamily,
|
|
58
|
+
fontWeight: titleFontWeight,
|
|
59
|
+
textAlign: 'center'
|
|
60
|
+
}
|
|
35
61
|
};
|
|
36
62
|
}
|
|
37
63
|
const pinnedStyle = {
|
|
@@ -60,6 +86,7 @@ const pinnedStyle = {
|
|
|
60
86
|
function ContentSheet({
|
|
61
87
|
children,
|
|
62
88
|
modes = EMPTY_MODES,
|
|
89
|
+
title,
|
|
63
90
|
avoidKeyboard = true,
|
|
64
91
|
keyboardSpacing = 0,
|
|
65
92
|
safeAreaBottom = true,
|
|
@@ -83,18 +110,25 @@ function ContentSheet({
|
|
|
83
110
|
// `avoidKeyboard` (and platform). Because they are distinct component types,
|
|
84
111
|
// React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
|
|
85
112
|
// inside `KeyboardAwareSheet` always runs in a stable hook order.
|
|
113
|
+
const header = title ? /*#__PURE__*/_jsx(View, {
|
|
114
|
+
style: resolved.headerStyle,
|
|
115
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
116
|
+
style: resolved.titleStyle,
|
|
117
|
+
children: title
|
|
118
|
+
})
|
|
119
|
+
}) : null;
|
|
86
120
|
if (avoidKeyboard && !IS_WEB) {
|
|
87
|
-
return /*#__PURE__*/
|
|
121
|
+
return /*#__PURE__*/_jsxs(KeyboardAwareSheet, {
|
|
88
122
|
style: containerStyle,
|
|
89
123
|
spacing: keyboardSpacing,
|
|
90
124
|
...rest,
|
|
91
|
-
children: processedChildren
|
|
125
|
+
children: [header, processedChildren]
|
|
92
126
|
});
|
|
93
127
|
}
|
|
94
|
-
return /*#__PURE__*/
|
|
128
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
95
129
|
style: containerStyle,
|
|
96
130
|
...rest,
|
|
97
|
-
children: processedChildren
|
|
131
|
+
children: [header, processedChildren]
|
|
98
132
|
});
|
|
99
133
|
}
|
|
100
134
|
/**
|
|
@@ -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);
|