@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
@@ -3,20 +3,32 @@ import { View } from "react-native"
3
3
  import * as AvatarPrimitive from "@rn-primitives/avatar"
4
4
  import styled from "@emotion/native"
5
5
  import { Typography } from "../Typography"
6
+ import { Icon, type IconSize } from "../Icon"
7
+ import { DogHeadOutlinedPrimary } from "@butternutbox/pawprint-icons/marketing"
6
8
 
7
9
  export type AvatarSize = "sm" | "md" | "lg"
8
- export type AvatarBorder = "none" | "sm" | "md"
9
10
  export type AvatarFallbackType = "string" | "image"
10
11
 
11
- export interface AvatarProps {
12
+ type SmallAvatarProps = {
13
+ size?: "sm"
14
+ border?: "none" | "sm"
15
+ }
16
+
17
+ type MediumLargeAvatarProps = {
18
+ size?: "md" | "lg"
19
+ border?: "none" | "md"
20
+ }
21
+
22
+ type BaseAvatarProps = {
12
23
  src?: string
13
24
  alt: string
14
- size?: AvatarSize
15
- border?: AvatarBorder
16
25
  fallbackType?: AvatarFallbackType
17
26
  fallbackString?: string
18
27
  }
19
28
 
29
+ export type AvatarProps = BaseAvatarProps &
30
+ (SmallAvatarProps | MediumLargeAvatarProps)
31
+
20
32
  const SIZE_MAP = {
21
33
  sm: "small",
22
34
  md: "medium",
@@ -27,7 +39,7 @@ const parseTokenValue = (value: string): number => parseFloat(value)
27
39
 
28
40
  const StyledRoot = styled(AvatarPrimitive.Root)<{
29
41
  size: AvatarSize
30
- border: AvatarBorder
42
+ border: "none" | "sm" | "md"
31
43
  }>(({ theme, size, border }) => {
32
44
  const { avatar } = theme.tokens.components
33
45
  const { borderRadius } = theme.tokens.semantics.dimensions
@@ -67,17 +79,43 @@ const StyledFallback = styled(AvatarPrimitive.Fallback)(({ theme }) => {
67
79
  justifyContent: "center",
68
80
  width: "100%",
69
81
  height: "100%",
70
- backgroundColor: background.container.disabled,
71
- color: text.disabled
82
+ backgroundColor: background.container.secondary,
83
+ color: text.primary
84
+ }
85
+ })
86
+
87
+ const StyledIcon = styled(Icon)<{ avatarSize: AvatarSize }>(({
88
+ theme,
89
+ avatarSize
90
+ }) => {
91
+ const {
92
+ icons: {
93
+ sizing: { icons }
94
+ }
95
+ } = theme.tokens.components
96
+
97
+ const iconSize = parseTokenValue(
98
+ icons.core[AVATAR_SIZE_TO_ICON_SIZE[avatarSize] as keyof typeof icons.core]
99
+ )
100
+
101
+ return {
102
+ width: iconSize,
103
+ height: iconSize
72
104
  }
73
105
  })
74
106
 
75
107
  const AVATAR_TO_BODY_SIZE = {
76
- sm: "xs",
77
- md: "sm",
108
+ sm: "sm",
109
+ md: "md",
78
110
  lg: "md"
79
111
  } as const
80
112
 
113
+ const AVATAR_SIZE_TO_ICON_SIZE: Record<AvatarSize, IconSize> = {
114
+ sm: "md",
115
+ md: "md",
116
+ lg: "xl"
117
+ }
118
+
81
119
  /**
82
120
  * Avatar component for displaying user profile pictures or fallback initials.
83
121
  * Built on @rn-primitives/avatar with design system styling.
@@ -105,10 +143,19 @@ const AVATAR_TO_BODY_SIZE = {
105
143
  *
106
144
  * @param {string} [src] - Image source URL.
107
145
  * @param {string} alt - Accessible label for the avatar.
108
- * @param {"sm" | "md" | "lg"} [size="md"] - Size variant.
109
- * @param {"none" | "sm" | "md"} [border="none"] - Border width variant.
110
- * @param {"string" | "image"} [fallbackType] - Fallback type (auto-detected based on fallbackString if not specified).
111
- * @param {string} [fallbackString] - Text to display as fallback (typically user initials).
146
+ * @param {"sm" | "md" | "lg"} [size="md"] - Avatar size:
147
+ * - `sm`: Small (supports `none` or `sm` border)
148
+ * - `md`: Medium (supports `none` or `md` border)
149
+ * - `lg`: Large (supports `none` or `md` border)
150
+ * @param {"none" | "sm" | "md"} [border="none"] - Border width:
151
+ * - `none`: No border
152
+ * - `sm`: Small border (only for small avatars)
153
+ * - `md`: Medium border (only for medium/large avatars)
154
+ * @param {"string" | "image"} [fallbackType] - Fallback display when image unavailable:
155
+ * - `string`: Shows initials from `fallbackString`
156
+ * - `image`: Shows dog head icon (default if no `fallbackString` provided)
157
+ * @param {string} [fallbackString] - Initials to display (1-2 characters recommended).
158
+ * Automatically sets `fallbackType` to `"string"` when provided.
112
159
  */
113
160
  const AvatarRoot = React.forwardRef<View, AvatarProps>(
114
161
  (
@@ -135,18 +182,17 @@ const AvatarRoot = React.forwardRef<View, AvatarProps>(
135
182
  {src && <StyledImage source={{ uri: src }} />}
136
183
  <StyledFallback>
137
184
  {showStringFallback ? (
138
- <Typography
139
- size={AVATAR_TO_BODY_SIZE[size]}
140
- weight="bold"
141
- color="disabled"
142
- >
185
+ <Typography size={AVATAR_TO_BODY_SIZE[size]} weight="bold">
143
186
  {fallbackString}
144
187
  </Typography>
145
188
  ) : (
146
- // TODO: replace with the Icon when available
147
- <Typography size={AVATAR_TO_BODY_SIZE[size]} color="disabled">
148
- 👤
149
- </Typography>
189
+ <StyledIcon
190
+ avatarSize={size}
191
+ icon={DogHeadOutlinedPrimary}
192
+ size={AVATAR_SIZE_TO_ICON_SIZE[size]}
193
+ colour="primary"
194
+ aria-label="Profile image"
195
+ />
150
196
  )}
151
197
  </StyledFallback>
152
198
  </StyledRoot>
@@ -1,7 +1,2 @@
1
1
  export { Avatar } from "./Avatar"
2
- export type {
3
- AvatarProps,
4
- AvatarSize,
5
- AvatarBorder,
6
- AvatarFallbackType
7
- } from "./Avatar"
2
+ export type { AvatarProps, AvatarSize, AvatarFallbackType } from "./Avatar"
@@ -1,28 +1,9 @@
1
1
  import React from "react"
2
2
  import { View, StyleSheet, Text } from "react-native"
3
+ import { CheckCircle } from "@butternutbox/pawprint-icons/core"
3
4
  import { Badge } from "./Badge"
4
5
  import type { BadgeProps } from "./Badge"
5
6
 
6
- const PlaceholderIcon = ({
7
- width = 24,
8
- height = 24,
9
- color = "#000"
10
- }: {
11
- width?: number
12
- height?: number
13
- color?: string
14
- }) => (
15
- <View
16
- style={{
17
- width,
18
- height,
19
- borderRadius: 4,
20
- backgroundColor: color,
21
- opacity: 0.3
22
- }}
23
- />
24
- )
25
-
26
7
  export default {
27
8
  title: "Atoms/Badge",
28
9
  component: Badge,
@@ -90,12 +71,7 @@ export const WithIcons = () => (
90
71
  <Text style={styles.label}>{variant} with Icon</Text>
91
72
  <View style={styles.row}>
92
73
  {sizes.map((size) => (
93
- <Badge
94
- key={size}
95
- variant={variant}
96
- size={size}
97
- icon={PlaceholderIcon}
98
- >
74
+ <Badge key={size} variant={variant} size={size} icon={CheckCircle}>
99
75
  {size}
100
76
  </Badge>
101
77
  ))}
@@ -117,7 +93,7 @@ export const Pinned = () => (
117
93
  size="medium"
118
94
  pinned
119
95
  top={16}
120
- icon={PlaceholderIcon}
96
+ icon={CheckCircle}
121
97
  >
122
98
  {variant}
123
99
  </Badge>
@@ -135,7 +111,7 @@ export const Pinned = () => (
135
111
  size="medium"
136
112
  pinned
137
113
  bottom={16}
138
- icon={PlaceholderIcon}
114
+ icon={CheckCircle}
139
115
  >
140
116
  {variant}
141
117
  </Badge>
@@ -179,7 +155,7 @@ export const UsageExamples = () => (
179
155
  <View style={styles.section}>
180
156
  <Text style={styles.label}>Promotional tags</Text>
181
157
  <View style={styles.row}>
182
- <Badge variant="promo" size="large" icon={PlaceholderIcon}>
158
+ <Badge variant="promo" size="large" icon={CheckCircle}>
183
159
  Premium
184
160
  </Badge>
185
161
  <Badge variant="primary" size="large">
@@ -0,0 +1,90 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import { describe, it, expect } from "vitest"
4
+ import { renderWithTheme } from "../../../test-utils"
5
+ import { Badge } from "./Badge"
6
+
7
+ const MockIcon = ({ width, height, color }: any) => (
8
+ <svg data-testid="mock-icon" width={width} height={height} fill={color} />
9
+ )
10
+ MockIcon.category = "core" as const
11
+
12
+ describe("Badge", () => {
13
+ describe("when component is rendering", () => {
14
+ it("renders children text", () => {
15
+ renderWithTheme(<Badge>New</Badge>)
16
+ expect(screen.getByText("New")).toBeInTheDocument()
17
+ })
18
+
19
+ it("renders with default variant and size", () => {
20
+ renderWithTheme(<Badge>Default</Badge>)
21
+ expect(screen.getByText("Default")).toBeInTheDocument()
22
+ })
23
+ })
24
+
25
+ describe("when rendering all variants", () => {
26
+ it.each(["primary", "promo", "success", "warning", "error"] as const)(
27
+ "renders %s variant",
28
+ (variant) => {
29
+ renderWithTheme(<Badge variant={variant}>{variant}</Badge>)
30
+ expect(screen.getByText(variant)).toBeInTheDocument()
31
+ }
32
+ )
33
+ })
34
+
35
+ describe("when rendering all sizes", () => {
36
+ it.each(["small", "medium", "large"] as const)(
37
+ "renders %s size",
38
+ (size) => {
39
+ renderWithTheme(<Badge size={size}>{size}</Badge>)
40
+ expect(screen.getByText(size)).toBeInTheDocument()
41
+ }
42
+ )
43
+ })
44
+
45
+ describe("when rendering with icon", () => {
46
+ it("renders with icon", () => {
47
+ renderWithTheme(<Badge icon={MockIcon}>With Icon</Badge>)
48
+ expect(screen.getByTestId("mock-icon")).toBeInTheDocument()
49
+ expect(screen.getByText("With Icon")).toBeInTheDocument()
50
+ })
51
+
52
+ it("renders without icon when not provided", () => {
53
+ renderWithTheme(<Badge>No Icon</Badge>)
54
+ expect(screen.queryByTestId("mock-icon")).not.toBeInTheDocument()
55
+ })
56
+ })
57
+
58
+ describe("when rendering pinned badges", () => {
59
+ it("renders pinned badge", () => {
60
+ renderWithTheme(<Badge pinned>Pinned</Badge>)
61
+ expect(screen.getByText("Pinned")).toBeInTheDocument()
62
+ })
63
+
64
+ it("renders pinned badge with top position", () => {
65
+ renderWithTheme(
66
+ <Badge pinned top={16}>
67
+ Pinned Top
68
+ </Badge>
69
+ )
70
+ expect(screen.getByText("Pinned Top")).toBeInTheDocument()
71
+ })
72
+
73
+ it("renders pinned badge with bottom position", () => {
74
+ renderWithTheme(
75
+ <Badge pinned bottom={16}>
76
+ Pinned Bottom
77
+ </Badge>
78
+ )
79
+ expect(screen.getByText("Pinned Bottom")).toBeInTheDocument()
80
+ })
81
+ })
82
+
83
+ describe("when using with ref", () => {
84
+ it("forwards ref", () => {
85
+ const ref = React.createRef<any>()
86
+ renderWithTheme(<Badge ref={ref}>Ref test</Badge>)
87
+ expect(ref.current).toBeTruthy()
88
+ })
89
+ })
90
+ })
@@ -0,0 +1,123 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { describe, it, expect, vi } from "vitest"
5
+ import { renderWithTheme } from "../../../test-utils"
6
+ import { Button } from "./Button"
7
+
8
+ describe("Button", () => {
9
+ describe("when component is rendering", () => {
10
+ it("renders children text", () => {
11
+ renderWithTheme(<Button>Click me</Button>)
12
+ expect(screen.getByText("Click me")).toBeInTheDocument()
13
+ })
14
+
15
+ it("renders with default props", () => {
16
+ renderWithTheme(<Button>Default</Button>)
17
+ expect(screen.getByText("Default")).toBeInTheDocument()
18
+ })
19
+ })
20
+
21
+ describe("when rendering variants", () => {
22
+ it.each(["filled", "outlined", "text"] as const)(
23
+ "renders %s variant",
24
+ (variant) => {
25
+ renderWithTheme(<Button variant={variant}>{variant}</Button>)
26
+ expect(screen.getByText(variant)).toBeInTheDocument()
27
+ }
28
+ )
29
+ })
30
+
31
+ describe("when rendering sizes", () => {
32
+ it.each(["sm", "md", "lg"] as const)("renders %s size", (size) => {
33
+ renderWithTheme(<Button size={size}>Size {size}</Button>)
34
+ expect(screen.getByText(`Size ${size}`)).toBeInTheDocument()
35
+ })
36
+ })
37
+
38
+ describe("when rendering colours", () => {
39
+ it.each(["primary", "secondary"] as const)(
40
+ "renders %s colour",
41
+ (colour) => {
42
+ renderWithTheme(<Button colour={colour}>Colour</Button>)
43
+ expect(screen.getByText("Colour")).toBeInTheDocument()
44
+ }
45
+ )
46
+ })
47
+
48
+ describe("when component is interactive", () => {
49
+ it("calls onPress when pressed", async () => {
50
+ const user = userEvent.setup()
51
+ const onPress = vi.fn()
52
+ renderWithTheme(<Button onPress={onPress}>Press me</Button>)
53
+
54
+ await user.click(screen.getByText("Press me"))
55
+ expect(onPress).toHaveBeenCalledTimes(1)
56
+ })
57
+
58
+ it("does not call onPress when disabled", async () => {
59
+ const user = userEvent.setup()
60
+ const onPress = vi.fn()
61
+ renderWithTheme(
62
+ <Button onPress={onPress} disabled>
63
+ Disabled
64
+ </Button>
65
+ )
66
+
67
+ await user.click(screen.getByText("Disabled"))
68
+ expect(onPress).not.toHaveBeenCalled()
69
+ })
70
+ })
71
+
72
+ describe("when loading", () => {
73
+ it("renders loading state", () => {
74
+ renderWithTheme(<Button loading>Loading</Button>)
75
+ expect(screen.getByText("Loading")).toBeInTheDocument()
76
+ })
77
+
78
+ it("does not call onPress when loading", async () => {
79
+ const user = userEvent.setup()
80
+ const onPress = vi.fn()
81
+ renderWithTheme(
82
+ <Button onPress={onPress} loading>
83
+ Loading
84
+ </Button>
85
+ )
86
+
87
+ await user.click(screen.getByText("Loading"))
88
+ expect(onPress).not.toHaveBeenCalled()
89
+ })
90
+ })
91
+
92
+ describe("when rendering with icons", () => {
93
+ it("renders with startIcon", () => {
94
+ const StartIcon = () => <span data-testid="start-icon">→</span>
95
+ renderWithTheme(<Button startIcon={<StartIcon />}>With icon</Button>)
96
+
97
+ expect(screen.getByTestId("start-icon")).toBeInTheDocument()
98
+ expect(screen.getByText("With icon")).toBeInTheDocument()
99
+ })
100
+
101
+ it("renders with endIcon", () => {
102
+ const EndIcon = () => <span data-testid="end-icon">→</span>
103
+ renderWithTheme(<Button endIcon={<EndIcon />}>With icon</Button>)
104
+
105
+ expect(screen.getByTestId("end-icon")).toBeInTheDocument()
106
+ expect(screen.getByText("With icon")).toBeInTheDocument()
107
+ })
108
+ })
109
+
110
+ describe("accessibility", () => {
111
+ it("sets disabled accessibility state", () => {
112
+ renderWithTheme(<Button disabled>Disabled</Button>)
113
+ const button = screen.getByText("Disabled").closest("button")
114
+ expect(button).toBeDisabled()
115
+ })
116
+
117
+ it("sets busy accessibility state when loading", () => {
118
+ const { container } = renderWithTheme(<Button loading>Busy</Button>)
119
+ const element = container.querySelector('[aria-busy="true"]')
120
+ expect(element).toBeInTheDocument()
121
+ })
122
+ })
123
+ })
@@ -230,7 +230,7 @@ const Button = React.forwardRef<View, ButtonProps>(
230
230
  fontFamily: typography.fontFamily,
231
231
  fontWeight: typography.fontWeight,
232
232
  fontSize: typography.fontSize,
233
- lineHeight: typography.fontSize,
233
+ lineHeight: typography.lineHeight,
234
234
  letterSpacing: typography.letterSpacing
235
235
  }}
236
236
  color={variantStyles.textColor as never}
@@ -0,0 +1,217 @@
1
+ import React, { useState } from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { Typography } from "../Typography"
4
+ import { Button } from "../Button"
5
+ import {
6
+ CarouselControls,
7
+ type CarouselControlsProps
8
+ } from "./CarouselControls"
9
+
10
+ export default {
11
+ title: "Atoms/CarouselControls",
12
+ component: CarouselControls,
13
+ argTypes: {
14
+ count: {
15
+ control: { type: "number", min: 2, max: 7, step: 1 },
16
+ description: "Total number of carousel items (2–7)"
17
+ },
18
+ activeIndex: {
19
+ control: { type: "number", min: 0, max: 6, step: 1 },
20
+ description: "Zero-based index of the active item"
21
+ },
22
+ showBackground: {
23
+ control: { type: "boolean" },
24
+ description: "Show pill background container"
25
+ }
26
+ }
27
+ }
28
+
29
+ export const Playground = (args: CarouselControlsProps) => (
30
+ <View style={styles.container}>
31
+ <CarouselControls {...args} />
32
+ </View>
33
+ )
34
+ Playground.args = {
35
+ count: 5,
36
+ activeIndex: 2,
37
+ showBackground: true
38
+ }
39
+
40
+ export const AllCounts = () => (
41
+ <View style={styles.column}>
42
+ {([2, 3, 4, 5, 6, 7] as const).map((count) => (
43
+ <View key={count} style={styles.row}>
44
+ <View style={styles.labelContainer}>
45
+ <Typography size="sm">{count} items</Typography>
46
+ </View>
47
+ <CarouselControls count={count} activeIndex={0} />
48
+ </View>
49
+ ))}
50
+ </View>
51
+ )
52
+
53
+ export const WithBackground = () => (
54
+ <View style={styles.column}>
55
+ <Typography variant="heading" size="sm">
56
+ On light background
57
+ </Typography>
58
+ <CarouselControls
59
+ count={5}
60
+ activeIndex={2}
61
+ showBackground
62
+ style={{ alignSelf: "flex-start" }}
63
+ />
64
+
65
+ <Typography variant="heading" size="sm">
66
+ On dark background
67
+ </Typography>
68
+ <View style={styles.darkSurface}>
69
+ <CarouselControls count={5} activeIndex={2} showBackground />
70
+ </View>
71
+ </View>
72
+ )
73
+
74
+ export const WithoutBackground = () => (
75
+ <View style={styles.column}>
76
+ <Typography variant="heading" size="sm">
77
+ Standalone dots (no background)
78
+ </Typography>
79
+ <CarouselControls count={5} activeIndex={2} showBackground={false} />
80
+ </View>
81
+ )
82
+
83
+ export const DiminishingWindow = () => (
84
+ <View style={styles.column}>
85
+ <Typography variant="heading" size="sm">
86
+ 7 items — active position changes
87
+ </Typography>
88
+ {Array.from({ length: 7 }, (_, i) => (
89
+ <View key={i} style={styles.row}>
90
+ <View style={styles.labelContainer}>
91
+ <Typography size="sm">Active: {i + 1}</Typography>
92
+ </View>
93
+ <CarouselControls count={7} activeIndex={i} />
94
+ </View>
95
+ ))}
96
+ </View>
97
+ )
98
+
99
+ export const AllStates = () => (
100
+ <View style={styles.column}>
101
+ <Typography variant="heading" size="sm">
102
+ 2–5 items (uniform dots)
103
+ </Typography>
104
+ {([2, 3, 4, 5] as const).map((count) =>
105
+ Array.from({ length: count }, (_, i) => (
106
+ <View key={`${count}-${i}`} style={styles.row}>
107
+ <View style={styles.labelWide}>
108
+ <Typography size="sm">
109
+ {count} items, #{i + 1}
110
+ </Typography>
111
+ </View>
112
+ <CarouselControls count={count} activeIndex={i} />
113
+ </View>
114
+ ))
115
+ )}
116
+
117
+ <Typography variant="heading" size="sm">
118
+ 6–7 items (diminishing window)
119
+ </Typography>
120
+ {([6, 7] as const).map((count) =>
121
+ Array.from({ length: count }, (_, i) => (
122
+ <View key={`${count}-${i}`} style={styles.row}>
123
+ <View style={styles.labelWide}>
124
+ <Typography size="sm">
125
+ {count} items, #{i + 1}
126
+ </Typography>
127
+ </View>
128
+ <CarouselControls count={count} activeIndex={i} />
129
+ </View>
130
+ ))
131
+ )}
132
+ </View>
133
+ )
134
+
135
+ function InteractiveCarousel() {
136
+ const [activeIndex, setActiveIndex] = useState(0)
137
+ const count = 5
138
+
139
+ return (
140
+ <View style={styles.interactiveContainer}>
141
+ <View style={styles.slide}>
142
+ <Typography variant="heading" size="md">
143
+ Slide {activeIndex + 1}
144
+ </Typography>
145
+ </View>
146
+ <View style={styles.controls}>
147
+ <Button
148
+ variant="outlined"
149
+ colour="primary"
150
+ size="sm"
151
+ onPress={() => setActiveIndex((i) => Math.max(0, i - 1))}
152
+ disabled={activeIndex === 0}
153
+ >
154
+ Prev
155
+ </Button>
156
+ <CarouselControls count={count} activeIndex={activeIndex} />
157
+ <Button
158
+ variant="outlined"
159
+ colour="primary"
160
+ size="sm"
161
+ onPress={() => setActiveIndex((i) => Math.min(count - 1, i + 1))}
162
+ disabled={activeIndex === count - 1}
163
+ >
164
+ Next
165
+ </Button>
166
+ </View>
167
+ </View>
168
+ )
169
+ }
170
+
171
+ export const Interactive = () => <InteractiveCarousel />
172
+
173
+ const styles = StyleSheet.create({
174
+ container: {
175
+ padding: 24,
176
+ alignItems: "center"
177
+ },
178
+ column: {
179
+ padding: 24,
180
+ gap: 16
181
+ },
182
+ row: {
183
+ flexDirection: "row",
184
+ alignItems: "center",
185
+ gap: 16
186
+ },
187
+ labelContainer: {
188
+ width: 64
189
+ },
190
+ labelWide: {
191
+ width: 88
192
+ },
193
+ darkSurface: {
194
+ backgroundColor: "#3d2317",
195
+ padding: 24,
196
+ borderRadius: 12,
197
+ alignSelf: "flex-start"
198
+ },
199
+ interactiveContainer: {
200
+ padding: 24,
201
+ gap: 16,
202
+ alignItems: "center"
203
+ },
204
+ slide: {
205
+ width: 280,
206
+ height: 160,
207
+ backgroundColor: "#f5f0eb",
208
+ borderRadius: 12,
209
+ alignItems: "center",
210
+ justifyContent: "center"
211
+ },
212
+ controls: {
213
+ flexDirection: "row",
214
+ alignItems: "center",
215
+ gap: 12
216
+ }
217
+ })