@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,362 @@
1
+ import React from "react"
2
+ import { View, ViewProps, Image, ImageSourcePropType } 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 { Illustration } from "../../atoms/Illustration"
7
+ import { Typography } from "../../atoms/Typography"
8
+ import { Link } from "../../atoms/Link"
9
+
10
+ // ─── Shared ───────────────────────────────────────────────────────────────────
11
+
12
+ type PawprintIllustration = React.ComponentType<{
13
+ width?: number
14
+ height?: number
15
+ }>
16
+
17
+ const parseTokenValue = (value: string): number => parseFloat(value)
18
+
19
+ // ─── Insight variant ──────────────────────────────────────────────────────────
20
+
21
+ type InsightVariant = "standalone" | "supporting"
22
+
23
+ type MessageCardInsightOwnProps = {
24
+ variant?: InsightVariant
25
+ icon?: PawprintIcon
26
+ illustration?: PawprintIllustration
27
+ title?: string
28
+ children: React.ReactNode
29
+ }
30
+
31
+ export type MessageCardInsightProps = MessageCardInsightOwnProps &
32
+ Omit<ViewProps, keyof MessageCardInsightOwnProps>
33
+
34
+ const StyledInsightRoot = styled(View)<{
35
+ rootGap: number
36
+ rootPaddingH: number
37
+ rootPaddingTop: number
38
+ rootPaddingBottom: number
39
+ rootBorderTopLeftRadius: number
40
+ rootBorderTopRightRadius: number
41
+ rootBorderBottomLeftRadius: number
42
+ rootBorderBottomRightRadius: number
43
+ rootBgColor: string
44
+ rootMinWidth: number
45
+ }>(
46
+ ({
47
+ rootGap,
48
+ rootPaddingH,
49
+ rootPaddingTop,
50
+ rootPaddingBottom,
51
+ rootBorderTopLeftRadius,
52
+ rootBorderTopRightRadius,
53
+ rootBorderBottomLeftRadius,
54
+ rootBorderBottomRightRadius,
55
+ rootBgColor,
56
+ rootMinWidth
57
+ }) => ({
58
+ flexDirection: "row",
59
+ alignItems: "center",
60
+ gap: rootGap,
61
+ paddingHorizontal: rootPaddingH,
62
+ paddingTop: rootPaddingTop,
63
+ paddingBottom: rootPaddingBottom,
64
+ borderTopLeftRadius: rootBorderTopLeftRadius,
65
+ borderTopRightRadius: rootBorderTopRightRadius,
66
+ borderBottomLeftRadius: rootBorderBottomLeftRadius,
67
+ borderBottomRightRadius: rootBorderBottomRightRadius,
68
+ backgroundColor: rootBgColor,
69
+ minWidth: rootMinWidth
70
+ })
71
+ )
72
+
73
+ const StyledInsightContent = styled(View)<{ contentGap: number }>(
74
+ ({ contentGap }) => ({
75
+ flex: 1,
76
+ flexDirection: "column",
77
+ minWidth: 0,
78
+ gap: contentGap
79
+ })
80
+ )
81
+
82
+ /**
83
+ * Yellow highlight card for bite-sized USPs and trust signals.
84
+ *
85
+ * @param {"standalone" | "supporting"} [variant="standalone"] - `supporting`
86
+ * renders a flat top edge when attached to a related element above.
87
+ * @param {PawprintIcon} [icon] - Optional core icon. Ignored when
88
+ * `illustration` is provided.
89
+ * @param {PawprintIllustration} [illustration] - Optional illustration; takes
90
+ * precedence over `icon`.
91
+ * @param {string} [title] - Optional headline. Renders above the body copy
92
+ * in both `standalone` and `supporting` variants.
93
+ * @param {React.ReactNode} children - Body copy.
94
+ */
95
+ const MessageCardInsight = React.forwardRef<View, MessageCardInsightProps>(
96
+ (
97
+ { variant = "standalone", icon, illustration, title, children, ...rest },
98
+ ref
99
+ ) => {
100
+ const theme = useTheme()
101
+ const { insightCard } = theme.tokens.components
102
+ const semanticSpacing = theme.tokens.semantics.dimensions.spacing
103
+
104
+ const isSupporting = variant === "supporting"
105
+ const supporting = insightCard.spacing.supporting
106
+ const standalone = insightCard.spacing.standalone
107
+
108
+ const radiusStandalone = parseTokenValue(
109
+ insightCard.borderRadius.standalone.default
110
+ )
111
+ const radiusSupporting = parseTokenValue(
112
+ insightCard.borderRadius.supporting.bottom
113
+ )
114
+
115
+ return (
116
+ <StyledInsightRoot
117
+ ref={ref}
118
+ rootGap={parseTokenValue(
119
+ isSupporting ? supporting.gap : standalone.gap
120
+ )}
121
+ rootPaddingH={parseTokenValue(
122
+ isSupporting ? semanticSpacing.md : standalone.horizontalPadding
123
+ )}
124
+ rootPaddingTop={parseTokenValue(
125
+ isSupporting ? supporting.topPadding : standalone.verticalPadding
126
+ )}
127
+ rootPaddingBottom={parseTokenValue(
128
+ isSupporting ? supporting.bottomPadding : standalone.verticalPadding
129
+ )}
130
+ rootBorderTopLeftRadius={isSupporting ? 0 : radiusStandalone}
131
+ rootBorderTopRightRadius={isSupporting ? 0 : radiusStandalone}
132
+ rootBorderBottomLeftRadius={
133
+ isSupporting ? radiusSupporting : radiusStandalone
134
+ }
135
+ rootBorderBottomRightRadius={
136
+ isSupporting ? radiusSupporting : radiusStandalone
137
+ }
138
+ rootBgColor={insightCard.colour.background.default}
139
+ rootMinWidth={parseTokenValue(insightCard.size.minWidth)}
140
+ accessible
141
+ accessibilityRole="summary"
142
+ {...rest}
143
+ >
144
+ {illustration ? (
145
+ <Illustration illustration={illustration} size="sm" />
146
+ ) : icon ? (
147
+ <Icon icon={icon} size="xl" colour="primary" />
148
+ ) : null}
149
+ <StyledInsightContent
150
+ contentGap={parseTokenValue(standalone.content.gap)}
151
+ >
152
+ {title && (
153
+ <Typography
154
+ token={insightCard.typography.title}
155
+ color={insightCard.colour.text.title}
156
+ >
157
+ {title}
158
+ </Typography>
159
+ )}
160
+ <Typography
161
+ token={insightCard.typography.subText}
162
+ color={insightCard.colour.text.subText}
163
+ >
164
+ {children}
165
+ </Typography>
166
+ </StyledInsightContent>
167
+ </StyledInsightRoot>
168
+ )
169
+ }
170
+ )
171
+
172
+ MessageCardInsight.displayName = "MessageCard.Insight"
173
+
174
+ // ─── Banner variant ───────────────────────────────────────────────────────────
175
+
176
+ type BannerColourScheme = "primary" | "secondary"
177
+
178
+ export type MessageCardBannerMedia =
179
+ | { type: "image"; source: ImageSourcePropType; alt?: string }
180
+ | { type: "illustration"; illustration: PawprintIllustration }
181
+
182
+ type MessageCardBannerOwnProps = {
183
+ colourScheme?: BannerColourScheme
184
+ media?: MessageCardBannerMedia
185
+ title?: string
186
+ children: React.ReactNode
187
+ linkLabel?: string
188
+ linkHref?: string
189
+ onLinkPress?: () => void
190
+ }
191
+
192
+ export type MessageCardBannerProps = MessageCardBannerOwnProps &
193
+ Omit<ViewProps, keyof MessageCardBannerOwnProps>
194
+
195
+ const StyledBannerRoot = styled(View)<{
196
+ rootGap: number
197
+ rootPadding: number
198
+ rootBorderRadius: number
199
+ rootBgColor: string
200
+ }>(({ rootGap, rootPadding, rootBorderRadius, rootBgColor }) => ({
201
+ flexDirection: "row",
202
+ alignItems: "center",
203
+ gap: rootGap,
204
+ padding: rootPadding,
205
+ borderRadius: rootBorderRadius,
206
+ backgroundColor: rootBgColor
207
+ }))
208
+
209
+ const StyledBannerImage = styled(View)<{
210
+ imageRadius: number
211
+ imageBgColor: string
212
+ }>(({ imageRadius, imageBgColor }) => ({
213
+ flexShrink: 0,
214
+ width: 108,
215
+ height: 108,
216
+ borderRadius: imageRadius,
217
+ overflow: "hidden",
218
+ backgroundColor: imageBgColor
219
+ }))
220
+
221
+ const StyledBannerIllustrationSlot = styled(View)({
222
+ flexShrink: 0,
223
+ width: 108,
224
+ height: 108,
225
+ alignItems: "center",
226
+ justifyContent: "center"
227
+ })
228
+
229
+ const StyledBannerContents = styled(View)<{ contentsGap: number }>(
230
+ ({ contentsGap }) => ({
231
+ flex: 1,
232
+ flexDirection: "column",
233
+ alignItems: "flex-start",
234
+ justifyContent: "center",
235
+ gap: contentsGap,
236
+ minWidth: 0
237
+ })
238
+ )
239
+
240
+ const StyledBannerCopy = styled(View)<{ copyGap: number }>(({ copyGap }) => ({
241
+ flexDirection: "column",
242
+ alignItems: "flex-start",
243
+ gap: copyGap,
244
+ width: "100%"
245
+ }))
246
+
247
+ /**
248
+ * Horizontal card with an image or illustration, title, body copy and an
249
+ * optional link CTA.
250
+ *
251
+ * @param {"primary" | "secondary"} [colourScheme="primary"] - Background
252
+ * scheme. `primary` is saturated yellow; `secondary` is a lighter cream.
253
+ * @param {MessageCardBannerMedia} [media] - Tagged slot accepting either
254
+ * `{ type: "image", source, alt }` or `{ type: "illustration", illustration }`.
255
+ * @param {string} [title] - Optional headline.
256
+ * @param {React.ReactNode} children - Body copy.
257
+ * @param {string} [linkLabel] - CTA label. When provided with `linkHref` or
258
+ * `onLinkPress`, renders a link row.
259
+ * @param {string} [linkHref] - URL opened via `Linking` when pressed.
260
+ * @param {() => void} [onLinkPress] - Callback when the CTA is pressed.
261
+ */
262
+ const MessageCardBanner = React.forwardRef<View, MessageCardBannerProps>(
263
+ (
264
+ {
265
+ colourScheme = "primary",
266
+ media,
267
+ title,
268
+ children,
269
+ linkLabel,
270
+ linkHref,
271
+ onLinkPress,
272
+ ...rest
273
+ },
274
+ ref
275
+ ) => {
276
+ const theme = useTheme()
277
+ const { colour, dimensions } = theme.tokens.semantics
278
+
279
+ const bgColor =
280
+ colourScheme === "secondary"
281
+ ? colour.background.container.secondary
282
+ : colour.background.container.alt
283
+
284
+ return (
285
+ <StyledBannerRoot
286
+ ref={ref}
287
+ rootGap={parseTokenValue(dimensions.spacing.md)}
288
+ rootPadding={parseTokenValue(dimensions.spacing.md)}
289
+ rootBorderRadius={parseTokenValue(dimensions.borderRadius.md)}
290
+ rootBgColor={bgColor}
291
+ accessible
292
+ accessibilityRole="summary"
293
+ {...rest}
294
+ >
295
+ {media?.type === "illustration" ? (
296
+ <StyledBannerIllustrationSlot>
297
+ {React.createElement(media.illustration, {
298
+ width: 108,
299
+ height: 108
300
+ })}
301
+ </StyledBannerIllustrationSlot>
302
+ ) : media?.type === "image" ? (
303
+ <StyledBannerImage
304
+ imageRadius={parseTokenValue(dimensions.borderRadius.sm)}
305
+ imageBgColor={colour.background.surface.secondary}
306
+ >
307
+ <Image
308
+ source={media.source}
309
+ accessibilityLabel={media.alt}
310
+ style={{ width: "100%", height: "100%" }}
311
+ resizeMode="cover"
312
+ />
313
+ </StyledBannerImage>
314
+ ) : null}
315
+ <StyledBannerContents
316
+ contentsGap={parseTokenValue(dimensions.spacing.xs)}
317
+ >
318
+ <StyledBannerCopy
319
+ copyGap={parseTokenValue(dimensions.spacing["3xs"])}
320
+ >
321
+ {title && (
322
+ <Typography variant="heading" size="xs" color="primary">
323
+ {title}
324
+ </Typography>
325
+ )}
326
+ <Typography variant="body" size="md" color="secondary">
327
+ {children}
328
+ </Typography>
329
+ </StyledBannerCopy>
330
+ {linkLabel && (linkHref || onLinkPress) && (
331
+ <Link
332
+ href={linkHref}
333
+ onPress={onLinkPress}
334
+ standalone
335
+ size="md"
336
+ weight="semiBold"
337
+ >
338
+ {linkLabel}
339
+ </Link>
340
+ )}
341
+ </StyledBannerContents>
342
+ </StyledBannerRoot>
343
+ )
344
+ }
345
+ )
346
+
347
+ MessageCardBanner.displayName = "MessageCard.Banner"
348
+
349
+ // ─── Public namespace ─────────────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Compound component exposing two sub-components:
353
+ *
354
+ * - `MessageCard.Insight` — yellow trust-signal card
355
+ * - `MessageCard.Banner` — horizontal image/illustration + copy + optional CTA
356
+ */
357
+ export const MessageCard = {
358
+ Insight: MessageCardInsight,
359
+ Banner: MessageCardBanner
360
+ }
361
+
362
+ export { MessageCardInsight, MessageCardBanner }
@@ -0,0 +1,10 @@
1
+ export {
2
+ MessageCard,
3
+ MessageCardInsight,
4
+ MessageCardBanner
5
+ } from "./MessageCard"
6
+ export type {
7
+ MessageCardInsightProps,
8
+ MessageCardBannerProps,
9
+ MessageCardBannerMedia
10
+ } from "./MessageCard"
@@ -0,0 +1,219 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { Notification } from "./Notification"
4
+
5
+ export default {
6
+ title: "Molecules/Notification",
7
+ component: Notification,
8
+ argTypes: {
9
+ variant: {
10
+ control: "select",
11
+ options: ["inline", "toast", "system"],
12
+ description: "Layout variant"
13
+ },
14
+ type: {
15
+ control: "select",
16
+ options: ["error", "success", "warning", "info"],
17
+ description: "Severity type controlling colours and icon"
18
+ },
19
+ showIcon: {
20
+ control: "boolean",
21
+ description: "Whether to show the status icon"
22
+ },
23
+ title: {
24
+ control: "text",
25
+ description: "Optional headline (inline variant only)"
26
+ },
27
+ children: {
28
+ control: "text",
29
+ description: "Notification body text"
30
+ }
31
+ }
32
+ }
33
+
34
+ // --- Playground ---
35
+
36
+ export const Playground = {
37
+ args: {
38
+ variant: "inline",
39
+ type: "error",
40
+ showIcon: true,
41
+ title: "Optional headline here",
42
+ children: "Try to keep the copy length here to a maximum of 2 lines"
43
+ }
44
+ }
45
+
46
+ // --- Inline / All Types ---
47
+
48
+ export const AllInlineVariants = {
49
+ name: "Inline / All Types",
50
+ render: () => (
51
+ <View style={styles.column}>
52
+ <Notification
53
+ type="error"
54
+ title="Optional headline here"
55
+ onClose={() => {}}
56
+ >
57
+ Try to keep the copy length here to a maximum of 2 lines
58
+ </Notification>
59
+ <Notification
60
+ type="success"
61
+ title="Optional headline here"
62
+ onClose={() => {}}
63
+ >
64
+ Try to keep the copy length here to a maximum of 2 lines
65
+ </Notification>
66
+ <Notification
67
+ type="warning"
68
+ title="Optional headline here"
69
+ onClose={() => {}}
70
+ >
71
+ Try to keep the copy length here to a maximum of 2 lines
72
+ </Notification>
73
+ <Notification
74
+ type="info"
75
+ title="Optional headline here"
76
+ onClose={() => {}}
77
+ >
78
+ Try to keep the copy length here to a maximum of 2 lines
79
+ </Notification>
80
+ </View>
81
+ )
82
+ }
83
+
84
+ // --- Toast / All Types ---
85
+
86
+ export const AllToastVariants = {
87
+ name: "Toast / All Types",
88
+ render: () => (
89
+ <View style={styles.column}>
90
+ <Notification
91
+ variant="toast"
92
+ type="error"
93
+ onClose={() => {}}
94
+ link={{ label: "Optional link" }}
95
+ >
96
+ Lorem ipsum dolor sit amet consectetur. Massa vestibulum eleifend sed
97
+ phasellus duis odio nunc massa.
98
+ </Notification>
99
+ <Notification
100
+ variant="toast"
101
+ type="success"
102
+ onClose={() => {}}
103
+ link={{ label: "Optional link" }}
104
+ >
105
+ Lorem ipsum dolor sit amet consectetur. Massa vestibulum eleifend sed
106
+ phasellus duis odio nunc massa.
107
+ </Notification>
108
+ <Notification
109
+ variant="toast"
110
+ type="warning"
111
+ onClose={() => {}}
112
+ link={{ label: "Optional link" }}
113
+ >
114
+ Lorem ipsum dolor sit amet consectetur. Massa vestibulum eleifend sed
115
+ phasellus duis odio nunc massa.
116
+ </Notification>
117
+ <Notification
118
+ variant="toast"
119
+ type="info"
120
+ onClose={() => {}}
121
+ link={{ label: "Optional link" }}
122
+ >
123
+ Lorem ipsum dolor sit amet consectetur. Massa vestibulum eleifend sed
124
+ phasellus duis odio nunc massa.
125
+ </Notification>
126
+ </View>
127
+ )
128
+ }
129
+
130
+ // --- System / Small ---
131
+
132
+ export const SystemSmall = {
133
+ name: "System / Small",
134
+ render: () => (
135
+ <View style={styles.column}>
136
+ <Notification variant="system" type="error" size="sm">
137
+ Your order is confirmed and will be with you within two weeks
138
+ </Notification>
139
+ <Notification variant="system" type="success" size="sm">
140
+ Your order is confirmed and will be with you within two weeks
141
+ </Notification>
142
+ <Notification variant="system" type="warning" size="sm">
143
+ Your order is confirmed and will be with you within two weeks
144
+ </Notification>
145
+ <Notification variant="system" type="info" size="sm">
146
+ Your order is confirmed and will be with you within two weeks
147
+ </Notification>
148
+ </View>
149
+ )
150
+ }
151
+
152
+ // --- System / Large ---
153
+
154
+ export const SystemLarge = {
155
+ name: "System / Large",
156
+ render: () => (
157
+ <View style={styles.column}>
158
+ <Notification variant="system" type="error" size="lg">
159
+ Due to a nationwide issue with DPD, we are experiencing 24-48 hour
160
+ delivery delays on all boxes.
161
+ </Notification>
162
+ <Notification variant="system" type="success" size="lg">
163
+ Due to a nationwide issue with DPD, we are experiencing 24-48 hour
164
+ delivery delays on all boxes.
165
+ </Notification>
166
+ <Notification variant="system" type="warning" size="lg">
167
+ Due to a nationwide issue with DPD, we are experiencing 24-48 hour
168
+ delivery delays on all boxes.
169
+ </Notification>
170
+ <Notification variant="system" type="info" size="lg">
171
+ Due to a nationwide issue with DPD, we are experiencing 24-48 hour
172
+ delivery delays on all boxes.
173
+ </Notification>
174
+ </View>
175
+ )
176
+ }
177
+
178
+ // --- States ---
179
+
180
+ export const WithoutIcon = {
181
+ name: "Without Icon",
182
+ render: () => (
183
+ <View style={styles.column}>
184
+ <Notification type="info" showIcon={false} title="No icon">
185
+ This notification has the icon hidden.
186
+ </Notification>
187
+ </View>
188
+ )
189
+ }
190
+
191
+ export const WithoutTitle = {
192
+ name: "Inline / Without Title",
193
+ render: () => (
194
+ <View style={styles.column}>
195
+ <Notification type="warning">
196
+ This inline notification has no title, just body text.
197
+ </Notification>
198
+ </View>
199
+ )
200
+ }
201
+
202
+ export const ToastWithoutLink = {
203
+ name: "Toast / Without Link",
204
+ render: () => (
205
+ <View style={styles.column}>
206
+ <Notification variant="toast" type="success" onClose={() => {}}>
207
+ A simple toast notification without a link.
208
+ </Notification>
209
+ </View>
210
+ )
211
+ }
212
+
213
+ const styles = StyleSheet.create({
214
+ column: {
215
+ flexDirection: "column",
216
+ gap: 16,
217
+ padding: 16
218
+ }
219
+ })