@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,426 @@
1
+ import React from "react"
2
+ import { View, Pressable, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import {
6
+ RemoveCircle,
7
+ CheckCircle,
8
+ Error as ErrorIcon,
9
+ Info,
10
+ Close
11
+ } from "@butternutbox/pawprint-icons/core"
12
+ import { Icon } from "../../atoms/Icon"
13
+ import { Typography } from "../../atoms/Typography"
14
+ import { Link } from "../../atoms/Link"
15
+
16
+ // ─── Types ────────────────────────────────────────────────────────────────────
17
+
18
+ type NotificationType = "error" | "success" | "warning" | "info"
19
+ type NotificationSize = "sm" | "lg"
20
+
21
+ type BaseProps = {
22
+ type?: NotificationType
23
+ showIcon?: boolean
24
+ children: React.ReactNode
25
+ }
26
+
27
+ type InlineVariantProps = BaseProps & {
28
+ variant?: "inline"
29
+ title?: string
30
+ onClose?: () => void
31
+ size?: never
32
+ link?: never
33
+ }
34
+
35
+ type ToastVariantProps = BaseProps & {
36
+ variant: "toast"
37
+ onClose?: () => void
38
+ link?: { label: string; href?: string; onPress?: () => void }
39
+ title?: never
40
+ size?: never
41
+ }
42
+
43
+ type SystemVariantProps = BaseProps & {
44
+ variant: "system"
45
+ size?: NotificationSize
46
+ title?: never
47
+ onClose?: never
48
+ link?: never
49
+ }
50
+
51
+ type NotificationOwnProps =
52
+ | InlineVariantProps
53
+ | ToastVariantProps
54
+ | SystemVariantProps
55
+
56
+ export type NotificationProps = NotificationOwnProps &
57
+ Omit<ViewProps, keyof NotificationOwnProps>
58
+
59
+ // ─── Icon Map ─────────────────────────────────────────────────────────────────
60
+
61
+ const ICON_MAP: Record<
62
+ NotificationType,
63
+ React.ComponentType<{ width?: number; height?: number; color?: string }>
64
+ > = {
65
+ error: RemoveCircle,
66
+ success: CheckCircle,
67
+ warning: ErrorIcon,
68
+ info: Info
69
+ }
70
+
71
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
72
+
73
+ const parseTokenValue = (value: string): number => parseFloat(value)
74
+
75
+ // ─── Styled Components ────────────────────────────────────────────────────────
76
+
77
+ const StyledInlineRoot = styled(View)<{
78
+ rootPaddingH: number
79
+ rootPaddingV: number
80
+ rootGap: number
81
+ rootBorderRadius: number
82
+ rootBgColor: string
83
+ }>(
84
+ ({ rootPaddingH, rootPaddingV, rootGap, rootBorderRadius, rootBgColor }) => ({
85
+ flexDirection: "row",
86
+ alignItems: "flex-start",
87
+ gap: rootGap,
88
+ paddingHorizontal: rootPaddingH,
89
+ paddingVertical: rootPaddingV,
90
+ borderRadius: rootBorderRadius,
91
+ backgroundColor: rootBgColor
92
+ })
93
+ )
94
+
95
+ const StyledToastRoot = styled(View)<{
96
+ rootPaddingH: number
97
+ rootPaddingV: number
98
+ rootGap: number
99
+ rootBorderRadius: number
100
+ rootBgColor: string
101
+ rootBorderWidth: number
102
+ rootBorderColor: string
103
+ rootShadowColor: string
104
+ }>(
105
+ ({
106
+ rootPaddingH,
107
+ rootPaddingV,
108
+ rootGap,
109
+ rootBorderRadius,
110
+ rootBgColor,
111
+ rootBorderWidth,
112
+ rootBorderColor,
113
+ rootShadowColor
114
+ }) => ({
115
+ flexDirection: "row",
116
+ alignItems: "flex-start",
117
+ gap: rootGap,
118
+ paddingHorizontal: rootPaddingH,
119
+ paddingVertical: rootPaddingV,
120
+ borderRadius: rootBorderRadius,
121
+ backgroundColor: rootBgColor,
122
+ borderWidth: rootBorderWidth,
123
+ borderColor: rootBorderColor,
124
+ shadowColor: rootShadowColor,
125
+ shadowOffset: { width: 0, height: 4 },
126
+ shadowOpacity: 1,
127
+ shadowRadius: 16,
128
+ elevation: 4
129
+ })
130
+ )
131
+
132
+ const StyledSystemRoot = styled(View)<{
133
+ rootPaddingH: number
134
+ rootPaddingTop: number
135
+ rootPaddingBottom: number
136
+ rootGap: number
137
+ rootBgColor: string
138
+ rootAlignCenter: boolean
139
+ }>(
140
+ ({
141
+ rootPaddingH,
142
+ rootPaddingTop,
143
+ rootPaddingBottom,
144
+ rootGap,
145
+ rootBgColor,
146
+ rootAlignCenter
147
+ }) => ({
148
+ flexDirection: "row",
149
+ alignItems: rootAlignCenter ? "center" : "flex-start",
150
+ justifyContent: rootAlignCenter ? "center" : undefined,
151
+ gap: rootGap,
152
+ paddingHorizontal: rootPaddingH,
153
+ paddingTop: rootPaddingTop,
154
+ paddingBottom: rootPaddingBottom,
155
+ backgroundColor: rootBgColor,
156
+ width: "100%"
157
+ })
158
+ )
159
+
160
+ const StyledContents = styled(View)<{
161
+ contentsGap: number
162
+ }>(({ contentsGap }) => ({
163
+ flexDirection: "column",
164
+ alignItems: "flex-start",
165
+ flex: 1,
166
+ minWidth: 0,
167
+ gap: contentsGap
168
+ }))
169
+
170
+ const StyledCopyRow = styled(View)<{
171
+ copyGap: number
172
+ }>(({ copyGap }) => ({
173
+ flexDirection: "row",
174
+ alignItems: "flex-start",
175
+ gap: copyGap,
176
+ width: "100%"
177
+ }))
178
+
179
+ const StyledInlineCopy = styled(View)<{
180
+ copyGap: number
181
+ }>(({ copyGap }) => ({
182
+ flexDirection: "column",
183
+ alignItems: "flex-start",
184
+ gap: copyGap,
185
+ flex: 1,
186
+ minWidth: 0
187
+ }))
188
+
189
+ const StyledLinkWrapper = styled(View)<{
190
+ linkPaddingLeft: number
191
+ }>(({ linkPaddingLeft }) => ({
192
+ paddingLeft: linkPaddingLeft,
193
+ width: "100%"
194
+ }))
195
+
196
+ const StyledCloseButton = styled(Pressable)({
197
+ alignItems: "center",
198
+ justifyContent: "center",
199
+ flexShrink: 0,
200
+ padding: 0
201
+ })
202
+
203
+ // ─── Notification ─────────────────────────────────────────────────────────────
204
+
205
+ /**
206
+ * Displays contextual feedback messages to users. Supports three layout
207
+ * variants — inline (within content flow), toast (floating overlay), and
208
+ * system (full-width banner) — each with four severity types.
209
+ *
210
+ * Note: Unlike the web version, toast link uses `onPress` callback instead
211
+ * of `href` for navigation. The `href` prop is also available and opens
212
+ * the URL via `Linking`.
213
+ *
214
+ * @param {"inline" | "toast" | "system"} [variant="inline"] - Layout variant.
215
+ * @param {"error" | "success" | "warning" | "info"} [type="error"] - Severity type controlling colours and icon.
216
+ * @param {boolean} [showIcon=true] - Whether to show the status icon.
217
+ * @param {string} [title] - Optional headline (inline variant only).
218
+ * @param {() => void} [onClose] - Close callback (inline and toast variants).
219
+ * @param {{ label: string; href?: string; onPress?: () => void }} [link] - Optional action link (toast variant only).
220
+ * @param {"sm" | "lg"} [size="sm"] - Size variant (system variant only).
221
+ * @param {React.ReactNode} children - The notification body text.
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * import { Notification } from "@butternutbox/pawprint-native"
226
+ *
227
+ * <Notification type="success" title="Order confirmed">
228
+ * Your order has been placed successfully.
229
+ * </Notification>
230
+ * ```
231
+ */
232
+ export const Notification = React.forwardRef<View, NotificationProps>(
233
+ (props, ref) => {
234
+ const {
235
+ variant = "inline",
236
+ type = "error",
237
+ showIcon = true,
238
+ children,
239
+ ...rest
240
+ } = props
241
+
242
+ const title = "title" in props ? props.title : undefined
243
+ const onClose = "onClose" in props ? props.onClose : undefined
244
+ const link = "link" in props ? props.link : undefined
245
+ const size: NotificationSize =
246
+ "size" in props && props.size ? props.size : "sm"
247
+
248
+ const theme = useTheme()
249
+ const { notifications, systemNotifications, toast } =
250
+ theme.tokens.components
251
+
252
+ const bgColours = systemNotifications.notification.colour.background
253
+ const bgMap: Record<NotificationType, string> = {
254
+ error: bgColours.error,
255
+ success: bgColours.success,
256
+ warning: bgColours.warning,
257
+ info: bgColours.info
258
+ }
259
+
260
+ const IconComponent = ICON_MAP[type]
261
+
262
+ if (variant === "system") {
263
+ const isLarge = size === "lg"
264
+ const sizeTokens = isLarge
265
+ ? systemNotifications.notification.spacing.large
266
+ : systemNotifications.notification.spacing.small
267
+
268
+ return (
269
+ <StyledSystemRoot
270
+ ref={ref}
271
+ accessible
272
+ accessibilityRole="summary"
273
+ rootPaddingH={parseTokenValue(sizeTokens.horizontalPadding)}
274
+ rootPaddingTop={parseTokenValue(sizeTokens.topPadding)}
275
+ rootPaddingBottom={parseTokenValue(sizeTokens.bottomPadding)}
276
+ rootGap={parseTokenValue(sizeTokens.content.gap)}
277
+ rootBgColor={bgMap[type]}
278
+ rootAlignCenter={isLarge}
279
+ {...(rest as ViewProps)}
280
+ >
281
+ {showIcon && (
282
+ <Icon
283
+ icon={IconComponent}
284
+ size="md"
285
+ colour={type}
286
+ aria-label={type}
287
+ />
288
+ )}
289
+ <Typography
290
+ token={systemNotifications.notifications.typography.default}
291
+ color={systemNotifications.notification.colour.text.default}
292
+ >
293
+ {children}
294
+ </Typography>
295
+ </StyledSystemRoot>
296
+ )
297
+ }
298
+
299
+ if (variant === "toast") {
300
+ return (
301
+ <StyledToastRoot
302
+ ref={ref}
303
+ accessible
304
+ accessibilityRole="alert"
305
+ rootPaddingH={parseTokenValue(toast.spacing.horizontalPadding)}
306
+ rootPaddingV={parseTokenValue(toast.spacing.verticalPadding)}
307
+ rootGap={parseTokenValue(toast.spacing.gap)}
308
+ rootBorderRadius={parseTokenValue(toast.borderRadius.default)}
309
+ rootBgColor={toast.colour.background.default}
310
+ rootBorderWidth={parseTokenValue(toast.borderWidth.default)}
311
+ rootBorderColor={toast.colour.border.default}
312
+ rootShadowColor={toast.shaddow.default.color}
313
+ {...(rest as ViewProps)}
314
+ >
315
+ <StyledContents
316
+ contentsGap={parseTokenValue(toast.content.spacing.gap)}
317
+ >
318
+ <StyledCopyRow copyGap={parseTokenValue(toast.copy.spacing.gap)}>
319
+ {showIcon && (
320
+ <Icon
321
+ icon={IconComponent}
322
+ size="md"
323
+ colour={type}
324
+ aria-label={type}
325
+ />
326
+ )}
327
+ <View style={{ flex: 1, minWidth: 0 }}>
328
+ <Typography
329
+ token={toast.typography.body}
330
+ color={toast.colour.text.body}
331
+ >
332
+ {children}
333
+ </Typography>
334
+ </View>
335
+ </StyledCopyRow>
336
+ {link && (
337
+ <StyledLinkWrapper
338
+ linkPaddingLeft={parseTokenValue(
339
+ toast.link.spacing.leftPadding
340
+ )}
341
+ >
342
+ <Link
343
+ href={link.href}
344
+ onPress={link.onPress}
345
+ weight="semiBold"
346
+ size="md"
347
+ >
348
+ {link.label}
349
+ </Link>
350
+ </StyledLinkWrapper>
351
+ )}
352
+ </StyledContents>
353
+ {onClose && (
354
+ <StyledCloseButton
355
+ onPress={onClose}
356
+ accessibilityRole="button"
357
+ accessibilityLabel="Close notification"
358
+ >
359
+ <Icon icon={Close} size="xs" aria-label="Close" />
360
+ </StyledCloseButton>
361
+ )}
362
+ </StyledToastRoot>
363
+ )
364
+ }
365
+
366
+ // inline (default)
367
+ const { spacing, borderRadius, content, colour } =
368
+ notifications.notification
369
+
370
+ return (
371
+ <StyledInlineRoot
372
+ ref={ref}
373
+ accessible
374
+ accessibilityRole="alert"
375
+ rootPaddingH={parseTokenValue(spacing.horizontalPadding)}
376
+ rootPaddingV={parseTokenValue(spacing.verticalPadding)}
377
+ rootGap={parseTokenValue(spacing.gap)}
378
+ rootBorderRadius={parseTokenValue(borderRadius.default)}
379
+ rootBgColor={bgMap[type]}
380
+ {...(rest as ViewProps)}
381
+ >
382
+ <StyledContents contentsGap={parseTokenValue(content.spacing.gap)}>
383
+ <StyledCopyRow copyGap={parseTokenValue(content.spacing.gap)}>
384
+ {showIcon && (
385
+ <Icon
386
+ icon={IconComponent}
387
+ size="md"
388
+ colour={type}
389
+ aria-label={type}
390
+ />
391
+ )}
392
+ <StyledInlineCopy
393
+ copyGap={parseTokenValue(content.copy.spacing.gap)}
394
+ >
395
+ {title && (
396
+ <Typography
397
+ token={notifications.typography.title}
398
+ color={colour.text.title}
399
+ >
400
+ {title}
401
+ </Typography>
402
+ )}
403
+ <Typography
404
+ token={notifications.typography.body}
405
+ color={colour.text.body}
406
+ >
407
+ {children}
408
+ </Typography>
409
+ </StyledInlineCopy>
410
+ </StyledCopyRow>
411
+ </StyledContents>
412
+ {onClose && (
413
+ <StyledCloseButton
414
+ onPress={onClose}
415
+ accessibilityRole="button"
416
+ accessibilityLabel="Close notification"
417
+ >
418
+ <Icon icon={Close} size="xs" aria-label="Close" />
419
+ </StyledCloseButton>
420
+ )}
421
+ </StyledInlineRoot>
422
+ )
423
+ }
424
+ )
425
+
426
+ Notification.displayName = "Notification"
@@ -0,0 +1,2 @@
1
+ export { Notification } from "./Notification"
2
+ export type { NotificationProps } from "./Notification"
@@ -0,0 +1,231 @@
1
+ import React, { useState } from "react"
2
+ import { View, ScrollView, StyleSheet } from "react-native"
3
+ import { NumberField } from "./NumberField"
4
+ import type { NumberFieldProps } from "./NumberField"
5
+ import { Typography } from "../../atoms/Typography"
6
+
7
+ export default {
8
+ title: "Molecules/NumberField",
9
+ component: NumberField,
10
+ argTypes: {
11
+ size: {
12
+ control: { type: "select" },
13
+ options: ["sm", "lg"],
14
+ description: "Size variant controlling typography and spacing"
15
+ },
16
+ label: {
17
+ control: { type: "text" },
18
+ description: "Primary label text above the field"
19
+ },
20
+ smallLabel: {
21
+ control: { type: "text" },
22
+ description: "Secondary label below the primary label"
23
+ },
24
+ description: {
25
+ control: { type: "text" },
26
+ description: "Help text displayed below the field"
27
+ },
28
+ error: {
29
+ control: { type: "text" },
30
+ description: "Error message when state is error"
31
+ },
32
+ state: {
33
+ control: { type: "select" },
34
+ options: ["default", "error", "success"],
35
+ description: "Visual state of the field"
36
+ },
37
+ disabled: {
38
+ control: { type: "boolean" },
39
+ description: "Whether the input and buttons are disabled"
40
+ },
41
+ incrementDisabled: {
42
+ control: { type: "boolean" },
43
+ description: "Whether the + button is independently disabled"
44
+ },
45
+ decrementDisabled: {
46
+ control: { type: "boolean" },
47
+ description: "Whether the - button is independently disabled"
48
+ },
49
+ showIncrementButton: {
50
+ control: { type: "boolean" },
51
+ description: "Whether to show the + button"
52
+ },
53
+ showDecrementButton: {
54
+ control: { type: "boolean" },
55
+ description: "Whether to show the - button"
56
+ }
57
+ }
58
+ }
59
+
60
+ export const Default = () => {
61
+ const [value, setValue] = useState(12)
62
+
63
+ return (
64
+ <View style={styles.section}>
65
+ <NumberField
66
+ label="Weight"
67
+ size="lg"
68
+ description="Help text"
69
+ value={String(value)}
70
+ onChangeText={(t) => setValue(Number(t) || 0)}
71
+ onIncrement={() => setValue((v) => v + 1)}
72
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
73
+ />
74
+ </View>
75
+ )
76
+ }
77
+
78
+ export const Small = () => {
79
+ const [value, setValue] = useState(2)
80
+
81
+ return (
82
+ <View style={styles.section}>
83
+ <NumberField
84
+ label="Quantity"
85
+ size="sm"
86
+ value={String(value)}
87
+ onChangeText={(t) => setValue(Number(t) || 0)}
88
+ onIncrement={() => setValue((v) => v + 1)}
89
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
90
+ />
91
+ </View>
92
+ )
93
+ }
94
+
95
+ export const Error = () => {
96
+ const [value, setValue] = useState(0)
97
+
98
+ return (
99
+ <View style={styles.section}>
100
+ <NumberField
101
+ label="Weight"
102
+ size="lg"
103
+ state="error"
104
+ error="Weight must be greater than 0"
105
+ value={String(value)}
106
+ onChangeText={(t) => setValue(Number(t) || 0)}
107
+ onIncrement={() => setValue((v) => v + 1)}
108
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
109
+ />
110
+ </View>
111
+ )
112
+ }
113
+
114
+ export const Success = () => {
115
+ const [value, setValue] = useState(12)
116
+
117
+ return (
118
+ <View style={styles.section}>
119
+ <NumberField
120
+ label="Weight"
121
+ size="lg"
122
+ state="success"
123
+ value={String(value)}
124
+ onChangeText={(t) => setValue(Number(t) || 0)}
125
+ onIncrement={() => setValue((v) => v + 1)}
126
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
127
+ />
128
+ </View>
129
+ )
130
+ }
131
+
132
+ export const Disabled = () => (
133
+ <View style={styles.section}>
134
+ <NumberField label="Weight" size="lg" disabled value="12" />
135
+ </View>
136
+ )
137
+
138
+ export const ButtonDisabled = () => (
139
+ <ScrollView contentContainerStyle={styles.column}>
140
+ <View style={styles.section}>
141
+ <Typography size="sm" weight="semiBold" color="tertiary">
142
+ Decrement disabled (at min)
143
+ </Typography>
144
+ <NumberField label="Weight" size="lg" decrementDisabled value="0" />
145
+ </View>
146
+
147
+ <View style={styles.section}>
148
+ <Typography size="sm" weight="semiBold" color="tertiary">
149
+ Increment disabled (at max)
150
+ </Typography>
151
+ <NumberField label="Weight" size="lg" incrementDisabled value="99" />
152
+ </View>
153
+ </ScrollView>
154
+ )
155
+
156
+ export const ButtonVisibility = () => (
157
+ <ScrollView contentContainerStyle={styles.column}>
158
+ <View style={styles.section}>
159
+ <Typography size="sm" weight="semiBold" color="tertiary">
160
+ Increment only
161
+ </Typography>
162
+ <NumberField
163
+ label="Amount"
164
+ size="lg"
165
+ showDecrementButton={false}
166
+ value="10"
167
+ />
168
+ </View>
169
+
170
+ <View style={styles.section}>
171
+ <Typography size="sm" weight="semiBold" color="tertiary">
172
+ No buttons
173
+ </Typography>
174
+ <NumberField
175
+ label="Amount"
176
+ size="lg"
177
+ showIncrementButton={false}
178
+ showDecrementButton={false}
179
+ value="3"
180
+ />
181
+ </View>
182
+ </ScrollView>
183
+ )
184
+
185
+ export const Controlled = () => {
186
+ const [value, setValue] = useState(5)
187
+
188
+ return (
189
+ <View style={styles.section}>
190
+ <Typography size="sm" weight="semiBold" color="tertiary">
191
+ Value: {value}
192
+ </Typography>
193
+ <NumberField
194
+ label="Quantity"
195
+ size="lg"
196
+ value={String(value)}
197
+ onChangeText={(t) => setValue(Number(t) || 0)}
198
+ onIncrement={() => setValue((v) => v + 1)}
199
+ onDecrement={() => setValue((v) => Math.max(0, v - 1))}
200
+ />
201
+ </View>
202
+ )
203
+ }
204
+
205
+ export const Playground = {
206
+ args: {
207
+ size: "lg",
208
+ label: "Weight",
209
+ smallLabel: "",
210
+ description: "Help text",
211
+ error: "",
212
+ state: "default",
213
+ disabled: false,
214
+ incrementDisabled: false,
215
+ decrementDisabled: false,
216
+ showIncrementButton: true,
217
+ showDecrementButton: true
218
+ },
219
+ render: (args: NumberFieldProps) => <NumberField {...args} defaultValue="0" />
220
+ }
221
+
222
+ const styles = StyleSheet.create({
223
+ column: {
224
+ flexDirection: "column",
225
+ gap: 32
226
+ },
227
+ section: {
228
+ flexDirection: "column",
229
+ gap: 12
230
+ }
231
+ })