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
|
@@ -20,6 +20,7 @@ export declare const FIGMA_MODES: {
|
|
|
20
20
|
readonly AppearanceBrand: readonly ["Primary", "Secondary", "Neutral", "Tertiary"];
|
|
21
21
|
readonly AppearanceSystem: readonly ["positive", "warning", "negative"];
|
|
22
22
|
readonly "Attached / Output": readonly ["Default"];
|
|
23
|
+
readonly "Autoplay Control": readonly ["Default"];
|
|
23
24
|
readonly "Avatar / Output": readonly ["Default"];
|
|
24
25
|
readonly "Avatar Group / Output": readonly ["Default"];
|
|
25
26
|
readonly "Avatar Size": readonly ["L", "M", "S", "XS"];
|
|
@@ -44,6 +45,7 @@ export declare const FIGMA_MODES: {
|
|
|
44
45
|
readonly "Calendar Glyph / Output": readonly ["Default"];
|
|
45
46
|
readonly "Calendar Glyph State": readonly ["Idle", "notSaved", "saved"];
|
|
46
47
|
readonly "Card / Output": readonly ["Default"];
|
|
48
|
+
readonly "Card Apperance": readonly ["Default", "Plain"];
|
|
47
49
|
readonly "Card Feedback / Output": readonly ["Default"];
|
|
48
50
|
readonly "Card.Media": readonly ["Default"];
|
|
49
51
|
readonly "Card/CTA / Output": readonly ["Default"];
|
|
@@ -52,6 +54,7 @@ export declare const FIGMA_MODES: {
|
|
|
52
54
|
readonly "CardAdvisory / Output": readonly ["Default"];
|
|
53
55
|
readonly cardsShared: readonly ["Default"];
|
|
54
56
|
readonly "Carousel / Output": readonly ["Default"];
|
|
57
|
+
readonly "Carousel Control Gap": readonly ["Default"];
|
|
55
58
|
readonly "CarouselCardAccounts / Output": readonly ["Default"];
|
|
56
59
|
readonly "ccCard / Output": readonly ["Default"];
|
|
57
60
|
readonly "checkbox / Output": readonly ["Default"];
|
|
@@ -78,7 +81,7 @@ export declare const FIGMA_MODES: {
|
|
|
78
81
|
readonly "contentInset/Right": readonly ["Default", "S", "M", "L"];
|
|
79
82
|
readonly "ContentSheet / Output": readonly ["Default"];
|
|
80
83
|
readonly Context: readonly ["Default", "Nudge&Alert", "CTACard", "ListItem"];
|
|
81
|
-
readonly "context 10": readonly ["Default", "
|
|
84
|
+
readonly "context 10": readonly ["Default", "jioPlus"];
|
|
82
85
|
readonly "context 8": readonly ["Default", "Section"];
|
|
83
86
|
readonly "context 9": readonly ["Default", "Stack"];
|
|
84
87
|
readonly Context2: readonly ["Default", "AppBar"];
|
|
@@ -87,6 +90,7 @@ export declare const FIGMA_MODES: {
|
|
|
87
90
|
readonly context5: readonly ["Default", "Fullscreen Modal"];
|
|
88
91
|
readonly context7: readonly ["Default", "Card", "Page Hero"];
|
|
89
92
|
readonly "Contrast Context": readonly ["on dark", "on light"];
|
|
93
|
+
readonly "Control Toggle ": readonly ["Default"];
|
|
90
94
|
readonly "Conversation Feed / Output": readonly ["Default"];
|
|
91
95
|
readonly "Counter Badge / Output": readonly ["Default"];
|
|
92
96
|
readonly "Coverage Bar Comparison / Output": readonly ["Default"];
|
|
@@ -118,7 +122,7 @@ export declare const FIGMA_MODES: {
|
|
|
118
122
|
readonly Gap: readonly ["S", "M", "L", "None"];
|
|
119
123
|
readonly "Gauge / Output": readonly ["Default"];
|
|
120
124
|
readonly "Grid / Output": readonly ["Default"];
|
|
121
|
-
readonly "Handle Boolean": readonly ["
|
|
125
|
+
readonly "Handle Boolean": readonly ["True", "False"];
|
|
122
126
|
readonly "Heading / Output": readonly ["Default"];
|
|
123
127
|
readonly "Heading text": readonly ["Default", "Slot"];
|
|
124
128
|
readonly "HeroSection / Output": readonly ["Default"];
|
|
@@ -139,7 +143,7 @@ export declare const FIGMA_MODES: {
|
|
|
139
143
|
readonly "lazyList / Output": readonly ["Default"];
|
|
140
144
|
readonly "LinearMeter / Output": readonly ["Default"];
|
|
141
145
|
readonly "LinearProgress / Output": readonly ["Default"];
|
|
142
|
-
readonly "LinearProgress Size": readonly ["M", "L"];
|
|
146
|
+
readonly "LinearProgress Size": readonly ["S", "M", "L"];
|
|
143
147
|
readonly "Link / Output": readonly ["Default"];
|
|
144
148
|
readonly "Link Apperances": readonly ["Neutral", "Primary", "Secondary", "Tertiary"];
|
|
145
149
|
readonly "List Item / Output": readonly ["Default"];
|
|
@@ -160,10 +164,10 @@ export declare const FIGMA_MODES: {
|
|
|
160
164
|
readonly NavArrow: readonly ["Default"];
|
|
161
165
|
readonly "NavArrow / Output": readonly ["Default"];
|
|
162
166
|
readonly "NavArrow Direction": readonly ["Left&Right", "Top&Bottom"];
|
|
163
|
-
readonly "New Collection": readonly ["Default"];
|
|
164
167
|
readonly "NoteInput / Output": readonly ["Default"];
|
|
165
168
|
readonly "Nudge / Output": readonly ["Default"];
|
|
166
169
|
readonly "Nudge padding": readonly ["Default", "None"];
|
|
170
|
+
readonly "number pagination": readonly ["Default"];
|
|
167
171
|
readonly "Numpad / Output": readonly ["Default"];
|
|
168
172
|
readonly "OTP / Output": readonly ["Mode 1"];
|
|
169
173
|
readonly "Overlay / Output": readonly ["Mode 1"];
|
|
@@ -182,6 +186,7 @@ export declare const FIGMA_MODES: {
|
|
|
182
186
|
readonly "Product Merchandising card": readonly ["Default"];
|
|
183
187
|
readonly "ProductLabel / Output": readonly ["Default"];
|
|
184
188
|
readonly "ProductOverview / Output": readonly ["Default"];
|
|
189
|
+
readonly "Profile Card / Output": readonly ["Default"];
|
|
185
190
|
readonly "Profile Card Appearance": readonly ["Default", "Premium"];
|
|
186
191
|
readonly "ProgressBadge / Output": readonly ["Default"];
|
|
187
192
|
readonly ProjectionMarker: readonly ["Default"];
|
|
@@ -253,6 +258,7 @@ export declare const FIGMA_MODES: {
|
|
|
253
258
|
readonly "TransactionBubble / Output": readonly ["Default"];
|
|
254
259
|
readonly "TransationDetails / Output": readonly ["Default"];
|
|
255
260
|
readonly "UPI Handle / Output": readonly ["Default"];
|
|
261
|
+
readonly "UPI Handle Image": readonly ["True", "False"];
|
|
256
262
|
readonly "ValueBar / Output": readonly ["Default"];
|
|
257
263
|
readonly "Video / Output": readonly ["L", "M", "S"];
|
|
258
264
|
readonly "VStack / Output": readonly ["Default"];
|
|
@@ -170,6 +170,7 @@ export * from './IconBrightnessdisplay';
|
|
|
170
170
|
export * from './IconBrightnessfull';
|
|
171
171
|
export * from './IconBrightnesshalf';
|
|
172
172
|
export * from './IconBroadcast';
|
|
173
|
+
export * from './IconBrokenimage';
|
|
173
174
|
export * from './IconBroom';
|
|
174
175
|
export * from './IconBrush';
|
|
175
176
|
export * from './IconBuffer';
|
|
@@ -626,6 +627,7 @@ export * from './IconIce';
|
|
|
626
627
|
export * from './IconIceskating';
|
|
627
628
|
export * from './IconId';
|
|
628
629
|
export * from './IconIdcheck';
|
|
630
|
+
export * from './IconImage';
|
|
629
631
|
export * from './IconImagemask';
|
|
630
632
|
export * from './IconImport';
|
|
631
633
|
export * from './IconIncognito';
|
|
@@ -724,7 +726,9 @@ export * from './IconMerge';
|
|
|
724
726
|
export * from './IconMergeoff';
|
|
725
727
|
export * from './IconMesh';
|
|
726
728
|
export * from './IconMessagesend';
|
|
729
|
+
export * from './IconMic';
|
|
727
730
|
export * from './IconMicmuteforced';
|
|
731
|
+
export * from './IconMicoff';
|
|
728
732
|
export * from './IconMicroscope';
|
|
729
733
|
export * from './IconMilk';
|
|
730
734
|
export * from './IconMilklow';
|
|
@@ -846,6 +850,7 @@ export * from './IconPasses';
|
|
|
846
850
|
export * from './IconPaste';
|
|
847
851
|
export * from './IconPastedocument';
|
|
848
852
|
export * from './IconPatient';
|
|
853
|
+
export * from './IconPause';
|
|
849
854
|
export * from './IconPausecircle';
|
|
850
855
|
export * from './IconPaybill';
|
|
851
856
|
export * from './IconPayemiinstall';
|
|
@@ -887,6 +892,7 @@ export * from './IconPlank';
|
|
|
887
892
|
export * from './IconPlans';
|
|
888
893
|
export * from './IconPlantgrowth';
|
|
889
894
|
export * from './IconPlanting';
|
|
895
|
+
export * from './IconPlay';
|
|
890
896
|
export * from './IconPlaycircle';
|
|
891
897
|
export * from './IconPlaypause';
|
|
892
898
|
export * from './IconPlaybox';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Auto-generated from SVG files in src/icons/
|
|
5
5
|
* DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
|
|
6
6
|
*
|
|
7
|
-
* Generated: 2026-07-
|
|
7
|
+
* Generated: 2026-07-07T17:35:34.036Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
type StyleProp,
|
|
5
|
+
type ViewStyle,
|
|
6
|
+
} from 'react-native'
|
|
7
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
8
|
+
import Icon from '../../icons/Icon'
|
|
9
|
+
import type { Modes } from '../../design-tokens'
|
|
10
|
+
|
|
11
|
+
export type AutoplayControlProps = {
|
|
12
|
+
state?: 'pause' | 'play';
|
|
13
|
+
modes?: Modes;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AutoplayControlTokens {
|
|
20
|
+
containerStyle: ViewStyle;
|
|
21
|
+
iconColor: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveAutoplayControlTokens(modes: Modes, disabled: boolean): AutoplayControlTokens {
|
|
25
|
+
const width = (getVariableByName('autoplayControl/width', modes) ?? 36) as number
|
|
26
|
+
const height = (getVariableByName('autoplayControl/height', modes) ?? 36) as number
|
|
27
|
+
const radiusRaw = (getVariableByName('autoplayControl/radius', modes) ?? 9999) as number
|
|
28
|
+
const backgroundColor = (getVariableByName('autoplayControl/background/color', modes) ?? '#ffffff') as string
|
|
29
|
+
const iconColor = (getVariableByName('autoplayControl/icon/color', modes) ?? '#000000') as string
|
|
30
|
+
|
|
31
|
+
const borderRadius = radiusRaw === 9999 ? width / 2 : radiusRaw
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
containerStyle: {
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
borderRadius,
|
|
38
|
+
backgroundColor,
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
opacity: disabled ? 0.5 : 1,
|
|
42
|
+
},
|
|
43
|
+
iconColor,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AutoplayControl is a button component that toggles between play and pause states.
|
|
49
|
+
* All styling values are resolved from Figma design tokens.
|
|
50
|
+
*/
|
|
51
|
+
export default function AutoplayControl({
|
|
52
|
+
state = 'pause',
|
|
53
|
+
modes,
|
|
54
|
+
onPress,
|
|
55
|
+
disabled = false,
|
|
56
|
+
style,
|
|
57
|
+
}: AutoplayControlProps) {
|
|
58
|
+
const tokens = useMemo(() => resolveAutoplayControlTokens(modes ?? {}, disabled), [modes, disabled])
|
|
59
|
+
|
|
60
|
+
const iconName = state === 'play' ? 'ic_play' : 'ic_pause'
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Pressable
|
|
64
|
+
onPress={disabled ? undefined : onPress}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
style={({ pressed }) => [
|
|
67
|
+
tokens.containerStyle,
|
|
68
|
+
pressed && { opacity: 0.7 },
|
|
69
|
+
style,
|
|
70
|
+
]}
|
|
71
|
+
accessibilityRole="button"
|
|
72
|
+
accessibilityState={{ disabled }}
|
|
73
|
+
>
|
|
74
|
+
<Icon name={iconName} size={24} color={tokens.iconColor} />
|
|
75
|
+
</Pressable>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
22
22
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
23
23
|
import type { Modes } from '../../design-tokens'
|
|
24
|
+
import AutoplayControl from '../AutoplayControl/AutoplayControl'
|
|
25
|
+
import NumberPagination from '../NumberPagination/NumberPagination'
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Context
|
|
@@ -75,6 +77,13 @@ export interface CarouselProps {
|
|
|
75
77
|
paddingHorizontal?: number
|
|
76
78
|
/** Explicit vertical padding (px) for the outer container. Overrides `carousel/padding/vertical`. */
|
|
77
79
|
paddingVertical?: number
|
|
80
|
+
/**
|
|
81
|
+
* Carousel visual type.
|
|
82
|
+
* - `'Default'` — standard carousel with dot pagination below.
|
|
83
|
+
* - `'Numbered'` — full-bleed carousel with floating AutoplayControl and
|
|
84
|
+
* NumberPagination overlaid at the bottom center.
|
|
85
|
+
*/
|
|
86
|
+
type?: 'Default' | 'Numbered'
|
|
78
87
|
|
|
79
88
|
/** Called when the active index changes. */
|
|
80
89
|
onIndexChange?: (index: number) => void
|
|
@@ -97,10 +106,12 @@ export function Carousel({
|
|
|
97
106
|
itemWidth: itemWidthProp,
|
|
98
107
|
paddingHorizontal: paddingHorizontalProp,
|
|
99
108
|
paddingVertical: paddingVerticalProp,
|
|
109
|
+
type = 'Default',
|
|
100
110
|
|
|
101
111
|
onIndexChange,
|
|
102
112
|
style,
|
|
103
113
|
}: CarouselProps) {
|
|
114
|
+
const isNumbered = type === 'Numbered'
|
|
104
115
|
// ---- Token resolution ----
|
|
105
116
|
const tokenGap = parseFloat(
|
|
106
117
|
getVariableByName('carousel/gap', modes),
|
|
@@ -119,15 +130,32 @@ export function Carousel({
|
|
|
119
130
|
const maxHeight = parseFloat(
|
|
120
131
|
getVariableByName('carousel/maxHeight', modes),
|
|
121
132
|
)
|
|
133
|
+
// Gap between AutoplayControl and NumberPagination in the Numbered overlay.
|
|
134
|
+
const controlGap = parseFloat(
|
|
135
|
+
getVariableByName('carouselControl/gap', modes),
|
|
136
|
+
)
|
|
137
|
+
|
|
122
138
|
// Spacing between the cards row and the pagination dots uses `carousel/gap`.
|
|
123
139
|
const paginationOffset = gap
|
|
124
140
|
|
|
125
141
|
// ---- Refs & state ----
|
|
126
142
|
const scrollRef = useRef<ScrollView>(null)
|
|
127
143
|
const [activeIndex, setActiveIndex] = useState(0)
|
|
144
|
+
const activeIndexRef = useRef(0)
|
|
128
145
|
const [containerWidth, setContainerWidth] = useState(0)
|
|
129
146
|
const autoPlayTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
130
147
|
const userInteracting = useRef(false)
|
|
148
|
+
// Guard: suppress handleScroll index updates during programmatic scrollTo
|
|
149
|
+
// animations so intermediate offsets don't flicker activeIndex back.
|
|
150
|
+
const programmaticScroll = useRef(false)
|
|
151
|
+
const programmaticTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
activeIndexRef.current = activeIndex
|
|
155
|
+
}, [activeIndex])
|
|
156
|
+
|
|
157
|
+
// Interactive play/pause state for the Numbered variant.
|
|
158
|
+
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
|
131
159
|
|
|
132
160
|
// Flatten children so we can count items
|
|
133
161
|
const items = useMemo(
|
|
@@ -137,13 +165,15 @@ export function Carousel({
|
|
|
137
165
|
|
|
138
166
|
const totalItems = items.length
|
|
139
167
|
|
|
140
|
-
// Effective item width: provided prop, or full container width minus peek offsets
|
|
168
|
+
// Effective item width: provided prop, or full container width minus peek offsets.
|
|
169
|
+
// For the Numbered variant the items are full-bleed (no peek padding).
|
|
141
170
|
const effectiveItemWidth = useMemo(() => {
|
|
142
171
|
if (itemWidthProp != null) return itemWidthProp
|
|
143
172
|
if (containerWidth === 0) return 0
|
|
173
|
+
if (isNumbered) return containerWidth
|
|
144
174
|
// Full-width minus peekOffset on each side
|
|
145
175
|
return containerWidth - containerPaddingH * 4
|
|
146
|
-
}, [itemWidthProp, containerWidth, containerPaddingH])
|
|
176
|
+
}, [itemWidthProp, containerWidth, containerPaddingH, isNumbered])
|
|
147
177
|
|
|
148
178
|
// Snap interval = item width + gap
|
|
149
179
|
const snapInterval = effectiveItemWidth + gap
|
|
@@ -158,6 +188,17 @@ export function Carousel({
|
|
|
158
188
|
} else {
|
|
159
189
|
targetIndex = Math.max(0, Math.min(index, totalItems - 1))
|
|
160
190
|
}
|
|
191
|
+
// Lock out handleScroll during the animated scroll so intermediate
|
|
192
|
+
// offsets don't flicker activeIndex back to the old value.
|
|
193
|
+
if (animated) {
|
|
194
|
+
programmaticScroll.current = true
|
|
195
|
+
if (programmaticTimer.current != null) {
|
|
196
|
+
clearTimeout(programmaticTimer.current)
|
|
197
|
+
}
|
|
198
|
+
programmaticTimer.current = setTimeout(() => {
|
|
199
|
+
programmaticScroll.current = false
|
|
200
|
+
}, 500)
|
|
201
|
+
}
|
|
161
202
|
scrollRef.current?.scrollTo({
|
|
162
203
|
x: targetIndex * snapInterval,
|
|
163
204
|
animated,
|
|
@@ -185,6 +226,9 @@ export function Carousel({
|
|
|
185
226
|
const handleScroll = useCallback(
|
|
186
227
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
187
228
|
if (snapInterval === 0) return
|
|
229
|
+
// Skip index recalculation during programmatic scrollTo animations
|
|
230
|
+
// to prevent the active indicator from flickering back to the old page.
|
|
231
|
+
if (programmaticScroll.current) return
|
|
188
232
|
const offsetX = event.nativeEvent.contentOffset.x
|
|
189
233
|
const newIndex = Math.round(offsetX / snapInterval)
|
|
190
234
|
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1))
|
|
@@ -205,31 +249,28 @@ export function Carousel({
|
|
|
205
249
|
}, [])
|
|
206
250
|
|
|
207
251
|
const startAutoPlay = useCallback(() => {
|
|
208
|
-
if (!autoPlay || totalItems <= 1) return
|
|
252
|
+
if ((!autoPlay && !isPlaying) || totalItems <= 1) return
|
|
253
|
+
if (!isPlaying) return
|
|
209
254
|
clearAutoPlay()
|
|
210
255
|
autoPlayTimer.current = setInterval(() => {
|
|
211
256
|
if (!userInteracting.current) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})
|
|
220
|
-
onIndexChange?.(next)
|
|
221
|
-
return next
|
|
222
|
-
})
|
|
257
|
+
const prev = activeIndexRef.current
|
|
258
|
+
const next = loop
|
|
259
|
+
? (prev + 1) % totalItems
|
|
260
|
+
: Math.min(prev + 1, totalItems - 1)
|
|
261
|
+
// Route through scrollToIndex so the programmaticScroll guard
|
|
262
|
+
// suppresses intermediate handleScroll offsets during the animation.
|
|
263
|
+
scrollToIndex(next, true)
|
|
223
264
|
}
|
|
224
265
|
}, autoPlayInterval)
|
|
225
266
|
}, [
|
|
226
267
|
autoPlay,
|
|
268
|
+
isPlaying,
|
|
227
269
|
totalItems,
|
|
228
270
|
loop,
|
|
229
|
-
snapInterval,
|
|
230
271
|
autoPlayInterval,
|
|
231
272
|
clearAutoPlay,
|
|
232
|
-
|
|
273
|
+
scrollToIndex,
|
|
233
274
|
])
|
|
234
275
|
|
|
235
276
|
useEffect(() => {
|
|
@@ -244,8 +285,27 @@ export function Carousel({
|
|
|
244
285
|
const handleScrollEndDrag = useCallback(() => {
|
|
245
286
|
userInteracting.current = false
|
|
246
287
|
// Reset autoplay timer after interaction
|
|
247
|
-
if (autoPlay) startAutoPlay()
|
|
248
|
-
}, [autoPlay, startAutoPlay])
|
|
288
|
+
if (autoPlay || isPlaying) startAutoPlay()
|
|
289
|
+
}, [autoPlay, isPlaying, startAutoPlay])
|
|
290
|
+
|
|
291
|
+
const finishProgrammaticScroll = useCallback(
|
|
292
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
293
|
+
programmaticScroll.current = false
|
|
294
|
+
if (programmaticTimer.current != null) {
|
|
295
|
+
clearTimeout(programmaticTimer.current)
|
|
296
|
+
programmaticTimer.current = null
|
|
297
|
+
}
|
|
298
|
+
if (snapInterval === 0) return
|
|
299
|
+
const offsetX = event.nativeEvent.contentOffset.x
|
|
300
|
+
const newIndex = Math.round(offsetX / snapInterval)
|
|
301
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1))
|
|
302
|
+
if (clampedIndex !== activeIndexRef.current) {
|
|
303
|
+
setActiveIndex(clampedIndex)
|
|
304
|
+
onIndexChange?.(clampedIndex)
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[snapInterval, totalItems, onIndexChange],
|
|
308
|
+
)
|
|
249
309
|
|
|
250
310
|
// ---- Layout ----
|
|
251
311
|
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
|
@@ -265,15 +325,37 @@ export function Carousel({
|
|
|
265
325
|
[modes, activeIndex, totalItems, goTo, goNext, goPrev],
|
|
266
326
|
)
|
|
267
327
|
|
|
328
|
+
// ---- Toggle handler for Numbered variant ----
|
|
329
|
+
const handleToggleAutoplay = useCallback(() => {
|
|
330
|
+
setIsPlaying((prev) => {
|
|
331
|
+
if (prev) {
|
|
332
|
+
// Pausing
|
|
333
|
+
clearAutoPlay()
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
// Resuming — autoplay will restart via the effect
|
|
337
|
+
return true
|
|
338
|
+
})
|
|
339
|
+
}, [clearAutoPlay])
|
|
340
|
+
|
|
341
|
+
// Restart autoplay whenever isPlaying flips to true
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (isPlaying) startAutoPlay()
|
|
344
|
+
else clearAutoPlay()
|
|
345
|
+
}, [isPlaying, startAutoPlay, clearAutoPlay])
|
|
346
|
+
|
|
268
347
|
// ---- Render ----
|
|
269
348
|
// Explicit padding props win verbatim; otherwise fall back to the
|
|
270
349
|
// token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
|
|
271
|
-
|
|
272
|
-
const
|
|
350
|
+
// Numbered variant is full-bleed: zero padding.
|
|
351
|
+
const effectivePaddingH = isNumbered ? 0 : (paddingHorizontalProp ?? containerPaddingH)
|
|
352
|
+
const effectivePaddingV = isNumbered ? 0 : (paddingVerticalProp ?? containerPaddingV)
|
|
273
353
|
|
|
274
354
|
const outerStyle: ViewStyle = {
|
|
275
355
|
paddingVertical: effectivePaddingV,
|
|
276
|
-
maxHeight,
|
|
356
|
+
maxHeight: isNumbered ? undefined : maxHeight,
|
|
357
|
+
// Numbered needs relative positioning for the floating controls overlay.
|
|
358
|
+
...(isNumbered ? { overflow: 'hidden' as const } : {}),
|
|
277
359
|
}
|
|
278
360
|
|
|
279
361
|
const contentContainerStyle: ViewStyle = {
|
|
@@ -299,7 +381,7 @@ export function Carousel({
|
|
|
299
381
|
scrollEventThrottle={16}
|
|
300
382
|
onScrollBeginDrag={handleScrollBeginDrag}
|
|
301
383
|
onScrollEndDrag={handleScrollEndDrag}
|
|
302
|
-
|
|
384
|
+
onMomentumScrollEnd={finishProgrammaticScroll}
|
|
303
385
|
>
|
|
304
386
|
{items.map((child, index) => {
|
|
305
387
|
// Strict slot box: width must be honored; never grow or shrink with
|
|
@@ -343,12 +425,40 @@ export function Carousel({
|
|
|
343
425
|
})}
|
|
344
426
|
</ScrollView>
|
|
345
427
|
|
|
346
|
-
{showPagination && totalItems > 1 && (
|
|
428
|
+
{!isNumbered && showPagination && totalItems > 1 && (
|
|
347
429
|
<Pagination
|
|
348
430
|
modes={modes}
|
|
349
431
|
style={{ marginTop: paginationOffset }}
|
|
350
432
|
/>
|
|
351
433
|
)}
|
|
434
|
+
|
|
435
|
+
{isNumbered && showPagination && totalItems > 1 && (
|
|
436
|
+
<View
|
|
437
|
+
style={{
|
|
438
|
+
position: 'absolute',
|
|
439
|
+
bottom: 12,
|
|
440
|
+
left: 0,
|
|
441
|
+
right: 0,
|
|
442
|
+
flexDirection: 'row',
|
|
443
|
+
justifyContent: 'center',
|
|
444
|
+
alignItems: 'center',
|
|
445
|
+
gap: controlGap,
|
|
446
|
+
}}
|
|
447
|
+
pointerEvents="box-none"
|
|
448
|
+
>
|
|
449
|
+
<AutoplayControl
|
|
450
|
+
state={isPlaying ? 'pause' : 'play'}
|
|
451
|
+
onPress={handleToggleAutoplay}
|
|
452
|
+
modes={modes}
|
|
453
|
+
/>
|
|
454
|
+
<NumberPagination
|
|
455
|
+
activePage={activeIndex + 1}
|
|
456
|
+
totalPages={totalItems}
|
|
457
|
+
onPageChange={(page) => goTo(page - 1)}
|
|
458
|
+
modes={modes}
|
|
459
|
+
/>
|
|
460
|
+
</View>
|
|
461
|
+
)}
|
|
352
462
|
</View>
|
|
353
463
|
</CarouselContext.Provider>
|
|
354
464
|
)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useContext, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
|
+
Text,
|
|
4
5
|
View,
|
|
5
6
|
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
6
8
|
type ViewProps,
|
|
7
9
|
type ViewStyle,
|
|
8
10
|
} from 'react-native'
|
|
@@ -31,6 +33,12 @@ export type ContentSheetProps = {
|
|
|
31
33
|
* the same theme without you wiring `modes` onto each one.
|
|
32
34
|
*/
|
|
33
35
|
modes?: Modes
|
|
36
|
+
/**
|
|
37
|
+
* Optional title rendered in a centered, sticky header at the top of the
|
|
38
|
+
* sheet. Uses the same `drawer/title*` token family as the `Drawer` title so
|
|
39
|
+
* the two surfaces stay visually consistent.
|
|
40
|
+
*/
|
|
41
|
+
title?: string
|
|
34
42
|
/**
|
|
35
43
|
* Keep the sheet above the on-screen keyboard. When the keyboard opens the
|
|
36
44
|
* sheet rises by exactly the keyboard height; when it closes the sheet drops
|
|
@@ -70,6 +78,8 @@ export type ContentSheetProps = {
|
|
|
70
78
|
interface ContentSheetStyle {
|
|
71
79
|
container: ViewStyle
|
|
72
80
|
paddingBottom: number
|
|
81
|
+
headerStyle: ViewStyle
|
|
82
|
+
titleStyle: TextStyle
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
function resolveContentSheetStyle(modes: Modes): ContentSheetStyle {
|
|
@@ -81,6 +91,23 @@ function resolveContentSheetStyle(modes: Modes): ContentSheetStyle {
|
|
|
81
91
|
const radiusTop = getVariableByName('contentSheet/padding/radius/top', modes) as number
|
|
82
92
|
const radiusBottom = getVariableByName('contentSheet/padding/radius/bottom', modes) as number
|
|
83
93
|
|
|
94
|
+
// Title tokens mirror the Drawer title so the two bottom-sheet surfaces stay
|
|
95
|
+
// visually aligned.
|
|
96
|
+
const titleColor = getVariableByName('drawer/title/color', modes) as string
|
|
97
|
+
const titleSize = getVariableByName('drawer/title/fontSize', modes) as number
|
|
98
|
+
const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes) as number
|
|
99
|
+
const titleFontFamily = getVariableByName('drawer/title/fontFamily', modes) as string
|
|
100
|
+
const titleFontWeightRaw = getVariableByName('drawer/title/fontWeight', modes)
|
|
101
|
+
const titleFontWeight =
|
|
102
|
+
typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw
|
|
103
|
+
|
|
104
|
+
const titleWrapPaddingTop = getVariableByName('drawer/titleWrap/padding/top', modes) as number
|
|
105
|
+
const titleWrapPaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes) as number
|
|
106
|
+
const titleWrapPaddingHorizontal = getVariableByName(
|
|
107
|
+
'drawer/titleWrap/padding/horizontal',
|
|
108
|
+
modes
|
|
109
|
+
) as number
|
|
110
|
+
|
|
84
111
|
return {
|
|
85
112
|
container: {
|
|
86
113
|
backgroundColor,
|
|
@@ -97,6 +124,20 @@ function resolveContentSheetStyle(modes: Modes): ContentSheetStyle {
|
|
|
97
124
|
overflow: 'hidden',
|
|
98
125
|
},
|
|
99
126
|
paddingBottom,
|
|
127
|
+
headerStyle: {
|
|
128
|
+
paddingTop: titleWrapPaddingTop,
|
|
129
|
+
paddingBottom: titleWrapPaddingBottom,
|
|
130
|
+
paddingHorizontal: titleWrapPaddingHorizontal,
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
},
|
|
133
|
+
titleStyle: {
|
|
134
|
+
color: titleColor,
|
|
135
|
+
fontSize: titleSize,
|
|
136
|
+
lineHeight: titleLineHeight,
|
|
137
|
+
fontFamily: titleFontFamily,
|
|
138
|
+
fontWeight: titleFontWeight as TextStyle['fontWeight'],
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
},
|
|
100
141
|
}
|
|
101
142
|
}
|
|
102
143
|
|
|
@@ -126,6 +167,7 @@ const pinnedStyle: ViewStyle = {
|
|
|
126
167
|
function ContentSheet({
|
|
127
168
|
children,
|
|
128
169
|
modes = EMPTY_MODES,
|
|
170
|
+
title,
|
|
129
171
|
avoidKeyboard = true,
|
|
130
172
|
keyboardSpacing = 0,
|
|
131
173
|
safeAreaBottom = true,
|
|
@@ -162,6 +204,12 @@ function ContentSheet({
|
|
|
162
204
|
// `avoidKeyboard` (and platform). Because they are distinct component types,
|
|
163
205
|
// React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
|
|
164
206
|
// inside `KeyboardAwareSheet` always runs in a stable hook order.
|
|
207
|
+
const header = title ? (
|
|
208
|
+
<View style={resolved.headerStyle}>
|
|
209
|
+
<Text style={resolved.titleStyle}>{title}</Text>
|
|
210
|
+
</View>
|
|
211
|
+
) : null
|
|
212
|
+
|
|
165
213
|
if (avoidKeyboard && !IS_WEB) {
|
|
166
214
|
return (
|
|
167
215
|
<KeyboardAwareSheet
|
|
@@ -169,6 +217,7 @@ function ContentSheet({
|
|
|
169
217
|
spacing={keyboardSpacing}
|
|
170
218
|
{...rest}
|
|
171
219
|
>
|
|
220
|
+
{header}
|
|
172
221
|
{processedChildren}
|
|
173
222
|
</KeyboardAwareSheet>
|
|
174
223
|
)
|
|
@@ -176,6 +225,7 @@ function ContentSheet({
|
|
|
176
225
|
|
|
177
226
|
return (
|
|
178
227
|
<View style={containerStyle} {...rest}>
|
|
228
|
+
{header}
|
|
179
229
|
{processedChildren}
|
|
180
230
|
</View>
|
|
181
231
|
)
|