@butternutbox/pawprint-native 0.0.1 → 0.2.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 (187) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +30 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12413 -1459
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1111 -13
  7. package/dist/index.d.ts +1111 -13
  8. package/dist/index.js +12365 -1457
  9. package/dist/index.js.map +1 -1
  10. package/package.json +29 -11
  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.stories.tsx +2 -2
  42. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  43. package/src/components/atoms/Illustration/Illustration.tsx +3 -3
  44. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  45. package/src/components/atoms/Input/Input.test.tsx +306 -0
  46. package/src/components/atoms/Input/Input.tsx +9 -1
  47. package/src/components/atoms/Input/InputField.tsx +226 -74
  48. package/src/components/atoms/Link/Link.test.tsx +89 -0
  49. package/src/components/atoms/Link/Link.tsx +7 -6
  50. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  51. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  52. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  53. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  54. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  55. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  56. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  57. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  58. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  61. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  62. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  63. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  64. package/src/components/atoms/Logo/assets/index.ts +11 -0
  65. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  66. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  67. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  68. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  69. package/src/components/atoms/NumberInput/index.ts +4 -0
  70. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  71. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  72. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  73. package/src/components/atoms/Switch/Switch.tsx +28 -17
  74. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  75. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  76. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  77. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  78. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  79. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  80. package/src/components/atoms/TextArea/index.ts +6 -0
  81. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  82. package/src/components/atoms/index.ts +3 -0
  83. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  84. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  85. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  86. package/src/components/molecules/Accordion/index.ts +6 -0
  87. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  88. package/src/components/molecules/Animated/Animated.tsx +283 -0
  89. package/src/components/molecules/Animated/index.ts +10 -0
  90. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
  91. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  92. package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
  93. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
  94. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  95. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  96. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  97. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  98. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  99. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  100. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  101. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  102. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  103. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  104. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  105. package/src/components/molecules/CopyField/index.ts +4 -0
  106. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  107. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  108. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  109. package/src/components/molecules/DatePicker/index.ts +2 -0
  110. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  111. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  112. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  113. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  114. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  115. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  116. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  117. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  118. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  119. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  120. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  121. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  122. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  123. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  124. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  125. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  126. package/src/components/molecules/Drawer/index.ts +12 -0
  127. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  128. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  129. package/src/components/molecules/FilterTab/index.ts +2 -0
  130. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  131. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  132. package/src/components/molecules/MessageCard/index.ts +10 -0
  133. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  134. package/src/components/molecules/Notification/Notification.tsx +426 -0
  135. package/src/components/molecules/Notification/index.ts +2 -0
  136. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  137. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  138. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  139. package/src/components/molecules/NumberField/index.ts +2 -0
  140. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  141. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  142. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  143. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
  144. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  145. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
  146. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  147. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  148. package/src/components/molecules/PasswordField/index.ts +10 -0
  149. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
  150. package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
  151. package/src/components/molecules/PictureSelector/index.ts +5 -0
  152. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  153. package/src/components/molecules/Progress/Progress.tsx +184 -0
  154. package/src/components/molecules/Progress/index.ts +2 -0
  155. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  156. package/src/components/molecules/Radio/Radio.tsx +1 -2
  157. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  158. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  159. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  160. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  161. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  162. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  163. package/src/components/molecules/SearchField/index.ts +4 -0
  164. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  165. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  166. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  167. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  168. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  169. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  170. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  171. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  172. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  173. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  174. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  175. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  176. package/src/components/molecules/SelectField/index.ts +10 -0
  177. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  178. package/src/components/molecules/Slider/Slider.tsx +293 -180
  179. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  180. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  181. package/src/components/molecules/Tooltip/index.ts +2 -0
  182. package/src/components/molecules/index.ts +15 -0
  183. package/src/test-utils.tsx +20 -0
  184. package/tsconfig.json +1 -1
  185. package/tsup.config.ts +16 -2
  186. package/vitest.config.ts +114 -0
  187. 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"
@@ -1,8 +1,13 @@
1
1
  import React from "react"
2
- import { View, StyleSheet, Text } from "react-native"
2
+ import { View, StyleSheet } from "react-native"
3
+ import {
4
+ KeyboardArrowLeft,
5
+ KeyboardArrowRight
6
+ } from "@butternutbox/pawprint-icons/core"
3
7
  import { ButtonDock } from "./ButtonDock"
4
8
  import type { ButtonDockProps } from "./ButtonDock"
5
9
  import { Button } from "../../atoms/Button"
10
+ import { Icon } from "../../atoms/Icon"
6
11
 
7
12
  export default {
8
13
  title: "Molecules/ButtonDock",
@@ -11,7 +16,7 @@ export default {
11
16
  variant: {
12
17
  control: { type: "select" },
13
18
  options: ["stacked", "inline"],
14
- description: "Layout variant (stacked or inline)"
19
+ description: "Layout variant"
15
20
  },
16
21
  description: {
17
22
  control: { type: "text" },
@@ -23,13 +28,13 @@ export default {
23
28
  export const Playground = {
24
29
  args: {
25
30
  variant: "stacked",
26
- description: "Helper text goes here"
31
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
27
32
  },
28
33
  render: (args: ButtonDockProps) => (
29
34
  <ButtonDock {...args}>
30
35
  <Button fullWidth>Continue</Button>
31
- <Button fullWidth variant="outlined">
32
- Go back
36
+ <Button fullWidth variant="filled" colour="secondary">
37
+ Back
33
38
  </Button>
34
39
  </ButtonDock>
35
40
  )
@@ -37,10 +42,13 @@ export const Playground = {
37
42
 
38
43
  export const Stacked = () => (
39
44
  <View style={styles.column}>
40
- <ButtonDock variant="stacked" description="Need help? Contact support.">
45
+ <ButtonDock
46
+ variant="stacked"
47
+ description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
48
+ >
41
49
  <Button fullWidth>Continue</Button>
42
- <Button fullWidth variant="outlined">
43
- Go back
50
+ <Button fullWidth variant="filled" colour="secondary">
51
+ Back
44
52
  </Button>
45
53
  </ButtonDock>
46
54
  </View>
@@ -49,8 +57,22 @@ export const Stacked = () => (
49
57
  export const Inline = () => (
50
58
  <View style={styles.column}>
51
59
  <ButtonDock variant="inline">
52
- <Button>Confirm</Button>
53
- <Button variant="outlined">Cancel</Button>
60
+ <Button
61
+ variant="filled"
62
+ colour="secondary"
63
+ startIcon={
64
+ <Icon icon={KeyboardArrowLeft} size="md" colour="action-default" />
65
+ }
66
+ >
67
+ Back
68
+ </Button>
69
+ <Button
70
+ endIcon={
71
+ <Icon icon={KeyboardArrowRight} size="md" colour="action-inverse" />
72
+ }
73
+ >
74
+ Continue
75
+ </Button>
54
76
  </ButtonDock>
55
77
  </View>
56
78
  )
@@ -59,16 +81,12 @@ export const WithLeadingContent = () => (
59
81
  <View style={styles.column}>
60
82
  <ButtonDock
61
83
  variant="stacked"
62
- leadingContent={
63
- <Text style={styles.leadingText}>
64
- By continuing you agree to our terms and conditions.
65
- </Text>
66
- }
67
- description="You can change this later in settings."
84
+ leadingContent={<View style={styles.leadingSlot} />}
85
+ description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
68
86
  >
69
- <Button fullWidth>Accept & Continue</Button>
70
- <Button fullWidth variant="outlined">
71
- Go back
87
+ <Button fullWidth>Continue</Button>
88
+ <Button fullWidth variant="filled" colour="secondary">
89
+ Back
72
90
  </Button>
73
91
  </ButtonDock>
74
92
  </View>
@@ -76,8 +94,8 @@ export const WithLeadingContent = () => (
76
94
 
77
95
  export const StackedSingleButton = () => (
78
96
  <View style={styles.column}>
79
- <ButtonDock variant="stacked" description="Only one action available.">
80
- <Button fullWidth>Continue</Button>
97
+ <ButtonDock variant="stacked">
98
+ <Button fullWidth>Update</Button>
81
99
  </ButtonDock>
82
100
  </View>
83
101
  )
@@ -87,9 +105,10 @@ const styles = StyleSheet.create({
87
105
  flexDirection: "column",
88
106
  gap: 24
89
107
  },
90
- leadingText: {
91
- textAlign: "center",
92
- fontSize: 14,
93
- color: "#666"
108
+ leadingSlot: {
109
+ height: 64,
110
+ width: "100%",
111
+ backgroundColor: "#f5f5f5",
112
+ borderRadius: 8
94
113
  }
95
114
  })