@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,244 @@
1
+ import React, { useMemo } from "react"
2
+ import { Image, ImageSourcePropType, View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import { DrawerClose } from "./DrawerClose"
6
+ import {
7
+ DrawerHeaderContext,
8
+ type DrawerHeaderVariant
9
+ } from "./DrawerHeaderContext"
10
+ import { useDrawerDragContext } from "./DrawerDragContext"
11
+
12
+ const parseTokenValue = (value: string): number => parseFloat(value)
13
+
14
+ type DrawerHeaderOwnProps = {
15
+ variant?: DrawerHeaderVariant
16
+ imageSource?: ImageSourcePropType
17
+ }
18
+
19
+ export type DrawerHeaderProps = DrawerHeaderOwnProps &
20
+ Omit<ViewProps, keyof DrawerHeaderOwnProps>
21
+
22
+ const StyledGrabberWrapper = styled(View)<{ grabberPaddingTop: number }>(
23
+ ({ grabberPaddingTop }) => ({
24
+ alignItems: "center",
25
+ justifyContent: "center",
26
+ width: "100%",
27
+ paddingTop: grabberPaddingTop
28
+ })
29
+ )
30
+
31
+ const StyledGrabberBar = styled(View)<{
32
+ barWidth: number
33
+ barHeight: number
34
+ barBorderRadius: number
35
+ barColor: string
36
+ }>(({ barWidth, barHeight, barBorderRadius, barColor }) => ({
37
+ width: barWidth,
38
+ height: barHeight,
39
+ borderRadius: barBorderRadius,
40
+ backgroundColor: barColor
41
+ }))
42
+
43
+ const StyledHeader = styled(View)<{
44
+ headerPaddingHorizontal: number
45
+ headerPaddingBottom: number
46
+ headerGap: number
47
+ headerBorderBottomWidth?: number
48
+ headerBorderColor?: string
49
+ headerBgColor?: string
50
+ }>(
51
+ ({
52
+ headerPaddingHorizontal,
53
+ headerPaddingBottom,
54
+ headerGap,
55
+ headerBorderBottomWidth,
56
+ headerBorderColor,
57
+ headerBgColor
58
+ }) => ({
59
+ flexDirection: "column",
60
+ flexShrink: 0,
61
+ width: "100%",
62
+ paddingHorizontal: headerPaddingHorizontal,
63
+ paddingBottom: headerPaddingBottom,
64
+ gap: headerGap,
65
+ ...(headerBorderBottomWidth
66
+ ? {
67
+ borderBottomWidth: headerBorderBottomWidth,
68
+ borderBottomColor: headerBorderColor
69
+ }
70
+ : {}),
71
+ ...(headerBgColor ? { backgroundColor: headerBgColor } : {})
72
+ })
73
+ )
74
+
75
+ const StyledImageContainer = styled(View)<{
76
+ imageHeight: number
77
+ imageBorderRadius: number
78
+ }>(({ imageHeight, imageBorderRadius }) => ({
79
+ position: "relative",
80
+ width: "100%",
81
+ height: imageHeight,
82
+ borderRadius: imageBorderRadius,
83
+ overflow: "hidden",
84
+ flexShrink: 0
85
+ }))
86
+
87
+ const StyledImageCloseWrapper = styled(View)<{
88
+ closeTop: number
89
+ closeRight: number
90
+ }>(({ closeTop, closeRight }) => ({
91
+ position: "absolute",
92
+ top: closeTop,
93
+ right: closeRight,
94
+ zIndex: 1
95
+ }))
96
+
97
+ const StyledTitleRow = styled(View)<{ rowGap: number }>(({ rowGap }) => ({
98
+ flexDirection: "row",
99
+ gap: rowGap,
100
+ alignItems: "flex-start",
101
+ width: "100%",
102
+ flexShrink: 0
103
+ }))
104
+
105
+ const StyledTitleContent = styled(View)<{ contentGap: number }>(
106
+ ({ contentGap }) => ({
107
+ flexDirection: "column",
108
+ gap: contentGap,
109
+ flex: 1,
110
+ minWidth: 0
111
+ })
112
+ )
113
+
114
+ function splitChildren(children: React.ReactNode) {
115
+ let closeChild: React.ReactNode = null
116
+ const otherChildren: React.ReactNode[] = []
117
+
118
+ React.Children.forEach(children, (child) => {
119
+ if (React.isValidElement(child) && child.type === DrawerClose) {
120
+ closeChild = child
121
+ } else {
122
+ otherChildren.push(child)
123
+ }
124
+ })
125
+
126
+ return { closeChild, otherChildren }
127
+ }
128
+
129
+ /**
130
+ * Header section of the drawer. Renders a grabber bar automatically and
131
+ * manages close button placement based on the variant.
132
+ *
133
+ * - `titleAndText` (default): title + description on the left, close button on the right.
134
+ * - `image`: image container with close button absolutely positioned top-right.
135
+ * Pass `imageSource` prop for the image.
136
+ * - `fullBleed`: same layout as `titleAndText` but with a brand background colour.
137
+ *
138
+ * @param {"image" | "titleAndText" | "fullBleed"} [variant="titleAndText"] - Header style variant
139
+ * @param {ImageSourcePropType} [imageSource] - Image source for the `image` variant
140
+ */
141
+ export const DrawerHeader = React.forwardRef<View, DrawerHeaderProps>(
142
+ ({ variant = "titleAndText", imageSource, children, ...props }, ref) => {
143
+ const theme = useTheme()
144
+ const { drawer, grabber } = theme.tokens.components
145
+ const { colour, dimensions } = theme.tokens.semantics
146
+ const dragContext = useDrawerDragContext()
147
+
148
+ const contextValue = useMemo(() => ({ variant }), [variant])
149
+ const { closeChild, otherChildren } = splitChildren(children)
150
+
151
+ const headerSpacing = drawer.spacing.drawerHeader[
152
+ variant === "image" ? "Image" : variant
153
+ ] as {
154
+ horizontalPadding: string
155
+ bottomPadding: string
156
+ gap: string
157
+ container?: { gap: string; content?: { gap: string } }
158
+ }
159
+
160
+ let content: React.ReactNode
161
+
162
+ if (variant === "image") {
163
+ content = (
164
+ <StyledImageContainer
165
+ imageHeight={parseTokenValue(drawer.size.imageHeader.image.maxHeight)}
166
+ imageBorderRadius={parseTokenValue(
167
+ drawer.drawerHeader.borderRadius.image.default
168
+ )}
169
+ >
170
+ {imageSource && (
171
+ <Image
172
+ source={imageSource}
173
+ style={{ width: "100%", height: "100%", resizeMode: "cover" }}
174
+ />
175
+ )}
176
+ {otherChildren}
177
+ {closeChild && (
178
+ <StyledImageCloseWrapper
179
+ closeTop={parseTokenValue(drawer.spacing.close.top.md)}
180
+ closeRight={parseTokenValue(drawer.spacing.close.right.md)}
181
+ >
182
+ {closeChild}
183
+ </StyledImageCloseWrapper>
184
+ )}
185
+ </StyledImageContainer>
186
+ )
187
+ } else {
188
+ const containerGap = headerSpacing.container?.gap ?? "0"
189
+ const contentGap = headerSpacing.container?.content?.gap ?? "0"
190
+ content = (
191
+ <StyledTitleRow rowGap={parseTokenValue(containerGap)}>
192
+ <StyledTitleContent contentGap={parseTokenValue(contentGap)}>
193
+ {otherChildren}
194
+ </StyledTitleContent>
195
+ {closeChild}
196
+ </StyledTitleRow>
197
+ )
198
+ }
199
+
200
+ return (
201
+ <DrawerHeaderContext.Provider value={contextValue}>
202
+ <StyledHeader
203
+ ref={ref}
204
+ headerPaddingHorizontal={parseTokenValue(
205
+ headerSpacing.horizontalPadding
206
+ )}
207
+ headerPaddingBottom={parseTokenValue(headerSpacing.bottomPadding)}
208
+ headerGap={parseTokenValue(headerSpacing.gap)}
209
+ headerBorderBottomWidth={
210
+ variant === "titleAndText"
211
+ ? parseTokenValue(dimensions.borderWidth.sm)
212
+ : undefined
213
+ }
214
+ headerBorderColor={
215
+ variant === "titleAndText" ? colour.border.default : undefined
216
+ }
217
+ headerBgColor={
218
+ variant === "fullBleed"
219
+ ? drawer.drawerHeader.colour.background.fullBleed
220
+ : undefined
221
+ }
222
+ {...props}
223
+ >
224
+ <StyledGrabberWrapper
225
+ grabberPaddingTop={parseTokenValue(
226
+ drawer.spacing.grabber.paddingTop
227
+ )}
228
+ {...(dragContext?.panHandlers ?? {})}
229
+ >
230
+ <StyledGrabberBar
231
+ barWidth={parseTokenValue(grabber.size.bar.width)}
232
+ barHeight={parseTokenValue(grabber.size.bar.height)}
233
+ barBorderRadius={parseTokenValue(dimensions.borderRadius.round)}
234
+ barColor={grabber.colour.background.default}
235
+ />
236
+ </StyledGrabberWrapper>
237
+ {content}
238
+ </StyledHeader>
239
+ </DrawerHeaderContext.Provider>
240
+ )
241
+ }
242
+ )
243
+
244
+ DrawerHeader.displayName = "Drawer.Header"
@@ -0,0 +1,13 @@
1
+ import React from "react"
2
+
3
+ export type DrawerHeaderVariant = "titleAndText" | "image" | "fullBleed"
4
+
5
+ export type DrawerHeaderContextValue = {
6
+ variant: DrawerHeaderVariant
7
+ }
8
+
9
+ export const DrawerHeaderContext =
10
+ React.createContext<DrawerHeaderContextValue | null>(null)
11
+
12
+ export const useDrawerHeaderContext = () =>
13
+ React.useContext(DrawerHeaderContext)
@@ -0,0 +1,53 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import { Animated, Pressable, StyleSheet } from "react-native"
3
+ import { useTheme } from "@emotion/react"
4
+ import { useDrawerContext } from "./DrawerContext"
5
+
6
+ export type DrawerOverlayProps = {
7
+ accessible?: boolean
8
+ }
9
+
10
+ const FADE_DURATION = 220
11
+
12
+ /**
13
+ * Full-screen backdrop behind the drawer panel.
14
+ * Fades in when the drawer opens and fades out when it closes.
15
+ * Tapping it dismisses the drawer.
16
+ */
17
+ export const DrawerOverlay = React.forwardRef<
18
+ React.ComponentRef<typeof Pressable>,
19
+ DrawerOverlayProps
20
+ >(({ accessible = false }, _ref) => {
21
+ const theme = useTheme()
22
+ const { isOpen, closeDrawer } = useDrawerContext()
23
+ const opacity = useRef(new Animated.Value(0)).current
24
+
25
+ useEffect(() => {
26
+ Animated.timing(opacity, {
27
+ toValue: isOpen ? 1 : 0,
28
+ duration: FADE_DURATION,
29
+ useNativeDriver: true
30
+ }).start()
31
+ }, [isOpen, opacity])
32
+
33
+ return (
34
+ <Pressable
35
+ style={StyleSheet.absoluteFillObject}
36
+ onPress={closeDrawer}
37
+ accessible={accessible}
38
+ accessibilityLabel="Close drawer"
39
+ >
40
+ <Animated.View
41
+ style={[
42
+ StyleSheet.absoluteFillObject,
43
+ {
44
+ backgroundColor: theme.tokens.components.modal.overlay.colour,
45
+ opacity
46
+ }
47
+ ]}
48
+ />
49
+ </Pressable>
50
+ )
51
+ })
52
+
53
+ DrawerOverlay.displayName = "Drawer.Overlay"
@@ -0,0 +1,32 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import { useTheme } from "@emotion/react"
4
+ import { Typography } from "../../atoms/Typography"
5
+
6
+ export type DrawerTitleProps = {
7
+ children: React.ReactNode
8
+ } & Omit<ViewProps, "children">
9
+
10
+ /**
11
+ * Title text for the drawer header. Uses the heading typography token from
12
+ * the drawer component tokens.
13
+ */
14
+ export const DrawerTitle = React.forwardRef<View, DrawerTitleProps>(
15
+ ({ children, ...props }, ref) => {
16
+ const theme = useTheme()
17
+ const { drawerHeader } = theme.tokens.components.drawer
18
+
19
+ return (
20
+ <View ref={ref} {...props}>
21
+ <Typography
22
+ token={drawerHeader.typography.title}
23
+ color={drawerHeader.colour.text.title}
24
+ >
25
+ {children}
26
+ </Typography>
27
+ </View>
28
+ )
29
+ }
30
+ )
31
+
32
+ DrawerTitle.displayName = "Drawer.Title"
@@ -0,0 +1,12 @@
1
+ export { Drawer } from "./Drawer"
2
+ export type { DrawerProps, DrawerTriggerProps } from "./Drawer"
3
+ export type { DrawerContentProps } from "./DrawerContent"
4
+ export type { DrawerOverlayProps } from "./DrawerOverlay"
5
+ export type { DrawerGrabberProps } from "./DrawerGrabber"
6
+ export type { DrawerHeaderProps } from "./DrawerHeader"
7
+ export type { DrawerHeaderVariant } from "./DrawerHeaderContext"
8
+ export type { DrawerTitleProps } from "./DrawerTitle"
9
+ export type { DrawerDescriptionProps } from "./DrawerDescription"
10
+ export type { DrawerCloseProps } from "./DrawerClose"
11
+ export type { DrawerBodyProps } from "./DrawerBody"
12
+ export type { DrawerFooterProps } from "./DrawerFooter"
@@ -0,0 +1,210 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { FilterTab } from "./FilterTab"
4
+ import type { FilterTabProps } from "./FilterTab"
5
+ import { FilterAlt } from "@butternutbox/pawprint-icons/core"
6
+
7
+ export default {
8
+ title: "Molecules/FilterTab",
9
+ component: FilterTab,
10
+ argTypes: {
11
+ size: {
12
+ control: "select",
13
+ options: ["sm", "lg"],
14
+ description: "Size of all tab items"
15
+ },
16
+ multiple: {
17
+ control: "boolean",
18
+ description:
19
+ "Allow multiple items to be selected. When false, only one item can be selected at a time."
20
+ }
21
+ }
22
+ }
23
+
24
+ // --- Playground ---
25
+
26
+ export const Playground = {
27
+ args: {
28
+ size: "sm",
29
+ multiple: true
30
+ },
31
+ render: (args: FilterTabProps) => {
32
+ const [value, setValue] = React.useState(["all"])
33
+ return (
34
+ <View style={styles.container}>
35
+ <FilterTab {...args} value={value} onValueChange={setValue}>
36
+ <FilterTab.Item value="all" label="All" />
37
+ <FilterTab.Item value="wellness" label="Wellness" />
38
+ <FilterTab.Item value="treats" label="Treats & Chews" />
39
+ <FilterTab.Item value="accessories" label="Accessories" />
40
+ </FilterTab>
41
+ </View>
42
+ )
43
+ }
44
+ }
45
+
46
+ // --- Small ---
47
+
48
+ export const Small = {
49
+ name: "Small",
50
+ render: () => {
51
+ const [value, setValue] = React.useState(["all"])
52
+ return (
53
+ <View style={styles.container}>
54
+ <FilterTab size="sm" value={value} onValueChange={setValue}>
55
+ <FilterTab.Item value="all" label="All" />
56
+ <FilterTab.Item value="wellness" label="Wellness" />
57
+ <FilterTab.Item value="treats" label="Treats & Chews" />
58
+ <FilterTab.Item value="accessories" label="Accessories" />
59
+ <FilterTab.Item value="bundles" label="Bundles" />
60
+ </FilterTab>
61
+ </View>
62
+ )
63
+ }
64
+ }
65
+
66
+ // --- Large ---
67
+
68
+ export const Large = {
69
+ name: "Large",
70
+ render: () => {
71
+ const [value, setValue] = React.useState(["all"])
72
+ return (
73
+ <View style={styles.container}>
74
+ <FilterTab size="lg" value={value} onValueChange={setValue}>
75
+ <FilterTab.Item value="all" label="All" />
76
+ <FilterTab.Item value="wellness" label="Wellness" />
77
+ <FilterTab.Item value="treats" label="Treats & Chews" />
78
+ <FilterTab.Item value="accessories" label="Accessories" />
79
+ <FilterTab.Item value="bundles" label="Bundles" />
80
+ </FilterTab>
81
+ </View>
82
+ )
83
+ }
84
+ }
85
+
86
+ // --- With Icons ---
87
+
88
+ export const WithIcons = {
89
+ name: "With Icons",
90
+ render: () => {
91
+ const [value, setValue] = React.useState(["all"])
92
+ return (
93
+ <View style={styles.container}>
94
+ <FilterTab size="sm" value={value} onValueChange={setValue}>
95
+ <FilterTab.Item
96
+ value="all"
97
+ label="All"
98
+ leadingIcon={FilterAlt}
99
+ trailingIcon={FilterAlt}
100
+ />
101
+ <FilterTab.Item
102
+ value="wellness"
103
+ label="Wellness"
104
+ leadingIcon={FilterAlt}
105
+ />
106
+ <FilterTab.Item value="treats" label="Treats" />
107
+ </FilterTab>
108
+ </View>
109
+ )
110
+ }
111
+ }
112
+
113
+ // --- Icon Only ---
114
+
115
+ export const IconOnly = {
116
+ name: "Icon Only",
117
+ render: () => {
118
+ const [value, setValue] = React.useState<string[]>([])
119
+ return (
120
+ <View style={styles.container}>
121
+ <FilterTab size="sm" value={value} onValueChange={setValue}>
122
+ <FilterTab.Item
123
+ value="filter1"
124
+ icon={FilterAlt}
125
+ accessibilityLabel="Filter 1"
126
+ />
127
+ <FilterTab.Item
128
+ value="filter2"
129
+ icon={FilterAlt}
130
+ accessibilityLabel="Filter 2"
131
+ />
132
+ <FilterTab.Item
133
+ value="filter3"
134
+ icon={FilterAlt}
135
+ accessibilityLabel="Filter 3"
136
+ />
137
+ </FilterTab>
138
+ </View>
139
+ )
140
+ }
141
+ }
142
+
143
+ // --- Disabled ---
144
+
145
+ export const Disabled = {
146
+ name: "Inactive / Disabled",
147
+ render: () => {
148
+ const [value, setValue] = React.useState(["all"])
149
+ return (
150
+ <View style={styles.container}>
151
+ <FilterTab size="sm" value={value} onValueChange={setValue}>
152
+ <FilterTab.Item value="all" label="All" />
153
+ <FilterTab.Item value="wellness" label="Wellness" disabled />
154
+ <FilterTab.Item value="treats" label="Treats" disabled />
155
+ </FilterTab>
156
+ </View>
157
+ )
158
+ }
159
+ }
160
+
161
+ // --- Single Select ---
162
+
163
+ export const SingleSelect = {
164
+ name: "Single Select",
165
+ render: () => {
166
+ const [value, setValue] = React.useState(["all"])
167
+ return (
168
+ <View style={styles.container}>
169
+ <FilterTab
170
+ size="sm"
171
+ multiple={false}
172
+ value={value}
173
+ onValueChange={setValue}
174
+ >
175
+ <FilterTab.Item value="all" label="All" />
176
+ <FilterTab.Item value="wellness" label="Wellness" />
177
+ <FilterTab.Item value="treats" label="Treats & Chews" />
178
+ <FilterTab.Item value="accessories" label="Accessories" />
179
+ <FilterTab.Item value="bundles" label="Bundles" />
180
+ </FilterTab>
181
+ </View>
182
+ )
183
+ }
184
+ }
185
+
186
+ // --- Multiple Select ---
187
+
188
+ export const MultipleSelect = {
189
+ name: "Multiple Select",
190
+ render: () => {
191
+ const [value, setValue] = React.useState(["all", "wellness"])
192
+ return (
193
+ <View style={styles.container}>
194
+ <FilterTab size="sm" multiple value={value} onValueChange={setValue}>
195
+ <FilterTab.Item value="all" label="All" />
196
+ <FilterTab.Item value="wellness" label="Wellness" />
197
+ <FilterTab.Item value="treats" label="Treats & Chews" />
198
+ <FilterTab.Item value="accessories" label="Accessories" />
199
+ <FilterTab.Item value="bundles" label="Bundles" />
200
+ </FilterTab>
201
+ </View>
202
+ )
203
+ }
204
+ }
205
+
206
+ const styles = StyleSheet.create({
207
+ container: {
208
+ padding: 16
209
+ }
210
+ })