@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.
Files changed (182) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +16 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12370 -1455
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1110 -11
  7. package/dist/index.d.ts +1110 -11
  8. package/dist/index.js +12324 -1455
  9. package/dist/index.js.map +1 -1
  10. package/package.json +28 -9
  11. package/src/__mocks__/asset-stub.ts +1 -0
  12. package/src/__mocks__/emotion-native.tsx +18 -0
  13. package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
  14. package/src/__mocks__/react-native-reanimated.tsx +79 -0
  15. package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
  16. package/src/__mocks__/react-native-svg.tsx +27 -0
  17. package/src/__mocks__/react-native-worklets.tsx +11 -0
  18. package/src/__mocks__/react-native.tsx +338 -0
  19. package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
  20. package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
  21. package/src/__mocks__/rn-primitives/select.tsx +116 -0
  22. package/src/__mocks__/rn-primitives/slider.tsx +40 -0
  23. package/src/__mocks__/rn-primitives/slot.tsx +30 -0
  24. package/src/__mocks__/rn-primitives/switch.tsx +24 -0
  25. package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
  26. package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
  27. package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
  28. package/src/components/atoms/Avatar/Avatar.tsx +68 -22
  29. package/src/components/atoms/Avatar/index.ts +1 -6
  30. package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
  31. package/src/components/atoms/Badge/Badge.test.tsx +90 -0
  32. package/src/components/atoms/Button/Button.test.tsx +123 -0
  33. package/src/components/atoms/Button/Button.tsx +1 -1
  34. package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
  35. package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
  36. package/src/components/atoms/CarouselControls/index.ts +2 -0
  37. package/src/components/atoms/Hint/Hint.test.tsx +36 -0
  38. package/src/components/atoms/Icon/Icon.test.tsx +98 -0
  39. package/src/components/atoms/Icon/Icon.tsx +5 -1
  40. package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
  41. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  42. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  43. package/src/components/atoms/Input/Input.test.tsx +306 -0
  44. package/src/components/atoms/Input/Input.tsx +9 -1
  45. package/src/components/atoms/Input/InputField.tsx +226 -74
  46. package/src/components/atoms/Link/Link.test.tsx +89 -0
  47. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  48. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  49. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  50. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  51. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  52. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  53. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  54. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  55. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  56. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  57. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  58. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  61. package/src/components/atoms/Logo/assets/index.ts +11 -0
  62. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  63. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  64. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  65. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  66. package/src/components/atoms/NumberInput/index.ts +4 -0
  67. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  68. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  69. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  70. package/src/components/atoms/Switch/Switch.tsx +16 -13
  71. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  72. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  73. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  74. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  75. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  76. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  77. package/src/components/atoms/TextArea/index.ts +6 -0
  78. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  79. package/src/components/atoms/index.ts +3 -0
  80. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  81. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  82. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  83. package/src/components/molecules/Accordion/index.ts +6 -0
  84. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  85. package/src/components/molecules/Animated/Animated.tsx +283 -0
  86. package/src/components/molecules/Animated/index.ts +10 -0
  87. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  88. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
  89. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  90. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  91. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  92. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  93. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  94. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  95. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  96. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  97. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  98. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  99. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  100. package/src/components/molecules/CopyField/index.ts +4 -0
  101. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  102. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  103. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  104. package/src/components/molecules/DatePicker/index.ts +2 -0
  105. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  106. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  107. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  108. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  109. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  110. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  111. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  112. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  113. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  114. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  115. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  116. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  117. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  118. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  119. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  120. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  121. package/src/components/molecules/Drawer/index.ts +12 -0
  122. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  123. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  124. package/src/components/molecules/FilterTab/index.ts +2 -0
  125. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  126. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  127. package/src/components/molecules/MessageCard/index.ts +10 -0
  128. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  129. package/src/components/molecules/Notification/Notification.tsx +426 -0
  130. package/src/components/molecules/Notification/index.ts +2 -0
  131. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  132. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  133. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  134. package/src/components/molecules/NumberField/index.ts +2 -0
  135. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  136. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  137. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  138. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
  139. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  140. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
  141. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  142. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  143. package/src/components/molecules/PasswordField/index.ts +10 -0
  144. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
  145. package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
  146. package/src/components/molecules/PictureSelector/index.ts +5 -0
  147. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  148. package/src/components/molecules/Progress/Progress.tsx +184 -0
  149. package/src/components/molecules/Progress/index.ts +2 -0
  150. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  151. package/src/components/molecules/Radio/Radio.tsx +1 -2
  152. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  153. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  154. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  155. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  156. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  157. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  158. package/src/components/molecules/SearchField/index.ts +4 -0
  159. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  160. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  161. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  162. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  163. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  164. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  165. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  166. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  167. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  168. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  169. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  170. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  171. package/src/components/molecules/SelectField/index.ts +10 -0
  172. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  173. package/src/components/molecules/Slider/Slider.tsx +293 -180
  174. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  175. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  176. package/src/components/molecules/Tooltip/index.ts +2 -0
  177. package/src/components/molecules/index.ts +15 -0
  178. package/src/test-utils.tsx +20 -0
  179. package/tsconfig.json +1 -1
  180. package/tsup.config.ts +16 -2
  181. package/vitest.config.ts +114 -0
  182. 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,10 @@
1
+ export {
2
+ Animated,
3
+ fadeAnimation,
4
+ fadeUpAnimation,
5
+ fadeDownAnimation,
6
+ scaleAnimation,
7
+ slideInAnimation,
8
+ slideOutAnimation
9
+ } from "./Animated"
10
+ export type { AnimatedProps, AnimatedVariant } from "./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 fullWidth>Confirm</Button>
31
- <Button fullWidth variant="outlined">
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 fullWidth>Primary action</Button>
42
- <Button fullWidth variant="outlined">
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 fullWidth>Continue</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 fullWidth>Confirm</Button>
70
- <Button fullWidth variant="outlined">
71
- Cancel
72
- </Button>
65
+ <Button>Confirm</Button>
66
+ <Button variant="outlined">Cancel</Button>
73
67
  </ButtonGroup>
74
68
  </View>
75
69
  )