@butternutbox/pawprint-native 0.0.1 → 0.1.0
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/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +16 -13
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { StyleSheet, View } from "react-native"
|
|
3
|
+
import { Typography } from "../../atoms/Typography"
|
|
4
|
+
import { Button } from "../../atoms/Button"
|
|
5
|
+
import { Animated, type AnimatedProps } from "./Animated"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "Molecules/Animated",
|
|
9
|
+
component: Animated,
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"Reanimated wrapper that applies a named animation preset to its children. Exit animations run automatically on unmount — no AnimatePresence wrapper needed."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
variant: {
|
|
20
|
+
control: { type: "select" },
|
|
21
|
+
options: ["fade", "fadeUp", "fadeDown", "scale", "slideIn", "slideOut"],
|
|
22
|
+
description: "Animation preset"
|
|
23
|
+
},
|
|
24
|
+
delay: {
|
|
25
|
+
control: { type: "number", min: 0, max: 2, step: 0.05 },
|
|
26
|
+
description: "Delay before entering animation starts (seconds)"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Card = ({ label }: { label: string }) => (
|
|
32
|
+
<View style={styles.card}>
|
|
33
|
+
<Typography variant="heading" size="xs">
|
|
34
|
+
{label}
|
|
35
|
+
</Typography>
|
|
36
|
+
<Typography>Toggle the button to see enter and exit.</Typography>
|
|
37
|
+
</View>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
function ToggleStory({
|
|
41
|
+
variant,
|
|
42
|
+
label
|
|
43
|
+
}: {
|
|
44
|
+
variant: AnimatedProps["variant"]
|
|
45
|
+
label: string
|
|
46
|
+
}) {
|
|
47
|
+
const [visible, setVisible] = useState(true)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.toggleContainer}>
|
|
51
|
+
<Button
|
|
52
|
+
variant="filled"
|
|
53
|
+
colour="primary"
|
|
54
|
+
onPress={() => setVisible((v) => !v)}
|
|
55
|
+
>
|
|
56
|
+
{visible ? "Hide" : "Show"}
|
|
57
|
+
</Button>
|
|
58
|
+
{visible && (
|
|
59
|
+
<Animated variant={variant}>
|
|
60
|
+
<Card label={label} />
|
|
61
|
+
</Animated>
|
|
62
|
+
)}
|
|
63
|
+
</View>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Playground = (args: AnimatedProps) => (
|
|
68
|
+
<View style={styles.container}>
|
|
69
|
+
<Animated {...args}>
|
|
70
|
+
<Card label={args.variant ?? "fade"} />
|
|
71
|
+
</Animated>
|
|
72
|
+
</View>
|
|
73
|
+
)
|
|
74
|
+
Playground.args = {
|
|
75
|
+
variant: "fadeUp",
|
|
76
|
+
delay: 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const Fade = () => (
|
|
80
|
+
<View style={styles.container}>
|
|
81
|
+
<ToggleStory variant="fade" label="Fade" />
|
|
82
|
+
</View>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
export const FadeUp = () => (
|
|
86
|
+
<View style={styles.container}>
|
|
87
|
+
<ToggleStory variant="fadeUp" label="Fade up" />
|
|
88
|
+
</View>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
export const FadeDown = () => (
|
|
92
|
+
<View style={styles.container}>
|
|
93
|
+
<ToggleStory variant="fadeDown" label="Fade down" />
|
|
94
|
+
</View>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export const Scale = () => (
|
|
98
|
+
<View style={styles.container}>
|
|
99
|
+
<ToggleStory variant="scale" label="Scale" />
|
|
100
|
+
</View>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
export const SlideIn = () => (
|
|
104
|
+
<View style={styles.container}>
|
|
105
|
+
<ToggleStory variant="slideIn" label="Slide in (from right)" />
|
|
106
|
+
</View>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
export const SlideOut = () => (
|
|
110
|
+
<View style={styles.container}>
|
|
111
|
+
<ToggleStory variant="slideOut" label="Slide out (from left)" />
|
|
112
|
+
</View>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
export const Delay = () => {
|
|
116
|
+
const [visible, setVisible] = useState(true)
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View style={styles.container}>
|
|
120
|
+
<View style={styles.toggleContainer}>
|
|
121
|
+
<Button
|
|
122
|
+
variant="filled"
|
|
123
|
+
colour="primary"
|
|
124
|
+
onPress={() => setVisible((v) => !v)}
|
|
125
|
+
>
|
|
126
|
+
{visible ? "Hide" : "Show"}
|
|
127
|
+
</Button>
|
|
128
|
+
{visible && (
|
|
129
|
+
<Animated variant="scale" delay={0.6}>
|
|
130
|
+
<Card label="Appears after 0.6s delay" />
|
|
131
|
+
</Animated>
|
|
132
|
+
)}
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const StaggeredList = () => {
|
|
139
|
+
const [visible, setVisible] = useState(true)
|
|
140
|
+
|
|
141
|
+
const items = [
|
|
142
|
+
"Fresh chicken recipe",
|
|
143
|
+
"Salmon & sweet potato",
|
|
144
|
+
"Turkey & vegetables",
|
|
145
|
+
"Beef & brown rice"
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<View style={styles.container}>
|
|
150
|
+
<Button
|
|
151
|
+
variant="filled"
|
|
152
|
+
colour="primary"
|
|
153
|
+
onPress={() => setVisible((v) => !v)}
|
|
154
|
+
style={styles.staggerButton}
|
|
155
|
+
>
|
|
156
|
+
{visible ? "Hide list" : "Show list"}
|
|
157
|
+
</Button>
|
|
158
|
+
<View style={styles.list}>
|
|
159
|
+
{visible &&
|
|
160
|
+
items.map((item, i) => (
|
|
161
|
+
<Animated key={item} variant="fadeUp" delay={i * 0.08}>
|
|
162
|
+
<View style={styles.row}>
|
|
163
|
+
<Typography>{item}</Typography>
|
|
164
|
+
</View>
|
|
165
|
+
</Animated>
|
|
166
|
+
))}
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const AllVariants = () => {
|
|
173
|
+
const [visible, setVisible] = useState(true)
|
|
174
|
+
|
|
175
|
+
const variants: { key: AnimatedProps["variant"]; label: string }[] = [
|
|
176
|
+
{ key: "fade", label: "Fade" },
|
|
177
|
+
{ key: "fadeUp", label: "Fade up" },
|
|
178
|
+
{ key: "fadeDown", label: "Fade down" },
|
|
179
|
+
{ key: "scale", label: "Scale" },
|
|
180
|
+
{ key: "slideIn", label: "Slide in" },
|
|
181
|
+
{ key: "slideOut", label: "Slide out" }
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<View style={styles.container}>
|
|
186
|
+
<Button
|
|
187
|
+
variant="filled"
|
|
188
|
+
colour="primary"
|
|
189
|
+
onPress={() => setVisible((v) => !v)}
|
|
190
|
+
style={styles.staggerButton}
|
|
191
|
+
>
|
|
192
|
+
{visible ? "Hide all" : "Show all"}
|
|
193
|
+
</Button>
|
|
194
|
+
<View style={styles.grid}>
|
|
195
|
+
{variants.map(({ key, label }) =>
|
|
196
|
+
visible ? (
|
|
197
|
+
<Animated key={key} variant={key}>
|
|
198
|
+
<View style={styles.chip}>
|
|
199
|
+
<Typography size="sm">{label}</Typography>
|
|
200
|
+
</View>
|
|
201
|
+
</Animated>
|
|
202
|
+
) : null
|
|
203
|
+
)}
|
|
204
|
+
</View>
|
|
205
|
+
</View>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const styles = StyleSheet.create({
|
|
210
|
+
container: {
|
|
211
|
+
padding: 16
|
|
212
|
+
},
|
|
213
|
+
toggleContainer: {
|
|
214
|
+
gap: 24,
|
|
215
|
+
alignItems: "center"
|
|
216
|
+
},
|
|
217
|
+
card: {
|
|
218
|
+
padding: 24,
|
|
219
|
+
borderRadius: 12,
|
|
220
|
+
backgroundColor: "#fff6d8",
|
|
221
|
+
borderWidth: 1,
|
|
222
|
+
borderColor: "#e8d9b0",
|
|
223
|
+
width: 280,
|
|
224
|
+
gap: 8
|
|
225
|
+
},
|
|
226
|
+
row: {
|
|
227
|
+
padding: 12,
|
|
228
|
+
borderRadius: 8,
|
|
229
|
+
backgroundColor: "#fff6d8",
|
|
230
|
+
borderWidth: 1,
|
|
231
|
+
borderColor: "#e8d9b0",
|
|
232
|
+
marginBottom: 8
|
|
233
|
+
},
|
|
234
|
+
list: {
|
|
235
|
+
marginTop: 8
|
|
236
|
+
},
|
|
237
|
+
staggerButton: {
|
|
238
|
+
marginBottom: 16
|
|
239
|
+
},
|
|
240
|
+
grid: {
|
|
241
|
+
flexDirection: "row",
|
|
242
|
+
flexWrap: "wrap",
|
|
243
|
+
gap: 8
|
|
244
|
+
},
|
|
245
|
+
chip: {
|
|
246
|
+
padding: 12,
|
|
247
|
+
borderRadius: 8,
|
|
248
|
+
backgroundColor: "#fff6d8",
|
|
249
|
+
borderWidth: 1,
|
|
250
|
+
borderColor: "#e8d9b0",
|
|
251
|
+
alignItems: "center",
|
|
252
|
+
width: 100
|
|
253
|
+
}
|
|
254
|
+
})
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { type View, type ViewProps } from "react-native"
|
|
3
|
+
import Reanimated, {
|
|
4
|
+
Easing,
|
|
5
|
+
Keyframe,
|
|
6
|
+
FadeIn,
|
|
7
|
+
FadeOut,
|
|
8
|
+
FadeInUp,
|
|
9
|
+
FadeOutDown,
|
|
10
|
+
FadeInDown,
|
|
11
|
+
FadeOutUp,
|
|
12
|
+
FadeInRight,
|
|
13
|
+
FadeOutRight,
|
|
14
|
+
FadeInLeft,
|
|
15
|
+
FadeOutLeft,
|
|
16
|
+
ComplexAnimationBuilder,
|
|
17
|
+
BaseAnimationBuilder,
|
|
18
|
+
type EntryExitAnimationFunction
|
|
19
|
+
} from "react-native-reanimated"
|
|
20
|
+
|
|
21
|
+
// ─── Custom scale entering builder ───────────────────────────────────────────
|
|
22
|
+
// Entering: presetName = "ZoomIn" → built-in CSS on web (scale 0→1, no opacity).
|
|
23
|
+
// Not using withInitialValues() intentionally: patching the 0% frame creates a
|
|
24
|
+
// custom REA* keyframe, which triggers Reanimated's scheduleAnimationCleanup →
|
|
25
|
+
// setElementPosition (position:absolute) on the live element → breaks flex flow.
|
|
26
|
+
// Native gets the subtle 0.85→1 + opacity via the worklet in build() instead.
|
|
27
|
+
// Exiting uses a Keyframe (subtleScaleOut below) for precise 0.85 CSS targeting.
|
|
28
|
+
|
|
29
|
+
class SubtleScaleIn extends ComplexAnimationBuilder {
|
|
30
|
+
// presetName maps to the ZoomIn CSS keyframe on web (scale 0→1).
|
|
31
|
+
// The worklet in build() drives native with scale 0.85→1 + opacity 0→1.
|
|
32
|
+
static presetName = "ZoomIn"
|
|
33
|
+
|
|
34
|
+
static createInstance<T extends typeof BaseAnimationBuilder>(
|
|
35
|
+
this: T
|
|
36
|
+
): InstanceType<T> {
|
|
37
|
+
return new SubtleScaleIn() as unknown as InstanceType<T>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
build = (): EntryExitAnimationFunction => {
|
|
41
|
+
const delayFunction = this.getDelayFunction()
|
|
42
|
+
const [animation, config] = this.getAnimationAndConfig()
|
|
43
|
+
const delay = this.getDelay()
|
|
44
|
+
const callback = this.callbackV
|
|
45
|
+
const initialValues = this.initialValues
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
"worklet"
|
|
49
|
+
return {
|
|
50
|
+
animations: {
|
|
51
|
+
opacity: delayFunction(delay, animation(1, config)),
|
|
52
|
+
transform: [{ scale: delayFunction(delay, animation(1, config)) }]
|
|
53
|
+
},
|
|
54
|
+
initialValues: {
|
|
55
|
+
opacity: 0,
|
|
56
|
+
transform: [{ scale: 0.85 }],
|
|
57
|
+
...initialValues
|
|
58
|
+
},
|
|
59
|
+
callback
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Preset map ──────────────────────────────────────────────────────────────
|
|
66
|
+
// Each preset bundles an entering and exiting builder class.
|
|
67
|
+
// Exported so consumers can apply them directly on Reanimated.View for
|
|
68
|
+
// stagger patterns without using the Animated component wrapper.
|
|
69
|
+
//
|
|
70
|
+
// Usage:
|
|
71
|
+
// <Reanimated.View entering={fadeUpAnimation.entering.duration(200)} />
|
|
72
|
+
// <Reanimated.View entering={fadeUpAnimation.entering.duration(200).delay(80)} />
|
|
73
|
+
|
|
74
|
+
export const fadeAnimation = { entering: FadeIn, exiting: FadeOut } as const
|
|
75
|
+
|
|
76
|
+
// fadeUp: starts below, moves upward — matches web Motion convention where
|
|
77
|
+
// the name describes direction of movement, not direction of origin.
|
|
78
|
+
export const fadeUpAnimation = {
|
|
79
|
+
entering: FadeInDown,
|
|
80
|
+
exiting: FadeOutDown
|
|
81
|
+
} as const
|
|
82
|
+
|
|
83
|
+
// fadeDown: starts above, moves downward — same convention as above.
|
|
84
|
+
export const fadeDownAnimation = {
|
|
85
|
+
entering: FadeInUp,
|
|
86
|
+
exiting: FadeOutUp
|
|
87
|
+
} as const
|
|
88
|
+
|
|
89
|
+
// slideIn: enters from the right — use for new content arriving
|
|
90
|
+
export const slideInAnimation = {
|
|
91
|
+
entering: FadeInRight,
|
|
92
|
+
exiting: FadeOutRight
|
|
93
|
+
} as const
|
|
94
|
+
|
|
95
|
+
// slideOut: enters from the left — use for content returning (back navigation)
|
|
96
|
+
export const slideOutAnimation = {
|
|
97
|
+
entering: FadeInLeft,
|
|
98
|
+
exiting: FadeOutLeft
|
|
99
|
+
} as const
|
|
100
|
+
|
|
101
|
+
// ─── Internal lookups ────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const DURATIONS = {
|
|
104
|
+
fade: 200,
|
|
105
|
+
scale: 150,
|
|
106
|
+
fadeUp: 200,
|
|
107
|
+
fadeDown: 200,
|
|
108
|
+
slideIn: 250,
|
|
109
|
+
slideOut: 250
|
|
110
|
+
} as const
|
|
111
|
+
|
|
112
|
+
// Keyframe-based exit: scale 1→0.85 with opacity fade.
|
|
113
|
+
// Using Keyframe (instead of ComplexAnimationBuilder + ZoomOut presetName) lets
|
|
114
|
+
// Reanimated generate precise CSS @keyframes on web, so the exit targets
|
|
115
|
+
// scale 0.85 rather than the built-in ZoomOut's scale 0.
|
|
116
|
+
// easing at frame 100 = destination frame per Reanimated Keyframe convention;
|
|
117
|
+
// the web CSS generator moves it to the 0% frame automatically.
|
|
118
|
+
const subtleScaleOut = new Keyframe({
|
|
119
|
+
0: {
|
|
120
|
+
opacity: 1,
|
|
121
|
+
transform: [{ scale: 1 }]
|
|
122
|
+
},
|
|
123
|
+
100: {
|
|
124
|
+
opacity: 0,
|
|
125
|
+
transform: [{ scale: 0.85 }],
|
|
126
|
+
easing: Easing.out(Easing.quad)
|
|
127
|
+
}
|
|
128
|
+
}).duration(DURATIONS.scale)
|
|
129
|
+
|
|
130
|
+
// scale: subtle grow 0.85↔1 with opacity fade — matches web tooltip CSS transition.
|
|
131
|
+
// Note: exiting is a Keyframe instance (not a class). Use it directly or call
|
|
132
|
+
// .duration()/.delay() — those mutate the shared instance, so create a fresh
|
|
133
|
+
// Keyframe if you need independent configuration.
|
|
134
|
+
export const scaleAnimation = {
|
|
135
|
+
entering: SubtleScaleIn,
|
|
136
|
+
exiting: subtleScaleOut
|
|
137
|
+
} as const
|
|
138
|
+
|
|
139
|
+
export type AnimatedVariant = keyof typeof DURATIONS
|
|
140
|
+
|
|
141
|
+
// Entering classes — needed to build fresh instances when a delay is applied.
|
|
142
|
+
// ComplexAnimationBuilder instance methods mutate the object, so a pre-built
|
|
143
|
+
// shared instance must not have .delay() called on it directly.
|
|
144
|
+
const ENTERING_CLS = {
|
|
145
|
+
fade: FadeIn,
|
|
146
|
+
scale: SubtleScaleIn,
|
|
147
|
+
fadeUp: FadeInDown,
|
|
148
|
+
fadeDown: FadeInUp,
|
|
149
|
+
slideIn: FadeInRight,
|
|
150
|
+
slideOut: FadeInLeft
|
|
151
|
+
} as const
|
|
152
|
+
|
|
153
|
+
// ─── Pre-built animation instances ───────────────────────────────────────────
|
|
154
|
+
// Built once at module load. All Reanimated builders default to
|
|
155
|
+
// ReduceMotion.System, so OS reduce-motion is respected automatically.
|
|
156
|
+
// Directional entering variants use easeOut for a natural deceleration.
|
|
157
|
+
|
|
158
|
+
const easeOut = Easing.out(Easing.quad)
|
|
159
|
+
const easeInOut = Easing.inOut(Easing.quad)
|
|
160
|
+
|
|
161
|
+
const ENTERING = {
|
|
162
|
+
fade: FadeIn.duration(DURATIONS.fade),
|
|
163
|
+
scale: SubtleScaleIn.duration(DURATIONS.scale).easing(easeOut),
|
|
164
|
+
fadeUp: FadeInDown.duration(DURATIONS.fadeUp).easing(easeInOut),
|
|
165
|
+
fadeDown: FadeInUp.duration(DURATIONS.fadeDown).easing(easeInOut),
|
|
166
|
+
slideIn: FadeInRight.duration(DURATIONS.slideIn).easing(easeOut),
|
|
167
|
+
slideOut: FadeInLeft.duration(DURATIONS.slideOut).easing(easeOut)
|
|
168
|
+
} as const
|
|
169
|
+
|
|
170
|
+
const EXITING = {
|
|
171
|
+
fade: FadeOut.duration(DURATIONS.fade),
|
|
172
|
+
scale: subtleScaleOut,
|
|
173
|
+
fadeUp: FadeOutDown.duration(DURATIONS.fadeUp),
|
|
174
|
+
fadeDown: FadeOutUp.duration(DURATIONS.fadeDown),
|
|
175
|
+
slideIn: FadeOutRight.duration(DURATIONS.slideIn),
|
|
176
|
+
slideOut: FadeOutLeft.duration(DURATIONS.slideOut)
|
|
177
|
+
} as const
|
|
178
|
+
|
|
179
|
+
// Builds a fresh entering instance with delay. Must be a fresh instance because
|
|
180
|
+
// instance methods (.easing, .delay) mutate the object in place.
|
|
181
|
+
function buildDelayedEntering(variant: AnimatedVariant, delayMs: number) {
|
|
182
|
+
const b = (ENTERING_CLS[variant] as typeof ComplexAnimationBuilder).duration(
|
|
183
|
+
DURATIONS[variant]
|
|
184
|
+
)
|
|
185
|
+
if (variant === "fadeUp" || variant === "fadeDown") {
|
|
186
|
+
b.easing(easeInOut)
|
|
187
|
+
} else if (
|
|
188
|
+
variant === "slideIn" ||
|
|
189
|
+
variant === "slideOut" ||
|
|
190
|
+
variant === "scale"
|
|
191
|
+
) {
|
|
192
|
+
b.easing(easeOut)
|
|
193
|
+
}
|
|
194
|
+
b.delay(delayMs)
|
|
195
|
+
return b
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
export type AnimatedProps = {
|
|
201
|
+
/**
|
|
202
|
+
* Animation preset. Defaults to `"fade"`.
|
|
203
|
+
*
|
|
204
|
+
* | Preset | Motion |
|
|
205
|
+
* |-------------|-------------------------------------------|
|
|
206
|
+
* | `fade` | opacity |
|
|
207
|
+
* | `fadeUp` | opacity + enter from below |
|
|
208
|
+
* | `fadeDown` | opacity + enter from above |
|
|
209
|
+
* | `scale` | opacity + subtle scale 0.85↔1 |
|
|
210
|
+
* | `slideIn` | opacity + enter from right |
|
|
211
|
+
* | `slideOut` | opacity + enter from left |
|
|
212
|
+
*/
|
|
213
|
+
variant?: AnimatedVariant
|
|
214
|
+
/**
|
|
215
|
+
* Delay in seconds before the entering animation starts. Useful for
|
|
216
|
+
* sequencing elements without a full stagger setup. Ignored when
|
|
217
|
+
* `prefers-reduced-motion` is active.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```tsx
|
|
221
|
+
* <Animated variant="fadeUp" delay={0}>First</Animated>
|
|
222
|
+
* <Animated variant="fadeUp" delay={0.08}>Second</Animated>
|
|
223
|
+
* <Animated variant="fadeUp" delay={0.16}>Third</Animated>
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
delay?: number
|
|
227
|
+
} & ViewProps
|
|
228
|
+
|
|
229
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reanimated wrapper that applies a named animation preset to its children.
|
|
233
|
+
*
|
|
234
|
+
* Exit animations run automatically when the component unmounts — no wrapper
|
|
235
|
+
* component needed (unlike `AnimatePresence` on web).
|
|
236
|
+
*
|
|
237
|
+
* Respects the OS reduce-motion setting automatically — Reanimated builders
|
|
238
|
+
* default to `ReduceMotion.System` so no extra hook is needed.
|
|
239
|
+
*
|
|
240
|
+
* Export `*Animation` objects (e.g. `fadeUpAnimation`) for applying presets
|
|
241
|
+
* directly on `Reanimated.View` in custom stagger patterns:
|
|
242
|
+
*
|
|
243
|
+
* ```tsx
|
|
244
|
+
* <Reanimated.View entering={fadeUpAnimation.entering.duration(200).delay(80)}>
|
|
245
|
+
* <Item />
|
|
246
|
+
* </Reanimated.View>
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```tsx
|
|
251
|
+
* // Enter animation
|
|
252
|
+
* <Animated variant="fadeUp"><Card /></Animated>
|
|
253
|
+
*
|
|
254
|
+
* // Enter + exit — condition the render and Reanimated handles both
|
|
255
|
+
* {visible && <Animated variant="slideIn"><Panel /></Animated>}
|
|
256
|
+
*
|
|
257
|
+
* // Sequenced elements
|
|
258
|
+
* <Animated variant="fadeUp" delay={0}>Title</Animated>
|
|
259
|
+
* <Animated variant="fadeUp" delay={0.1}>Subtitle</Animated>
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export const Animated = React.forwardRef<View, AnimatedProps>(
|
|
263
|
+
({ variant = "fade", delay, children, ...props }, ref) => {
|
|
264
|
+
const delayMs =
|
|
265
|
+
delay !== undefined && delay > 0 ? Math.round(delay * 1000) : 0
|
|
266
|
+
|
|
267
|
+
const entering =
|
|
268
|
+
delayMs > 0 ? buildDelayedEntering(variant, delayMs) : ENTERING[variant]
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Reanimated.View
|
|
272
|
+
ref={ref}
|
|
273
|
+
entering={entering}
|
|
274
|
+
exiting={EXITING[variant]}
|
|
275
|
+
{...props}
|
|
276
|
+
>
|
|
277
|
+
{children}
|
|
278
|
+
</Reanimated.View>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
Animated.displayName = "Animated"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { screen } from "@testing-library/react"
|
|
3
|
+
import { describe, it, expect } from "vitest"
|
|
4
|
+
import { renderWithTheme } from "../../../test-utils"
|
|
5
|
+
import { ButtonDock } from "./ButtonDock"
|
|
6
|
+
|
|
7
|
+
describe("ButtonDock", () => {
|
|
8
|
+
describe("when component is rendering", () => {
|
|
9
|
+
it("renders children", () => {
|
|
10
|
+
renderWithTheme(
|
|
11
|
+
<ButtonDock>
|
|
12
|
+
<button>Continue</button>
|
|
13
|
+
</ButtonDock>
|
|
14
|
+
)
|
|
15
|
+
expect(screen.getByText("Continue")).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("renders with default stacked variant", () => {
|
|
19
|
+
renderWithTheme(
|
|
20
|
+
<ButtonDock>
|
|
21
|
+
<button>Primary</button>
|
|
22
|
+
<button>Secondary</button>
|
|
23
|
+
</ButtonDock>
|
|
24
|
+
)
|
|
25
|
+
expect(screen.getByText("Primary")).toBeInTheDocument()
|
|
26
|
+
expect(screen.getByText("Secondary")).toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe("when rendering variants", () => {
|
|
31
|
+
it("renders stacked variant", () => {
|
|
32
|
+
renderWithTheme(
|
|
33
|
+
<ButtonDock variant="stacked">
|
|
34
|
+
<button>Action</button>
|
|
35
|
+
</ButtonDock>
|
|
36
|
+
)
|
|
37
|
+
expect(screen.getByText("Action")).toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("renders inline variant", () => {
|
|
41
|
+
renderWithTheme(
|
|
42
|
+
<ButtonDock variant="inline">
|
|
43
|
+
<button>Action</button>
|
|
44
|
+
</ButtonDock>
|
|
45
|
+
)
|
|
46
|
+
expect(screen.getByText("Action")).toBeInTheDocument()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("when rendering with description", () => {
|
|
51
|
+
it("renders description in stacked variant", () => {
|
|
52
|
+
renderWithTheme(
|
|
53
|
+
<ButtonDock description="Helper text">
|
|
54
|
+
<button>Action</button>
|
|
55
|
+
</ButtonDock>
|
|
56
|
+
)
|
|
57
|
+
expect(screen.getByText("Helper text")).toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("when rendering with leading content", () => {
|
|
62
|
+
it("renders leading content in stacked variant", () => {
|
|
63
|
+
renderWithTheme(
|
|
64
|
+
<ButtonDock leadingContent={<span>Leading</span>}>
|
|
65
|
+
<button>Action</button>
|
|
66
|
+
</ButtonDock>
|
|
67
|
+
)
|
|
68
|
+
expect(screen.getByText("Leading")).toBeInTheDocument()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("when using with ref", () => {
|
|
73
|
+
it("forwards ref", () => {
|
|
74
|
+
const ref = React.createRef<any>()
|
|
75
|
+
renderWithTheme(
|
|
76
|
+
<ButtonDock ref={ref}>
|
|
77
|
+
<button>Action</button>
|
|
78
|
+
</ButtonDock>
|
|
79
|
+
)
|
|
80
|
+
expect(ref.current).toBeTruthy()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -27,10 +27,8 @@ export const Playground = {
|
|
|
27
27
|
},
|
|
28
28
|
render: (args: ButtonGroupProps) => (
|
|
29
29
|
<ButtonGroup {...args}>
|
|
30
|
-
<Button
|
|
31
|
-
<Button
|
|
32
|
-
Cancel
|
|
33
|
-
</Button>
|
|
30
|
+
<Button>Confirm</Button>
|
|
31
|
+
<Button variant="outlined">Cancel</Button>
|
|
34
32
|
</ButtonGroup>
|
|
35
33
|
)
|
|
36
34
|
}
|
|
@@ -38,10 +36,8 @@ export const Playground = {
|
|
|
38
36
|
export const Stacked = () => (
|
|
39
37
|
<View style={styles.column}>
|
|
40
38
|
<ButtonGroup layout="stacked" description="Two buttons stacked vertically">
|
|
41
|
-
<Button
|
|
42
|
-
<Button
|
|
43
|
-
Secondary action
|
|
44
|
-
</Button>
|
|
39
|
+
<Button>Primary action</Button>
|
|
40
|
+
<Button variant="outlined">Secondary action</Button>
|
|
45
41
|
</ButtonGroup>
|
|
46
42
|
</View>
|
|
47
43
|
)
|
|
@@ -49,8 +45,8 @@ export const Stacked = () => (
|
|
|
49
45
|
export const Inline = () => (
|
|
50
46
|
<View style={styles.column}>
|
|
51
47
|
<ButtonGroup layout="inline" description="Two buttons side by side">
|
|
48
|
+
<Button colour="secondary">Cancel</Button>
|
|
52
49
|
<Button>Confirm</Button>
|
|
53
|
-
<Button variant="outlined">Cancel</Button>
|
|
54
50
|
</ButtonGroup>
|
|
55
51
|
</View>
|
|
56
52
|
)
|
|
@@ -58,7 +54,7 @@ export const Inline = () => (
|
|
|
58
54
|
export const SingleButton = () => (
|
|
59
55
|
<View style={styles.column}>
|
|
60
56
|
<ButtonGroup layout="stacked" description="Just one button">
|
|
61
|
-
<Button
|
|
57
|
+
<Button>Continue</Button>
|
|
62
58
|
</ButtonGroup>
|
|
63
59
|
</View>
|
|
64
60
|
)
|
|
@@ -66,10 +62,8 @@ export const SingleButton = () => (
|
|
|
66
62
|
export const WithoutDescription = () => (
|
|
67
63
|
<View style={styles.column}>
|
|
68
64
|
<ButtonGroup layout="stacked">
|
|
69
|
-
<Button
|
|
70
|
-
<Button
|
|
71
|
-
Cancel
|
|
72
|
-
</Button>
|
|
65
|
+
<Button>Confirm</Button>
|
|
66
|
+
<Button variant="outlined">Cancel</Button>
|
|
73
67
|
</ButtonGroup>
|
|
74
68
|
</View>
|
|
75
69
|
)
|