@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,310 @@
1
+ import React from "react"
2
+ import { View, Pressable, ViewProps, PressableProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import { Icon, type PawprintIcon } from "../../atoms/Icon"
6
+ import { Typography } from "../../atoms/Typography"
7
+
8
+ // ─── Types ────────────────────────────────────────────────────────────────────
9
+
10
+ type FilterTabSize = "sm" | "lg"
11
+
12
+ type FilterTabRootOwnProps = {
13
+ size?: FilterTabSize
14
+ multiple?: boolean
15
+ value?: string[]
16
+ defaultValue?: string[]
17
+ onValueChange?: (value: string[]) => void
18
+ children: React.ReactNode
19
+ }
20
+
21
+ export type FilterTabProps = FilterTabRootOwnProps &
22
+ Omit<ViewProps, keyof FilterTabRootOwnProps>
23
+
24
+ type FilterTabItemOwnProps = {
25
+ value: string
26
+ label?: string
27
+ icon?: PawprintIcon
28
+ leadingIcon?: PawprintIcon
29
+ trailingIcon?: PawprintIcon
30
+ disabled?: boolean
31
+ }
32
+
33
+ export type FilterTabItemProps = FilterTabItemOwnProps &
34
+ Omit<PressableProps, keyof FilterTabItemOwnProps | "children">
35
+
36
+ // ─── Context ──────────────────────────────────────────────────────────────────
37
+
38
+ type FilterTabContextValue = {
39
+ size: FilterTabSize
40
+ selectedValues: string[]
41
+ toggle: (value: string) => void
42
+ }
43
+
44
+ const FilterTabContext = React.createContext<FilterTabContextValue>({
45
+ size: "sm",
46
+ selectedValues: [],
47
+ toggle: () => {}
48
+ })
49
+
50
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
51
+
52
+ const parseTokenValue = (value: string): number => parseFloat(value)
53
+
54
+ // ─── Styled Components ────────────────────────────────────────────────────────
55
+
56
+ const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
57
+ flexDirection: "row",
58
+ alignItems: "center",
59
+ gap: rootGap,
60
+ flexWrap: "nowrap"
61
+ }))
62
+
63
+ const StyledItem = styled(Pressable)<{
64
+ itemHeight: number
65
+ itemMinWidth: number
66
+ itemPaddingH: number
67
+ itemPaddingV: number
68
+ itemBorderRadius: number
69
+ itemBgColor: string
70
+ itemOpacity: number
71
+ isIconOnly: boolean
72
+ }>(
73
+ ({
74
+ itemHeight,
75
+ itemMinWidth,
76
+ itemPaddingH,
77
+ itemPaddingV,
78
+ itemBorderRadius,
79
+ itemBgColor,
80
+ itemOpacity,
81
+ isIconOnly
82
+ }) => ({
83
+ flexDirection: "row",
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ height: itemHeight,
87
+ ...(isIconOnly
88
+ ? { width: itemHeight, maxWidth: itemHeight }
89
+ : { minWidth: itemMinWidth }),
90
+ paddingHorizontal: itemPaddingH,
91
+ paddingVertical: itemPaddingV,
92
+ borderRadius: itemBorderRadius,
93
+ backgroundColor: itemBgColor,
94
+ opacity: itemOpacity,
95
+ flexShrink: 0
96
+ })
97
+ )
98
+
99
+ // ─── FilterTab.Item ───────────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * An individual filter tab button. Must be used inside a `FilterTab` container.
103
+ *
104
+ * @param {string} value - Unique identifier for this tab item.
105
+ * @param {string} [label] - Text label. Omit for icon-only mode.
106
+ * @param {PawprintIcon} [icon] - Icon for icon-only mode (requires accessibilityLabel).
107
+ * @param {PawprintIcon} [leadingIcon] - Optional icon before the label.
108
+ * @param {PawprintIcon} [trailingIcon] - Optional icon after the label.
109
+ * @param {boolean} [disabled=false] - Disables the tab (inactive state).
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * import { FilterTab } from "@butternutbox/pawprint-native"
114
+ *
115
+ * <FilterTab.Item value="wellness" label="Wellness" />
116
+ * ```
117
+ */
118
+ const FilterTabItem = React.forwardRef<View, FilterTabItemProps>(
119
+ (
120
+ {
121
+ value,
122
+ label,
123
+ icon: IconOnlyIcon,
124
+ leadingIcon,
125
+ trailingIcon,
126
+ disabled = false,
127
+ accessibilityLabel,
128
+ ...rest
129
+ },
130
+ ref
131
+ ) => {
132
+ const theme = useTheme()
133
+ const { size, selectedValues, toggle } = React.useContext(FilterTabContext)
134
+ const isSelected = selectedValues.includes(value)
135
+ const isIconOnly = !!IconOnlyIcon && !label
136
+ const isActive = isSelected && !disabled
137
+
138
+ const { tabItem } = theme.tokens.components.filterTabs
139
+ const { dimensions } = theme.tokens.semantics
140
+ const height = parseTokenValue(
141
+ size === "lg" ? tabItem.size.large.height : tabItem.size.small.height
142
+ )
143
+ const minWidth = parseTokenValue(tabItem.size.small.minWidth)
144
+ const iconColour = isActive
145
+ ? tabItem.colour.icon.selected
146
+ : tabItem.colour.icon.unselected
147
+
148
+ return (
149
+ <StyledItem
150
+ ref={ref}
151
+ accessible
152
+ accessibilityRole="button"
153
+ accessibilityState={{ selected: isSelected, disabled }}
154
+ accessibilityLabel={isIconOnly ? accessibilityLabel : label}
155
+ disabled={disabled}
156
+ onPress={() => {
157
+ if (!disabled) toggle(value)
158
+ }}
159
+ itemHeight={height}
160
+ itemMinWidth={minWidth}
161
+ itemPaddingH={parseTokenValue(tabItem.spacing.horizontalPadding)}
162
+ itemPaddingV={parseTokenValue(tabItem.spacing.verticalPadding)}
163
+ itemBorderRadius={parseTokenValue(tabItem.borderRadius.default)}
164
+ itemBgColor={
165
+ isActive
166
+ ? tabItem.colour.background.selected
167
+ : tabItem.colour.background.default
168
+ }
169
+ itemOpacity={
170
+ disabled ? parseFloat(tabItem.opacity.disabled.default) : 1
171
+ }
172
+ isIconOnly={isIconOnly}
173
+ style={{ gap: parseTokenValue(dimensions.spacing["2xs"]) }}
174
+ {...rest}
175
+ >
176
+ {isIconOnly && IconOnlyIcon && (
177
+ <Icon
178
+ icon={IconOnlyIcon}
179
+ size="md"
180
+ customColour={iconColour}
181
+ aria-label={accessibilityLabel}
182
+ />
183
+ )}
184
+ {!isIconOnly && leadingIcon && (
185
+ <Icon
186
+ icon={leadingIcon}
187
+ size="md"
188
+ customColour={iconColour}
189
+ aria-label={label}
190
+ />
191
+ )}
192
+ {!isIconOnly && label && (
193
+ <Typography
194
+ token={
195
+ isActive
196
+ ? tabItem.typography.label.default
197
+ : tabItem.typography.label.inactive
198
+ }
199
+ color={
200
+ disabled
201
+ ? tabItem.colour.text.inactive
202
+ : isActive
203
+ ? tabItem.colour.text.selected
204
+ : tabItem.colour.text.default
205
+ }
206
+ >
207
+ {label}
208
+ </Typography>
209
+ )}
210
+ {!isIconOnly && trailingIcon && (
211
+ <Icon
212
+ icon={trailingIcon}
213
+ size="md"
214
+ customColour={iconColour}
215
+ aria-label={label}
216
+ />
217
+ )}
218
+ </StyledItem>
219
+ )
220
+ }
221
+ )
222
+
223
+ FilterTabItem.displayName = "FilterTab.Item"
224
+
225
+ // ─── FilterTab (Root) ─────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * A horizontal group of pill-shaped filter buttons. One or more tabs can be
229
+ * selected at a time to filter content. Supports controlled and uncontrolled
230
+ * selection.
231
+ *
232
+ * @param {"sm" | "lg"} [size="sm"] - Size of all tab items.
233
+ * @param {boolean} [multiple=true] - Allow multiple items to be selected. When false, only one item can be selected at a time.
234
+ * @param {string[]} [value] - Controlled selected tab values.
235
+ * @param {string[]} [defaultValue=[]] - Uncontrolled default selected values.
236
+ * @param {(value: string[]) => void} [onValueChange] - Fires when selection changes.
237
+ * @param {React.ReactNode} children - One or more `FilterTab.Item` elements.
238
+ *
239
+ * @example
240
+ * ```tsx
241
+ * import { FilterTab } from "@butternutbox/pawprint-native"
242
+ *
243
+ * <FilterTab size="sm" defaultValue={["all"]}>
244
+ * <FilterTab.Item value="all" label="All" />
245
+ * <FilterTab.Item value="wellness" label="Wellness" />
246
+ * </FilterTab>
247
+ * ```
248
+ */
249
+ const FilterTabRoot = React.forwardRef<View, FilterTabProps>(
250
+ (
251
+ {
252
+ size = "sm",
253
+ multiple = true,
254
+ value: controlledValue,
255
+ defaultValue = [],
256
+ onValueChange,
257
+ children,
258
+ ...rest
259
+ },
260
+ ref
261
+ ) => {
262
+ const theme = useTheme()
263
+ const { filterTabs } = theme.tokens.components
264
+
265
+ const isControlled = controlledValue !== undefined
266
+ const [internalValue, setInternalValue] =
267
+ React.useState<string[]>(defaultValue)
268
+ const selectedValues = isControlled ? controlledValue : internalValue
269
+
270
+ const toggle = React.useCallback(
271
+ (itemValue: string) => {
272
+ let next: string[]
273
+ if (multiple) {
274
+ next = selectedValues.includes(itemValue)
275
+ ? selectedValues.filter((v) => v !== itemValue)
276
+ : [...selectedValues, itemValue]
277
+ } else {
278
+ next = selectedValues.includes(itemValue) ? [] : [itemValue]
279
+ }
280
+
281
+ if (!isControlled) setInternalValue(next)
282
+ onValueChange?.(next)
283
+ },
284
+ [selectedValues, isControlled, onValueChange, multiple]
285
+ )
286
+
287
+ const contextValue = React.useMemo(
288
+ () => ({ size, selectedValues, toggle }),
289
+ [size, selectedValues, toggle]
290
+ )
291
+
292
+ return (
293
+ <FilterTabContext.Provider value={contextValue}>
294
+ <StyledRoot
295
+ ref={ref}
296
+ rootGap={parseTokenValue(filterTabs.filterTab.spacing.gap)}
297
+ {...rest}
298
+ >
299
+ {children}
300
+ </StyledRoot>
301
+ </FilterTabContext.Provider>
302
+ )
303
+ }
304
+ )
305
+
306
+ FilterTabRoot.displayName = "FilterTab"
307
+
308
+ export const FilterTab = Object.assign(FilterTabRoot, {
309
+ Item: FilterTabItem
310
+ })
@@ -0,0 +1,2 @@
1
+ export { FilterTab } from "./FilterTab"
2
+ export type { FilterTabProps, FilterTabItemProps } from "./FilterTab"
@@ -0,0 +1,169 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import {
4
+ MessageCard,
5
+ type MessageCardInsightProps,
6
+ type MessageCardBannerProps
7
+ } from "./MessageCard"
8
+ import { CheckCircle } from "@butternutbox/pawprint-icons/core"
9
+ import { AlsatianChefsHat } from "@butternutbox/pawprint-illustrations/breeds"
10
+
11
+ export default {
12
+ title: "Molecules/MessageCard"
13
+ }
14
+
15
+ export const InsightPlayground = (args: MessageCardInsightProps) => (
16
+ <View style={styles.column}>
17
+ <MessageCard.Insight {...args} icon={CheckCircle}>
18
+ {args.children}
19
+ </MessageCard.Insight>
20
+ </View>
21
+ )
22
+ InsightPlayground.args = {
23
+ variant: "standalone",
24
+ title: "Healthy from top to tail",
25
+ children: "Studies show fresh food improves dogs' poos, skin, coat, and gut."
26
+ }
27
+ InsightPlayground.argTypes = {
28
+ variant: {
29
+ control: { type: "select" },
30
+ options: ["standalone", "supporting"],
31
+ description:
32
+ "`supporting` renders a flat top edge when attached to a related element above."
33
+ },
34
+ title: {
35
+ control: { type: "text" },
36
+ description: "Optional headline (only rendered for `standalone`)."
37
+ },
38
+ children: { control: { type: "text" }, description: "Body copy." }
39
+ }
40
+
41
+ export const BannerPlayground = (args: MessageCardBannerProps) => (
42
+ <View style={styles.column}>
43
+ <MessageCard.Banner
44
+ {...args}
45
+ media={{ type: "illustration", illustration: AlsatianChefsHat }}
46
+ >
47
+ {args.children}
48
+ </MessageCard.Banner>
49
+ </View>
50
+ )
51
+ BannerPlayground.args = {
52
+ colourScheme: "primary",
53
+ title: "Short headline",
54
+ children: "Lorem ipsum dolor sit amet consectetur sit varius.",
55
+ linkLabel: "Find out more",
56
+ linkHref: "https://butternutbox.com"
57
+ }
58
+ BannerPlayground.argTypes = {
59
+ colourScheme: {
60
+ control: { type: "select" },
61
+ options: ["primary", "secondary"],
62
+ description:
63
+ "Background colour scheme. `primary` is saturated yellow; `secondary` is a lighter cream."
64
+ },
65
+ title: { control: { type: "text" }, description: "Optional headline." },
66
+ children: { control: { type: "text" }, description: "Body copy." },
67
+ linkLabel: { control: { type: "text" }, description: "CTA label." },
68
+ linkHref: { control: { type: "text" }, description: "CTA target URL." }
69
+ }
70
+
71
+ export const Insight = () => (
72
+ <View style={styles.column}>
73
+ <MessageCard.Insight icon={CheckCircle} title="Healthy from top to tail">
74
+ Studies show fresh food improves dogs' poos, skin, coat, and gut.
75
+ </MessageCard.Insight>
76
+ </View>
77
+ )
78
+
79
+ export const InsightSupporting = () => (
80
+ <View style={styles.column}>
81
+ <MessageCard.Insight variant="supporting" icon={CheckCircle}>
82
+ Studies show fresh food improves dogs' poos, skin, coat, and gut.
83
+ </MessageCard.Insight>
84
+ </View>
85
+ )
86
+
87
+ export const BannerAlt = () => (
88
+ <View style={styles.column}>
89
+ <MessageCard.Banner
90
+ media={{ type: "illustration", illustration: AlsatianChefsHat }}
91
+ title="Short headline"
92
+ linkLabel="Find out more"
93
+ linkHref="https://butternutbox.com"
94
+ >
95
+ Lorem ipsum dolor sit amet consectetur sit varius.
96
+ </MessageCard.Banner>
97
+ </View>
98
+ )
99
+
100
+ export const BannerSecondary = () => (
101
+ <View style={styles.column}>
102
+ <MessageCard.Banner
103
+ colourScheme="secondary"
104
+ media={{ type: "illustration", illustration: AlsatianChefsHat }}
105
+ title="Short headline"
106
+ linkLabel="Find out more"
107
+ linkHref="https://butternutbox.com"
108
+ >
109
+ Lorem ipsum dolor sit amet consectetur sit varius.
110
+ </MessageCard.Banner>
111
+ </View>
112
+ )
113
+
114
+ export const BannerImage = () => (
115
+ <View style={styles.column}>
116
+ <MessageCard.Banner
117
+ media={{
118
+ type: "image",
119
+ source: {
120
+ uri: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600"
121
+ },
122
+ alt: "Fresh dog food ingredients"
123
+ }}
124
+ title="Short headline"
125
+ linkLabel="Find out more"
126
+ linkHref="https://butternutbox.com"
127
+ >
128
+ Lorem ipsum dolor sit amet consectetur sit varius.
129
+ </MessageCard.Banner>
130
+ </View>
131
+ )
132
+
133
+ export const AllVariants = () => (
134
+ <View style={styles.column}>
135
+ <MessageCard.Insight icon={CheckCircle} title="Healthy from top to tail">
136
+ Studies show fresh food improves dogs' poos, skin, coat, and gut.
137
+ </MessageCard.Insight>
138
+ <MessageCard.Banner
139
+ media={{ type: "illustration", illustration: AlsatianChefsHat }}
140
+ title="Short headline"
141
+ linkLabel="Find out more"
142
+ linkHref="https://butternutbox.com"
143
+ >
144
+ Lorem ipsum dolor sit amet consectetur sit varius.
145
+ </MessageCard.Banner>
146
+ <MessageCard.Banner
147
+ media={{
148
+ type: "image",
149
+ source: {
150
+ uri: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600"
151
+ },
152
+ alt: "Fresh dog food ingredients"
153
+ }}
154
+ title="Short headline"
155
+ linkLabel="Find out more"
156
+ linkHref="https://butternutbox.com"
157
+ >
158
+ Lorem ipsum dolor sit amet consectetur sit varius.
159
+ </MessageCard.Banner>
160
+ </View>
161
+ )
162
+
163
+ const styles = StyleSheet.create({
164
+ column: {
165
+ flexDirection: "column",
166
+ gap: 16,
167
+ width: 358
168
+ }
169
+ })