overlapping-cards-scroll 0.1.0 → 0.1.2
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/README.md +16 -0
- package/dist/react-native-web.cjs +22 -4
- package/dist/react-native-web.js +21 -3
- package/dist/react-native.cjs +257 -30
- package/dist/react-native.js +258 -30
- package/dist/types/rn/OverlappingCardsScrollRN.native.d.ts +4 -26
- package/dist/types/rn/OverlappingCardsScrollRN.types.d.ts +74 -0
- package/dist/types/rn/OverlappingCardsScrollRN.web.d.ts +4 -18
- package/package.json +8 -3
- package/src/lib/OverlappingCardsScroll.css +206 -0
- package/src/lib/OverlappingCardsScroll.tsx +943 -0
- package/src/lib/index.ts +10 -0
- package/src/rn/OverlappingCardsScrollRN.native.tsx +868 -0
- package/src/rn/OverlappingCardsScrollRN.types.ts +102 -0
- package/src/rn/OverlappingCardsScrollRN.web.tsx +90 -0
- package/src/rn/RNWebDemo.tsx +241 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ComponentProps, ComponentType, ReactNode } from 'react'
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native'
|
|
3
|
+
import type {
|
|
4
|
+
CardItem as OverlappingCardsScrollWebCardItem,
|
|
5
|
+
OverlappingCardsScroll,
|
|
6
|
+
OverlappingCardsScrollFocusTriggerProps as OverlappingCardsScrollWebFocusTriggerProps,
|
|
7
|
+
} from '../lib/OverlappingCardsScroll'
|
|
8
|
+
|
|
9
|
+
type OverlappingCardsScrollWebProps = ComponentProps<typeof OverlappingCardsScroll>
|
|
10
|
+
|
|
11
|
+
type OverlappingCardsScrollRNSharedProps = Omit<
|
|
12
|
+
OverlappingCardsScrollWebProps,
|
|
13
|
+
'children' | 'items' | 'cardContainerStyle' | 'tabsComponent' | 'tabsContainerComponent'
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type OverlappingCardsScrollRNPageDotsPosition = NonNullable<
|
|
17
|
+
OverlappingCardsScrollWebProps['pageDotsPosition']
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
export type OverlappingCardsScrollRNFocusTriggerBehavior =
|
|
21
|
+
OverlappingCardsScrollWebFocusTriggerProps['behavior']
|
|
22
|
+
|
|
23
|
+
export type OverlappingCardsScrollRNFocusTransitionMode = NonNullable<
|
|
24
|
+
OverlappingCardsScrollWebFocusTriggerProps['transitionMode']
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
export type OverlappingCardsScrollRNSnapDecelerationRate = 'normal' | 'fast' | number
|
|
28
|
+
|
|
29
|
+
export type OverlappingCardsScrollRNItem = OverlappingCardsScrollWebCardItem
|
|
30
|
+
|
|
31
|
+
export type OverlappingCardsScrollRNTabsPosition = 'above' | 'below'
|
|
32
|
+
|
|
33
|
+
export interface OverlappingCardsScrollRNTabProps {
|
|
34
|
+
name: string
|
|
35
|
+
index: number
|
|
36
|
+
position: OverlappingCardsScrollRNTabsPosition
|
|
37
|
+
isPrincipal: boolean
|
|
38
|
+
influence: number
|
|
39
|
+
animate: {
|
|
40
|
+
opacity: number
|
|
41
|
+
}
|
|
42
|
+
className: string
|
|
43
|
+
style: StyleProp<ViewStyle>
|
|
44
|
+
textStyle: StyleProp<TextStyle>
|
|
45
|
+
ariaLabel: string
|
|
46
|
+
ariaCurrent?: 'page'
|
|
47
|
+
accessibilityLabel: string
|
|
48
|
+
accessibilityState?: {
|
|
49
|
+
selected?: boolean
|
|
50
|
+
}
|
|
51
|
+
onPress: () => void
|
|
52
|
+
onClick: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OverlappingCardsScrollRNTabsContainerProps {
|
|
56
|
+
children: ReactNode
|
|
57
|
+
position: OverlappingCardsScrollRNTabsPosition
|
|
58
|
+
className: string
|
|
59
|
+
style: StyleProp<ViewStyle>
|
|
60
|
+
ariaLabel: string
|
|
61
|
+
cardNames: string[]
|
|
62
|
+
activeIndex: number
|
|
63
|
+
progress: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type OverlappingCardsScrollRNWithChildren = OverlappingCardsScrollRNSharedProps & {
|
|
67
|
+
children: ReactNode
|
|
68
|
+
items?: never
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type OverlappingCardsScrollRNWithItems = OverlappingCardsScrollRNSharedProps & {
|
|
72
|
+
items: OverlappingCardsScrollRNItem[]
|
|
73
|
+
children?: never
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type OverlappingCardsScrollRNContentProps =
|
|
77
|
+
| OverlappingCardsScrollRNWithChildren
|
|
78
|
+
| OverlappingCardsScrollRNWithItems
|
|
79
|
+
|
|
80
|
+
export type OverlappingCardsScrollRNProps = OverlappingCardsScrollRNContentProps & {
|
|
81
|
+
style?: StyleProp<ViewStyle>
|
|
82
|
+
cardContainerStyle?: StyleProp<ViewStyle>
|
|
83
|
+
tabsComponent?: ComponentType<OverlappingCardsScrollRNTabProps>
|
|
84
|
+
tabsContainerComponent?: ComponentType<OverlappingCardsScrollRNTabsContainerProps>
|
|
85
|
+
showsHorizontalScrollIndicator?: boolean
|
|
86
|
+
snapDecelerationRate?: OverlappingCardsScrollRNSnapDecelerationRate
|
|
87
|
+
snapDisableIntervalMomentum?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface OverlappingCardsScrollRNFocusTriggerProps {
|
|
91
|
+
children?: ReactNode
|
|
92
|
+
className?: string
|
|
93
|
+
style?: StyleProp<ViewStyle>
|
|
94
|
+
textStyle?: StyleProp<TextStyle>
|
|
95
|
+
behavior?: OverlappingCardsScrollRNFocusTriggerBehavior
|
|
96
|
+
transitionMode?: OverlappingCardsScrollRNFocusTransitionMode
|
|
97
|
+
disabled?: boolean
|
|
98
|
+
accessibilityLabel?: string
|
|
99
|
+
testID?: string
|
|
100
|
+
onPress?: (event: unknown) => void
|
|
101
|
+
onClick?: (event: unknown) => void
|
|
102
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react'
|
|
2
|
+
import { StyleSheet, View } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
OverlappingCardsScroll,
|
|
5
|
+
OverlappingCardsScrollFocusTrigger,
|
|
6
|
+
} from '../lib/OverlappingCardsScroll'
|
|
7
|
+
import type {
|
|
8
|
+
OverlappingCardsScrollRNFocusTriggerProps,
|
|
9
|
+
OverlappingCardsScrollRNProps,
|
|
10
|
+
} from './OverlappingCardsScrollRN.types'
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
OverlappingCardsScrollRNFocusTransitionMode,
|
|
14
|
+
OverlappingCardsScrollRNFocusTriggerBehavior,
|
|
15
|
+
OverlappingCardsScrollRNFocusTriggerProps,
|
|
16
|
+
OverlappingCardsScrollRNItem,
|
|
17
|
+
OverlappingCardsScrollRNPageDotsPosition,
|
|
18
|
+
OverlappingCardsScrollRNProps,
|
|
19
|
+
OverlappingCardsScrollRNSnapDecelerationRate,
|
|
20
|
+
OverlappingCardsScrollRNTabsContainerProps,
|
|
21
|
+
OverlappingCardsScrollRNTabProps,
|
|
22
|
+
OverlappingCardsScrollRNTabsPosition,
|
|
23
|
+
} from './OverlappingCardsScrollRN.types'
|
|
24
|
+
|
|
25
|
+
export function OverlappingCardsScrollRNFocusTrigger({
|
|
26
|
+
children = 'Make principal',
|
|
27
|
+
className = '',
|
|
28
|
+
style = undefined,
|
|
29
|
+
textStyle = undefined,
|
|
30
|
+
behavior = 'smooth',
|
|
31
|
+
transitionMode = 'swoop',
|
|
32
|
+
disabled = false,
|
|
33
|
+
accessibilityLabel = undefined,
|
|
34
|
+
testID = undefined,
|
|
35
|
+
onPress = undefined,
|
|
36
|
+
onClick = undefined,
|
|
37
|
+
...buttonProps
|
|
38
|
+
}: OverlappingCardsScrollRNFocusTriggerProps) {
|
|
39
|
+
void style
|
|
40
|
+
void textStyle
|
|
41
|
+
|
|
42
|
+
const handleClick = (event: unknown) => {
|
|
43
|
+
onClick?.(event)
|
|
44
|
+
onPress?.(event)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<OverlappingCardsScrollFocusTrigger
|
|
49
|
+
className={className}
|
|
50
|
+
behavior={behavior}
|
|
51
|
+
transitionMode={transitionMode}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
aria-label={accessibilityLabel}
|
|
54
|
+
data-testid={testID}
|
|
55
|
+
onClick={handleClick}
|
|
56
|
+
{...buttonProps}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</OverlappingCardsScrollFocusTrigger>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function OverlappingCardsScrollRN({
|
|
64
|
+
style = undefined,
|
|
65
|
+
showsHorizontalScrollIndicator = true,
|
|
66
|
+
snapDecelerationRate = 'normal',
|
|
67
|
+
snapDisableIntervalMomentum = false,
|
|
68
|
+
...overlappingCardsScrollProps
|
|
69
|
+
}: OverlappingCardsScrollRNProps) {
|
|
70
|
+
void showsHorizontalScrollIndicator
|
|
71
|
+
void snapDecelerationRate
|
|
72
|
+
void snapDisableIntervalMomentum
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={[styles.root, style]}>
|
|
76
|
+
<OverlappingCardsScroll
|
|
77
|
+
{...(overlappingCardsScrollProps as ComponentProps<typeof OverlappingCardsScroll>)}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
root: {
|
|
85
|
+
width: '100%',
|
|
86
|
+
minWidth: 0,
|
|
87
|
+
flex: 1,
|
|
88
|
+
minHeight: 0,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
OverlappingCardsScrollRN,
|
|
5
|
+
OverlappingCardsScrollRNFocusTrigger,
|
|
6
|
+
} from './OverlappingCardsScrollRN.web'
|
|
7
|
+
import type {
|
|
8
|
+
OverlappingCardsScrollRNTabProps,
|
|
9
|
+
OverlappingCardsScrollRNTabsContainerProps,
|
|
10
|
+
} from './OverlappingCardsScrollRN.web'
|
|
11
|
+
|
|
12
|
+
const RN_DEMO_CARDS = [
|
|
13
|
+
{
|
|
14
|
+
id: 'rn-1',
|
|
15
|
+
tag: 'Card 01',
|
|
16
|
+
title: 'Native Layout',
|
|
17
|
+
body: 'Card width is 1/3 of the viewport measured from onLayout.',
|
|
18
|
+
color: '#0d9488',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'rn-2',
|
|
22
|
+
tag: 'Card 02',
|
|
23
|
+
title: 'Stacked Ordering',
|
|
24
|
+
body: 'Higher index cards remain above lower index cards in the stack.',
|
|
25
|
+
color: '#f97316',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'rn-3',
|
|
29
|
+
tag: 'Card 03',
|
|
30
|
+
title: 'Scroll Driven',
|
|
31
|
+
body: 'ScrollView offset drives a single incoming card transition each step.',
|
|
32
|
+
color: '#2563eb',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'rn-4',
|
|
36
|
+
tag: 'Card 04',
|
|
37
|
+
title: 'Expo Ready',
|
|
38
|
+
body: 'The component relies only on react-native primitives for portability.',
|
|
39
|
+
color: '#7c3aed',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'rn-5',
|
|
43
|
+
tag: 'Card 05',
|
|
44
|
+
title: 'Fanned Edges',
|
|
45
|
+
body: 'All cards keep a visible leading edge while focus transitions right.',
|
|
46
|
+
color: '#be123c',
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function RNCard({ tag, title, body, color }) {
|
|
51
|
+
const [clickCount, setClickCount] = useState(0)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={styles.card}>
|
|
55
|
+
<View style={[styles.bar, { backgroundColor: color }]} />
|
|
56
|
+
<Text style={styles.tag}>{tag}</Text>
|
|
57
|
+
<Pressable
|
|
58
|
+
style={styles.counter}
|
|
59
|
+
onPress={() => setClickCount((count) => count + 1)}
|
|
60
|
+
>
|
|
61
|
+
<Text style={styles.counterText}>Clicks: {clickCount}</Text>
|
|
62
|
+
</Pressable>
|
|
63
|
+
<OverlappingCardsScrollRNFocusTrigger>Make principal</OverlappingCardsScrollRNFocusTrigger>
|
|
64
|
+
<Text style={styles.title}>{title}</Text>
|
|
65
|
+
<Text style={styles.body}>{body}</Text>
|
|
66
|
+
</View>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function RNWebTabsContainer({
|
|
71
|
+
children,
|
|
72
|
+
style,
|
|
73
|
+
ariaLabel,
|
|
74
|
+
}: OverlappingCardsScrollRNTabsContainerProps) {
|
|
75
|
+
return (
|
|
76
|
+
<View style={[styles.tabsContainer, style]} accessibilityRole="tablist" accessibilityLabel={ariaLabel}>
|
|
77
|
+
{children}
|
|
78
|
+
</View>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function RNWebTab({
|
|
83
|
+
name,
|
|
84
|
+
isPrincipal,
|
|
85
|
+
style,
|
|
86
|
+
textStyle,
|
|
87
|
+
accessibilityLabel,
|
|
88
|
+
accessibilityState,
|
|
89
|
+
onPress,
|
|
90
|
+
onClick,
|
|
91
|
+
}: OverlappingCardsScrollRNTabProps) {
|
|
92
|
+
const handlePress = () => {
|
|
93
|
+
onClick?.()
|
|
94
|
+
onPress?.()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Pressable
|
|
99
|
+
accessibilityRole="tab"
|
|
100
|
+
accessibilityLabel={accessibilityLabel}
|
|
101
|
+
accessibilityState={accessibilityState}
|
|
102
|
+
onPress={handlePress}
|
|
103
|
+
style={({ pressed }) => [
|
|
104
|
+
styles.tab,
|
|
105
|
+
isPrincipal && styles.tabActive,
|
|
106
|
+
pressed && styles.tabPressed,
|
|
107
|
+
style,
|
|
108
|
+
]}
|
|
109
|
+
>
|
|
110
|
+
<Text style={[styles.tabText, isPrincipal && styles.tabTextActive, textStyle]}>{name}</Text>
|
|
111
|
+
</Pressable>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function RNWebDemo() {
|
|
116
|
+
return (
|
|
117
|
+
<View style={styles.root}>
|
|
118
|
+
<Text style={styles.helperText}>React Native Web Prototype (View/Text/ScrollView)</Text>
|
|
119
|
+
<OverlappingCardsScrollRN
|
|
120
|
+
cardHeight={260}
|
|
121
|
+
basePeek={58}
|
|
122
|
+
showPageDots
|
|
123
|
+
pageDotsPosition="below"
|
|
124
|
+
pageDotsOffset={10}
|
|
125
|
+
showTabs
|
|
126
|
+
tabsPosition="above"
|
|
127
|
+
tabsOffset={10}
|
|
128
|
+
tabsComponent={RNWebTab}
|
|
129
|
+
tabsContainerComponent={RNWebTabsContainer}
|
|
130
|
+
cardContainerStyle={styles.cardContainer}
|
|
131
|
+
showsHorizontalScrollIndicator
|
|
132
|
+
items={RN_DEMO_CARDS.map((card) => ({
|
|
133
|
+
id: card.id,
|
|
134
|
+
name: card.title,
|
|
135
|
+
jsx: <RNCard {...card} />,
|
|
136
|
+
}))}
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const styles = StyleSheet.create({
|
|
143
|
+
root: {
|
|
144
|
+
width: '100%',
|
|
145
|
+
},
|
|
146
|
+
helperText: {
|
|
147
|
+
color: '#2f4d65',
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
fontWeight: '600',
|
|
150
|
+
marginBottom: 10,
|
|
151
|
+
},
|
|
152
|
+
tabsContainer: {
|
|
153
|
+
flexDirection: 'row',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
flexWrap: 'wrap',
|
|
156
|
+
},
|
|
157
|
+
tab: {
|
|
158
|
+
borderRadius: 999,
|
|
159
|
+
borderWidth: 1,
|
|
160
|
+
borderColor: 'rgba(31, 70, 102, 0.26)',
|
|
161
|
+
backgroundColor: '#eef6ff',
|
|
162
|
+
paddingHorizontal: 10,
|
|
163
|
+
paddingVertical: 5,
|
|
164
|
+
marginHorizontal: 4,
|
|
165
|
+
marginVertical: 4,
|
|
166
|
+
},
|
|
167
|
+
tabActive: {
|
|
168
|
+
backgroundColor: '#1f4666',
|
|
169
|
+
borderColor: '#1f4666',
|
|
170
|
+
},
|
|
171
|
+
tabPressed: {
|
|
172
|
+
opacity: 0.82,
|
|
173
|
+
},
|
|
174
|
+
tabText: {
|
|
175
|
+
color: '#1f4666',
|
|
176
|
+
fontSize: 12,
|
|
177
|
+
fontWeight: '700',
|
|
178
|
+
},
|
|
179
|
+
tabTextActive: {
|
|
180
|
+
color: '#f4f9ff',
|
|
181
|
+
},
|
|
182
|
+
cardContainer: {
|
|
183
|
+
borderRadius: 18,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
},
|
|
186
|
+
card: {
|
|
187
|
+
flex: 1,
|
|
188
|
+
borderRadius: 16,
|
|
189
|
+
borderWidth: 1,
|
|
190
|
+
borderColor: 'rgba(17, 43, 69, 0.13)',
|
|
191
|
+
backgroundColor: '#ffffff',
|
|
192
|
+
padding: 12,
|
|
193
|
+
shadowColor: '#102841',
|
|
194
|
+
shadowOpacity: 0.16,
|
|
195
|
+
shadowRadius: 12,
|
|
196
|
+
shadowOffset: { width: 0, height: 8 },
|
|
197
|
+
elevation: 3,
|
|
198
|
+
},
|
|
199
|
+
bar: {
|
|
200
|
+
width: '100%',
|
|
201
|
+
height: 5,
|
|
202
|
+
borderRadius: 99,
|
|
203
|
+
marginBottom: 8,
|
|
204
|
+
},
|
|
205
|
+
tag: {
|
|
206
|
+
fontSize: 11,
|
|
207
|
+
letterSpacing: 2,
|
|
208
|
+
textTransform: 'uppercase',
|
|
209
|
+
color: '#4a6b84',
|
|
210
|
+
fontWeight: '700',
|
|
211
|
+
marginBottom: 4,
|
|
212
|
+
},
|
|
213
|
+
counter: {
|
|
214
|
+
alignSelf: 'flex-start',
|
|
215
|
+
borderRadius: 99,
|
|
216
|
+
borderWidth: 1,
|
|
217
|
+
borderColor: 'rgba(30, 67, 99, 0.25)',
|
|
218
|
+
backgroundColor: '#f3f8ff',
|
|
219
|
+
paddingHorizontal: 10,
|
|
220
|
+
paddingVertical: 5,
|
|
221
|
+
marginBottom: 6,
|
|
222
|
+
},
|
|
223
|
+
counterText: {
|
|
224
|
+
color: '#1f4666',
|
|
225
|
+
fontSize: 12,
|
|
226
|
+
fontWeight: '700',
|
|
227
|
+
letterSpacing: 0.3,
|
|
228
|
+
},
|
|
229
|
+
title: {
|
|
230
|
+
color: '#173047',
|
|
231
|
+
fontSize: 24,
|
|
232
|
+
lineHeight: 28,
|
|
233
|
+
fontWeight: '700',
|
|
234
|
+
marginBottom: 6,
|
|
235
|
+
},
|
|
236
|
+
body: {
|
|
237
|
+
color: '#2f4d65',
|
|
238
|
+
fontSize: 16,
|
|
239
|
+
lineHeight: 23,
|
|
240
|
+
},
|
|
241
|
+
})
|