jfs-components 0.0.21 → 0.0.24
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/lib/commonjs/assets/fonts/JioType Var.ttf +0 -0
- package/lib/commonjs/components/Carousel/Carousel.js +341 -0
- package/lib/commonjs/components/Carousel/Carousel.js.map +1 -0
- package/lib/commonjs/components/Carousel/Carousel.mdx +154 -0
- package/lib/commonjs/components/Drawer/Drawer.js +9 -2
- package/lib/commonjs/components/Drawer/Drawer.js.map +1 -1
- package/lib/commonjs/components/RadioButton/RadioButton.js +194 -0
- package/lib/commonjs/components/RadioButton/RadioButton.js.map +1 -0
- package/lib/commonjs/components/RadioButton/RadioButton.mdx +92 -0
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +3 -1
- package/lib/commonjs/components/UpiHandle/UpiHandle.js.map +1 -1
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/design-tokens/figma-variables-resolver.js +9 -3
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/assets/fonts/JioType Var.ttf +0 -0
- package/lib/module/components/Carousel/Carousel.js +333 -0
- package/lib/module/components/Carousel/Carousel.js.map +1 -0
- package/lib/module/components/Carousel/Carousel.mdx +154 -0
- package/lib/module/components/Drawer/Drawer.js +10 -3
- package/lib/module/components/Drawer/Drawer.js.map +1 -1
- package/lib/module/components/RadioButton/RadioButton.js +188 -0
- package/lib/module/components/RadioButton/RadioButton.js.map +1 -0
- package/lib/module/components/RadioButton/RadioButton.mdx +92 -0
- package/lib/module/components/UpiHandle/UpiHandle.js +3 -1
- package/lib/module/components/UpiHandle/UpiHandle.js.map +1 -1
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/components/Carousel/Carousel.d.ts +48 -0
- package/lib/typescript/components/Carousel/Carousel.d.ts.map +1 -0
- package/lib/typescript/components/Drawer/Drawer.d.ts.map +1 -1
- package/lib/typescript/components/RadioButton/RadioButton.d.ts +30 -0
- package/lib/typescript/components/RadioButton/RadioButton.d.ts.map +1 -0
- package/lib/typescript/components/UpiHandle/UpiHandle.d.ts +4 -2
- package/lib/typescript/components/UpiHandle/UpiHandle.d.ts.map +1 -1
- package/lib/typescript/components/index.d.ts +1 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/icons/registry.d.ts +1 -1
- package/package.json +3 -3
- package/src/assets/fonts/JioType Var.ttf +0 -0
- package/src/components/.token-metadata.json +72 -0
- package/src/components/Carousel/Carousel.mdx +154 -0
- package/src/components/Carousel/Carousel.tsx +472 -0
- package/src/components/Drawer/Drawer.tsx +10 -2
- package/src/components/RadioButton/RadioButton.mdx +92 -0
- package/src/components/RadioButton/RadioButton.tsx +226 -0
- package/src/components/UpiHandle/UpiHandle.tsx +5 -2
- package/src/components/index.ts +1 -0
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useEffect,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import {
|
|
11
|
+
View,
|
|
12
|
+
ScrollView,
|
|
13
|
+
Animated,
|
|
14
|
+
Pressable,
|
|
15
|
+
type ViewStyle,
|
|
16
|
+
type StyleProp,
|
|
17
|
+
type NativeSyntheticEvent,
|
|
18
|
+
type NativeScrollEvent,
|
|
19
|
+
type LayoutChangeEvent,
|
|
20
|
+
} from 'react-native'
|
|
21
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Context
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
interface CarouselContextValue {
|
|
28
|
+
modes: Record<string, any>
|
|
29
|
+
activeIndex: number
|
|
30
|
+
totalItems: number
|
|
31
|
+
goTo: (index: number) => void
|
|
32
|
+
goNext: () => void
|
|
33
|
+
goPrev: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CarouselContext = createContext<CarouselContextValue>({
|
|
37
|
+
modes: {},
|
|
38
|
+
activeIndex: 0,
|
|
39
|
+
totalItems: 0,
|
|
40
|
+
goTo: () => {},
|
|
41
|
+
goNext: () => {},
|
|
42
|
+
goPrev: () => {},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Types
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface CarouselProps {
|
|
50
|
+
/** Content items to display (should be Carousel.Item or any React nodes). */
|
|
51
|
+
children?: React.ReactNode
|
|
52
|
+
/** Modes object for design-token resolution. */
|
|
53
|
+
modes?: Record<string, any>
|
|
54
|
+
/** Enable auto-play. Default: false. */
|
|
55
|
+
autoPlay?: boolean
|
|
56
|
+
/** Auto-play interval in ms. Default: 4000. */
|
|
57
|
+
autoPlayInterval?: number
|
|
58
|
+
/** Show pagination dots. Default: true. */
|
|
59
|
+
showPagination?: boolean
|
|
60
|
+
/** Enable looping (jumps back to first after last). Default: true. */
|
|
61
|
+
loop?: boolean
|
|
62
|
+
/** Gap between items in px. Resolved from token `carousel/gap` or defaults to 12. */
|
|
63
|
+
gap?: number
|
|
64
|
+
/** Width of each carousel item. When undefined, items take full container width. */
|
|
65
|
+
itemWidth?: number
|
|
66
|
+
/** Horizontal padding around the carousel track, allowing peek of adjacent items. Defaults to 0. */
|
|
67
|
+
peekOffset?: number
|
|
68
|
+
/** Called when the active index changes. */
|
|
69
|
+
onIndexChange?: (index: number) => void
|
|
70
|
+
/** Style overrides for the outermost container. */
|
|
71
|
+
style?: StyleProp<ViewStyle>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Carousel
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export function Carousel({
|
|
79
|
+
children,
|
|
80
|
+
modes = {},
|
|
81
|
+
autoPlay = false,
|
|
82
|
+
autoPlayInterval = 4000,
|
|
83
|
+
showPagination = true,
|
|
84
|
+
loop = true,
|
|
85
|
+
gap: gapProp,
|
|
86
|
+
itemWidth: itemWidthProp,
|
|
87
|
+
peekOffset: peekOffsetProp,
|
|
88
|
+
onIndexChange,
|
|
89
|
+
style,
|
|
90
|
+
}: CarouselProps) {
|
|
91
|
+
// ---- Token resolution ----
|
|
92
|
+
const tokenGap = parseFloat(
|
|
93
|
+
getVariableByName('carousel/gap', modes) || '12',
|
|
94
|
+
)
|
|
95
|
+
const gap = gapProp ?? tokenGap
|
|
96
|
+
|
|
97
|
+
const tokenPeekOffset = parseFloat(
|
|
98
|
+
getVariableByName('carousel/peekOffset', modes) || '0',
|
|
99
|
+
)
|
|
100
|
+
const peekOffset = peekOffsetProp ?? tokenPeekOffset
|
|
101
|
+
|
|
102
|
+
const containerPaddingH = parseFloat(
|
|
103
|
+
getVariableByName('carousel/padding/horizontal', modes) || '0',
|
|
104
|
+
)
|
|
105
|
+
const containerPaddingV = parseFloat(
|
|
106
|
+
getVariableByName('carousel/padding/vertical', modes) || '0',
|
|
107
|
+
)
|
|
108
|
+
const paginationGap = parseFloat(
|
|
109
|
+
getVariableByName('carousel/pagination/gap', modes) || '12',
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// ---- Refs & state ----
|
|
113
|
+
const scrollRef = useRef<ScrollView>(null)
|
|
114
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
115
|
+
const [containerWidth, setContainerWidth] = useState(0)
|
|
116
|
+
const autoPlayTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
117
|
+
const userInteracting = useRef(false)
|
|
118
|
+
|
|
119
|
+
// Flatten children so we can count items
|
|
120
|
+
const items = useMemo(
|
|
121
|
+
() => React.Children.toArray(children).filter(React.isValidElement),
|
|
122
|
+
[children],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const totalItems = items.length
|
|
126
|
+
|
|
127
|
+
// Effective item width: provided prop, or full container width minus peek offsets
|
|
128
|
+
const effectiveItemWidth = useMemo(() => {
|
|
129
|
+
if (itemWidthProp != null) return itemWidthProp
|
|
130
|
+
if (containerWidth === 0) return 0
|
|
131
|
+
// Full-width minus peekOffset on each side
|
|
132
|
+
return containerWidth - peekOffset * 2 - containerPaddingH * 2
|
|
133
|
+
}, [itemWidthProp, containerWidth, peekOffset, containerPaddingH])
|
|
134
|
+
|
|
135
|
+
// Snap interval = item width + gap
|
|
136
|
+
const snapInterval = effectiveItemWidth + gap
|
|
137
|
+
|
|
138
|
+
// ---- Navigation helpers ----
|
|
139
|
+
const scrollToIndex = useCallback(
|
|
140
|
+
(index: number, animated = true) => {
|
|
141
|
+
if (totalItems === 0 || snapInterval === 0) return
|
|
142
|
+
let targetIndex = index
|
|
143
|
+
if (loop) {
|
|
144
|
+
targetIndex = ((index % totalItems) + totalItems) % totalItems
|
|
145
|
+
} else {
|
|
146
|
+
targetIndex = Math.max(0, Math.min(index, totalItems - 1))
|
|
147
|
+
}
|
|
148
|
+
scrollRef.current?.scrollTo({
|
|
149
|
+
x: targetIndex * snapInterval,
|
|
150
|
+
animated,
|
|
151
|
+
})
|
|
152
|
+
setActiveIndex(targetIndex)
|
|
153
|
+
onIndexChange?.(targetIndex)
|
|
154
|
+
},
|
|
155
|
+
[totalItems, snapInterval, loop, onIndexChange],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const goTo = useCallback(
|
|
159
|
+
(index: number) => scrollToIndex(index),
|
|
160
|
+
[scrollToIndex],
|
|
161
|
+
)
|
|
162
|
+
const goNext = useCallback(
|
|
163
|
+
() => scrollToIndex(activeIndex + 1),
|
|
164
|
+
[scrollToIndex, activeIndex],
|
|
165
|
+
)
|
|
166
|
+
const goPrev = useCallback(
|
|
167
|
+
() => scrollToIndex(activeIndex - 1),
|
|
168
|
+
[scrollToIndex, activeIndex],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// ---- Scroll event handler ----
|
|
172
|
+
const handleScroll = useCallback(
|
|
173
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
174
|
+
if (snapInterval === 0) return
|
|
175
|
+
const offsetX = event.nativeEvent.contentOffset.x
|
|
176
|
+
const newIndex = Math.round(offsetX / snapInterval)
|
|
177
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1))
|
|
178
|
+
if (clampedIndex !== activeIndex) {
|
|
179
|
+
setActiveIndex(clampedIndex)
|
|
180
|
+
onIndexChange?.(clampedIndex)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
[snapInterval, totalItems, activeIndex, onIndexChange],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// ---- Auto-play ----
|
|
187
|
+
const clearAutoPlay = useCallback(() => {
|
|
188
|
+
if (autoPlayTimer.current != null) {
|
|
189
|
+
clearInterval(autoPlayTimer.current)
|
|
190
|
+
autoPlayTimer.current = null
|
|
191
|
+
}
|
|
192
|
+
}, [])
|
|
193
|
+
|
|
194
|
+
const startAutoPlay = useCallback(() => {
|
|
195
|
+
if (!autoPlay || totalItems <= 1) return
|
|
196
|
+
clearAutoPlay()
|
|
197
|
+
autoPlayTimer.current = setInterval(() => {
|
|
198
|
+
if (!userInteracting.current) {
|
|
199
|
+
setActiveIndex((prev) => {
|
|
200
|
+
const next = loop
|
|
201
|
+
? (prev + 1) % totalItems
|
|
202
|
+
: Math.min(prev + 1, totalItems - 1)
|
|
203
|
+
scrollRef.current?.scrollTo({
|
|
204
|
+
x: next * snapInterval,
|
|
205
|
+
animated: true,
|
|
206
|
+
})
|
|
207
|
+
onIndexChange?.(next)
|
|
208
|
+
return next
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}, autoPlayInterval)
|
|
212
|
+
}, [
|
|
213
|
+
autoPlay,
|
|
214
|
+
totalItems,
|
|
215
|
+
loop,
|
|
216
|
+
snapInterval,
|
|
217
|
+
autoPlayInterval,
|
|
218
|
+
clearAutoPlay,
|
|
219
|
+
onIndexChange,
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
startAutoPlay()
|
|
224
|
+
return clearAutoPlay
|
|
225
|
+
}, [startAutoPlay, clearAutoPlay])
|
|
226
|
+
|
|
227
|
+
const handleScrollBeginDrag = useCallback(() => {
|
|
228
|
+
userInteracting.current = true
|
|
229
|
+
}, [])
|
|
230
|
+
|
|
231
|
+
const handleScrollEndDrag = useCallback(() => {
|
|
232
|
+
userInteracting.current = false
|
|
233
|
+
// Reset autoplay timer after interaction
|
|
234
|
+
if (autoPlay) startAutoPlay()
|
|
235
|
+
}, [autoPlay, startAutoPlay])
|
|
236
|
+
|
|
237
|
+
// ---- Layout ----
|
|
238
|
+
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
|
239
|
+
setContainerWidth(e.nativeEvent.layout.width)
|
|
240
|
+
}, [])
|
|
241
|
+
|
|
242
|
+
// ---- Context value ----
|
|
243
|
+
const contextValue = useMemo<CarouselContextValue>(
|
|
244
|
+
() => ({
|
|
245
|
+
modes,
|
|
246
|
+
activeIndex,
|
|
247
|
+
totalItems,
|
|
248
|
+
goTo,
|
|
249
|
+
goNext,
|
|
250
|
+
goPrev,
|
|
251
|
+
}),
|
|
252
|
+
[modes, activeIndex, totalItems, goTo, goNext, goPrev],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
// ---- Render ----
|
|
256
|
+
const outerStyle: ViewStyle = {
|
|
257
|
+
paddingVertical: containerPaddingV,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const contentContainerStyle: ViewStyle = {
|
|
261
|
+
paddingHorizontal: peekOffset + containerPaddingH,
|
|
262
|
+
gap,
|
|
263
|
+
// Align items to the start so snap works correctly
|
|
264
|
+
alignItems: 'flex-start',
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<CarouselContext.Provider value={contextValue}>
|
|
269
|
+
<View style={[outerStyle, style]} onLayout={handleLayout}>
|
|
270
|
+
<ScrollView
|
|
271
|
+
ref={scrollRef}
|
|
272
|
+
horizontal
|
|
273
|
+
pagingEnabled={false}
|
|
274
|
+
showsHorizontalScrollIndicator={false}
|
|
275
|
+
decelerationRate="fast"
|
|
276
|
+
snapToInterval={snapInterval > 0 ? snapInterval : undefined}
|
|
277
|
+
snapToAlignment="start"
|
|
278
|
+
contentContainerStyle={contentContainerStyle}
|
|
279
|
+
onScroll={handleScroll}
|
|
280
|
+
scrollEventThrottle={16}
|
|
281
|
+
onScrollBeginDrag={handleScrollBeginDrag}
|
|
282
|
+
onScrollEndDrag={handleScrollEndDrag}
|
|
283
|
+
|
|
284
|
+
>
|
|
285
|
+
{items.map((child, index) => {
|
|
286
|
+
const itemStyle: ViewStyle = {
|
|
287
|
+
width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Pass modes down to children
|
|
291
|
+
const childWithModes = React.isValidElement(child)
|
|
292
|
+
? React.cloneElement(child as React.ReactElement<any>, {
|
|
293
|
+
modes: {
|
|
294
|
+
...((child.props as any)?.modes || {}),
|
|
295
|
+
...modes,
|
|
296
|
+
},
|
|
297
|
+
style: [itemStyle, (child.props as any)?.style],
|
|
298
|
+
})
|
|
299
|
+
: child
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<View key={index} style={itemStyle}>
|
|
303
|
+
{childWithModes}
|
|
304
|
+
</View>
|
|
305
|
+
)
|
|
306
|
+
})}
|
|
307
|
+
</ScrollView>
|
|
308
|
+
|
|
309
|
+
{showPagination && totalItems > 1 && (
|
|
310
|
+
<Pagination
|
|
311
|
+
modes={modes}
|
|
312
|
+
style={{ marginTop: paginationGap }}
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
315
|
+
</View>
|
|
316
|
+
</CarouselContext.Provider>
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Carousel.Item
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
export interface CarouselItemProps {
|
|
325
|
+
children?: React.ReactNode
|
|
326
|
+
modes?: Record<string, any>
|
|
327
|
+
style?: StyleProp<ViewStyle>
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Optional wrapper for carousel items. Not strictly required — any
|
|
332
|
+
* React node works as a direct child of `<Carousel>`.
|
|
333
|
+
*/
|
|
334
|
+
export function Item({ children, modes: _modes, style }: CarouselItemProps) {
|
|
335
|
+
return <View style={style}>{children}</View>
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Carousel.Pagination
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
export interface PaginationProps {
|
|
343
|
+
modes?: Record<string, any>
|
|
344
|
+
style?: StyleProp<ViewStyle>
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function Pagination({ modes: propModes, style }: PaginationProps) {
|
|
348
|
+
const { modes: ctxModes, activeIndex, totalItems, goTo } =
|
|
349
|
+
useContext(CarouselContext)
|
|
350
|
+
const modes = propModes || ctxModes || {}
|
|
351
|
+
|
|
352
|
+
// Token resolution for dots
|
|
353
|
+
const dotSize = parseFloat(
|
|
354
|
+
getVariableByName('carousel/pagination/dotSize', modes) || '8',
|
|
355
|
+
)
|
|
356
|
+
const dotActiveWidth = parseFloat(
|
|
357
|
+
getVariableByName('carousel/pagination/dotActiveWidth', modes) || '24',
|
|
358
|
+
)
|
|
359
|
+
const dotGap = parseFloat(
|
|
360
|
+
getVariableByName('carousel/pagination/dotGap', modes) || '8',
|
|
361
|
+
)
|
|
362
|
+
const dotColor =
|
|
363
|
+
(getVariableByName('carousel/pagination/dotColor', modes) as string) ||
|
|
364
|
+
'rgba(255,255,255,0.35)'
|
|
365
|
+
const dotActiveColor =
|
|
366
|
+
(getVariableByName(
|
|
367
|
+
'carousel/pagination/dotActiveColor',
|
|
368
|
+
modes,
|
|
369
|
+
) as string) || '#ffffff'
|
|
370
|
+
const dotRadius = parseFloat(
|
|
371
|
+
getVariableByName('carousel/pagination/dotRadius', modes) || '4',
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const containerStyle: ViewStyle = {
|
|
375
|
+
flexDirection: 'row',
|
|
376
|
+
justifyContent: 'center',
|
|
377
|
+
alignItems: 'center',
|
|
378
|
+
gap: dotGap,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<View
|
|
383
|
+
style={[containerStyle, style]}
|
|
384
|
+
accessibilityRole="tablist"
|
|
385
|
+
accessibilityLabel="Carousel pagination"
|
|
386
|
+
>
|
|
387
|
+
{Array.from({ length: totalItems }).map((_, i) => {
|
|
388
|
+
const isActive = i === activeIndex
|
|
389
|
+
return (
|
|
390
|
+
<Pressable
|
|
391
|
+
key={i}
|
|
392
|
+
onPress={() => goTo(i)}
|
|
393
|
+
accessibilityRole="tab"
|
|
394
|
+
accessibilityState={{ selected: isActive }}
|
|
395
|
+
accessibilityLabel={`Go to slide ${i + 1}`}
|
|
396
|
+
>
|
|
397
|
+
<AnimatedDot
|
|
398
|
+
isActive={isActive}
|
|
399
|
+
size={dotSize}
|
|
400
|
+
activeWidth={dotActiveWidth}
|
|
401
|
+
radius={dotRadius}
|
|
402
|
+
color={dotColor}
|
|
403
|
+
activeColor={dotActiveColor}
|
|
404
|
+
/>
|
|
405
|
+
</Pressable>
|
|
406
|
+
)
|
|
407
|
+
})}
|
|
408
|
+
</View>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// AnimatedDot (internal)
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
interface AnimatedDotProps {
|
|
417
|
+
isActive: boolean
|
|
418
|
+
size: number
|
|
419
|
+
activeWidth: number
|
|
420
|
+
radius: number
|
|
421
|
+
color: string
|
|
422
|
+
activeColor: string
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function AnimatedDot({
|
|
426
|
+
isActive,
|
|
427
|
+
size,
|
|
428
|
+
activeWidth,
|
|
429
|
+
radius,
|
|
430
|
+
color,
|
|
431
|
+
activeColor,
|
|
432
|
+
}: AnimatedDotProps) {
|
|
433
|
+
const animValue = useRef(new Animated.Value(isActive ? 1 : 0)).current
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
Animated.timing(animValue, {
|
|
437
|
+
toValue: isActive ? 1 : 0,
|
|
438
|
+
duration: 250,
|
|
439
|
+
useNativeDriver: false,
|
|
440
|
+
}).start()
|
|
441
|
+
}, [isActive, animValue])
|
|
442
|
+
|
|
443
|
+
const width = animValue.interpolate({
|
|
444
|
+
inputRange: [0, 1],
|
|
445
|
+
outputRange: [size, activeWidth],
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const backgroundColor = animValue.interpolate({
|
|
449
|
+
inputRange: [0, 1],
|
|
450
|
+
outputRange: [color, activeColor],
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<Animated.View
|
|
455
|
+
style={{
|
|
456
|
+
width,
|
|
457
|
+
height: size,
|
|
458
|
+
borderRadius: radius,
|
|
459
|
+
backgroundColor,
|
|
460
|
+
}}
|
|
461
|
+
/>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// Attach sub-components
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
Carousel.Item = Item
|
|
470
|
+
Carousel.Pagination = Pagination
|
|
471
|
+
|
|
472
|
+
export default Carousel
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from 'react-native-gesture-handler'
|
|
9
9
|
import Animated, {
|
|
10
10
|
runOnJS,
|
|
11
|
+
useAnimatedProps,
|
|
11
12
|
useAnimatedScrollHandler,
|
|
12
13
|
useAnimatedStyle,
|
|
13
14
|
useSharedValue,
|
|
@@ -116,6 +117,13 @@ function Drawer({
|
|
|
116
117
|
const scrollY = useSharedValue(0)
|
|
117
118
|
const scrollRef = useRef(null)
|
|
118
119
|
|
|
120
|
+
// Dynamically disable top bounce so a downward swipe at the top of
|
|
121
|
+
// the content drags the whole drawer instead of rubber-banding the
|
|
122
|
+
// scroll view. Bottom bounce is preserved when the user has scrolled.
|
|
123
|
+
const animatedScrollProps = useAnimatedProps(() => ({
|
|
124
|
+
bounces: scrollY.value > 1,
|
|
125
|
+
}))
|
|
126
|
+
|
|
119
127
|
// We need to know if we are currently dragging the drawer or scrolling the content
|
|
120
128
|
// to prevent conflict.
|
|
121
129
|
const isDrawerActive = useSharedValue(false)
|
|
@@ -330,8 +338,8 @@ function Drawer({
|
|
|
330
338
|
style={[styles.content, contentStyle]}
|
|
331
339
|
contentContainerStyle={[{ paddingBottom: paddingBottom + bottomInset, gap: drawerGap, flexDirection: 'column', alignItems: 'stretch' }, contentContainerStyle]}
|
|
332
340
|
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
|
333
|
-
|
|
334
|
-
alwaysBounceVertical={
|
|
341
|
+
animatedProps={animatedScrollProps}
|
|
342
|
+
alwaysBounceVertical={false}
|
|
335
343
|
overScrollMode="always"
|
|
336
344
|
onScroll={useAnimatedScrollHandler((event) => {
|
|
337
345
|
scrollY.value = event.contentOffset.y
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as RadioButtonStories from './RadioButton.stories';
|
|
3
|
+
import { RadioButton } from './RadioButton';
|
|
4
|
+
|
|
5
|
+
<Meta of={RadioButtonStories} />
|
|
6
|
+
|
|
7
|
+
## Available Collections and Modes
|
|
8
|
+
|
|
9
|
+
This component uses the following design token collections. Each collection supports multiple modes that can be configured via the `modes` prop.
|
|
10
|
+
|
|
11
|
+
### Color Mode
|
|
12
|
+
- **Modes:** Light | Dark
|
|
13
|
+
- **Default:** Light
|
|
14
|
+
|
|
15
|
+
### Colors Router
|
|
16
|
+
- **Modes:** POC | Old
|
|
17
|
+
- **Default:** POC
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
# RadioButton
|
|
22
|
+
|
|
23
|
+
A standard RadioButton component. This component is used to select a single option from a set of options.
|
|
24
|
+
|
|
25
|
+
## Default
|
|
26
|
+
|
|
27
|
+
<Canvas of={RadioButtonStories.Default} />
|
|
28
|
+
|
|
29
|
+
<Controls of={RadioButtonStories.Default} />
|
|
30
|
+
|
|
31
|
+
## States
|
|
32
|
+
|
|
33
|
+
<Canvas of={RadioButtonStories.AllStates} />
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import React, { useState } from 'react';
|
|
38
|
+
import { RadioButton } from 'jfs-components';
|
|
39
|
+
import { View, Text } from 'react-native';
|
|
40
|
+
|
|
41
|
+
export default function App() {
|
|
42
|
+
const [selected, setSelected] = useState(false);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<RadioButton
|
|
46
|
+
selected={selected}
|
|
47
|
+
onPress={() => setSelected(!selected)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Design Tokens
|
|
55
|
+
|
|
56
|
+
This component uses the following design tokens, resolved through `getVariableByName`:
|
|
57
|
+
|
|
58
|
+
- **`radio/background/color`**
|
|
59
|
+
- **`radio/disabled/background/color`**
|
|
60
|
+
- **`radio/disabled/border/color`**
|
|
61
|
+
- **`radio/disabled/radio/disabled/border/size`**
|
|
62
|
+
- **`radio/disabledSelected/background`**
|
|
63
|
+
- **`radio/disabledSelected/border/color`**
|
|
64
|
+
- **`radio/disabledSelected/selector/background/color`**
|
|
65
|
+
- **`radio/focus/background/color`**
|
|
66
|
+
- **`radio/focus/border/color`**
|
|
67
|
+
- **`radio/focus/border/size`**
|
|
68
|
+
- **`radio/focus/boxShadow/size`**
|
|
69
|
+
- **`radio/focus/shadow/color`**
|
|
70
|
+
- **`radio/focusSelected/background/color`**
|
|
71
|
+
- **`radio/focusSelected/border/size`**
|
|
72
|
+
- **`radio/focusSelected/selector/background/color`**
|
|
73
|
+
- **`radio/height`**
|
|
74
|
+
- **`radio/hover/background/color`**
|
|
75
|
+
- **`radio/hover/border/color`**
|
|
76
|
+
- **`radio/hover/boxShadow/size`**
|
|
77
|
+
- **`radio/hover/shadow/color`**
|
|
78
|
+
- **`radio/hoverSelected/background/color`**
|
|
79
|
+
- **`radio/hoverSelected/border/color`**
|
|
80
|
+
- **`radio/hoverSelected/boxShadow/size`**
|
|
81
|
+
- **`radio/hoverSelected/selector/background/color`**
|
|
82
|
+
- **`radio/hoverSelected/shadow/color`**
|
|
83
|
+
- **`radio/idle/background/color`**
|
|
84
|
+
- **`radio/idle/border/color`**
|
|
85
|
+
- **`radio/selected/background/color`**
|
|
86
|
+
- **`radio/selected/border/color`**
|
|
87
|
+
- **`radio/selected/selector/background/color`**
|
|
88
|
+
- **`radio/selector/size`**
|
|
89
|
+
- **`radio/width`**
|
|
90
|
+
|
|
91
|
+
All tokens support mode-based theming through the `modes` prop.
|
|
92
|
+
|