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.
Files changed (81) hide show
  1. package/CHANGELOG.md +40 -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/ContentSheet/ContentSheet.js +39 -5
  5. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  6. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  7. package/lib/commonjs/components/Grid/Grid.js +206 -0
  8. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  9. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  10. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  11. package/lib/commonjs/components/index.js +42 -0
  12. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  13. package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
  14. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  15. package/lib/commonjs/icons/components/IconImage.js +19 -0
  16. package/lib/commonjs/icons/components/IconMic.js +19 -0
  17. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  18. package/lib/commonjs/icons/components/IconPause.js +19 -0
  19. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  20. package/lib/commonjs/icons/components/index.js +66 -0
  21. package/lib/commonjs/icons/registry.js +2 -2
  22. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  23. package/lib/module/components/Carousel/Carousel.js +111 -19
  24. package/lib/module/components/ContentSheet/ContentSheet.js +41 -7
  25. package/lib/module/components/FormUpload/Additem.js +162 -0
  26. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  27. package/lib/module/components/Grid/Grid.js +201 -0
  28. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  29. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  30. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  31. package/lib/module/components/index.js +7 -1
  32. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  33. package/lib/module/design-tokens/figma-modes.generated.js +21 -10
  34. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  35. package/lib/module/icons/components/IconImage.js +12 -0
  36. package/lib/module/icons/components/IconMic.js +12 -0
  37. package/lib/module/icons/components/IconMicoff.js +12 -0
  38. package/lib/module/icons/components/IconPause.js +12 -0
  39. package/lib/module/icons/components/IconPlay.js +12 -0
  40. package/lib/module/icons/components/index.js +6 -0
  41. package/lib/module/icons/registry.js +2 -2
  42. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  43. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  44. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
  45. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  46. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  47. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  48. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  49. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  50. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  51. package/lib/typescript/src/components/index.d.ts +34 -28
  52. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  53. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  54. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  55. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  56. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  57. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  58. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  59. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  60. package/lib/typescript/src/icons/registry.d.ts +1 -1
  61. package/package.json +1 -1
  62. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  63. package/src/components/Carousel/Carousel.tsx +133 -23
  64. package/src/components/ContentSheet/ContentSheet.tsx +50 -0
  65. package/src/components/FormUpload/Additem.tsx +262 -0
  66. package/src/components/FormUpload/FormUpload.tsx +313 -0
  67. package/src/components/Grid/Grid.tsx +368 -0
  68. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  69. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  70. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  71. package/src/components/index.ts +329 -182
  72. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  73. package/src/design-tokens/figma-modes.generated.ts +21 -10
  74. package/src/icons/components/IconBrokenimage.tsx +11 -0
  75. package/src/icons/components/IconImage.tsx +11 -0
  76. package/src/icons/components/IconMic.tsx +11 -0
  77. package/src/icons/components/IconMicoff.tsx +11 -0
  78. package/src/icons/components/IconPause.tsx +11 -0
  79. package/src/icons/components/IconPlay.tsx +11 -0
  80. package/src/icons/components/index.ts +6 -0
  81. 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", "Profile card"];
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 ["False", "True"];
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"];
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconBrokenimage: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconBrokenimage.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconImage: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconImage.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconMic: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconMic.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconMicoff: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconMicoff.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconPause: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconPause.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { SvgProps } from 'react-native-svg';
2
+ export declare const IconPlay: (props: SvgProps) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=IconPlay.d.ts.map
@@ -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-03T01:43:10.333Z
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -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
- setActiveIndex((prev) => {
213
- const next = loop
214
- ? (prev + 1) % totalItems
215
- : Math.min(prev + 1, totalItems - 1)
216
- scrollRef.current?.scrollTo({
217
- x: next * snapInterval,
218
- animated: true,
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
- onIndexChange,
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
- const effectivePaddingH = paddingHorizontalProp ?? containerPaddingH
272
- const effectivePaddingV = paddingVerticalProp ?? containerPaddingV
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
  )