@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,116 @@
1
+ import React from "react"
2
+
3
+ export const Root = React.forwardRef<any, any>(
4
+ (
5
+ { value, defaultValue, onValueChange, disabled, children, ...rest },
6
+ ref
7
+ ) => {
8
+ const [internalValue, setInternalValue] = React.useState(
9
+ value ?? defaultValue ?? null
10
+ )
11
+ const currentValue = value ?? internalValue
12
+
13
+ const handleChange = (newValue: any) => {
14
+ if (!disabled) {
15
+ setInternalValue(newValue)
16
+ onValueChange?.(newValue)
17
+ }
18
+ }
19
+
20
+ return (
21
+ <div ref={ref} data-disabled={disabled} {...rest}>
22
+ {typeof children === "function"
23
+ ? children({ value: currentValue, onChange: handleChange })
24
+ : children}
25
+ </div>
26
+ )
27
+ }
28
+ )
29
+ Root.displayName = "SelectPrimitive.Root"
30
+
31
+ export const Trigger = React.forwardRef<any, any>(
32
+ ({ asChild, children, ...rest }, ref) => {
33
+ if (asChild && React.isValidElement(children)) {
34
+ return React.cloneElement(children, { ref, ...rest } as any)
35
+ }
36
+ return (
37
+ <button ref={ref} role="button" {...rest}>
38
+ {children}
39
+ </button>
40
+ )
41
+ }
42
+ )
43
+ Trigger.displayName = "SelectPrimitive.Trigger"
44
+
45
+ export const Value = React.forwardRef<any, any>(
46
+ ({ placeholder, asChild, children, ...rest }, ref) => {
47
+ if (asChild && React.isValidElement(children)) {
48
+ return React.cloneElement(children, { ref, ...rest } as any)
49
+ }
50
+ return (
51
+ <span ref={ref} {...rest}>
52
+ {children || placeholder}
53
+ </span>
54
+ )
55
+ }
56
+ )
57
+ Value.displayName = "SelectPrimitive.Value"
58
+
59
+ export const Portal = ({ children }: any) => <>{children}</>
60
+ Portal.displayName = "SelectPrimitive.Portal"
61
+
62
+ export const Viewport = React.forwardRef<any, any>(
63
+ ({ children, ...rest }, ref) => (
64
+ <div ref={ref} {...rest}>
65
+ {children}
66
+ </div>
67
+ )
68
+ )
69
+ Viewport.displayName = "SelectPrimitive.Viewport"
70
+
71
+ export const Overlay = React.forwardRef<any, any>(
72
+ ({ asChild, children, ...rest }, ref) => {
73
+ if (asChild && React.isValidElement(children)) {
74
+ return React.cloneElement(children, { ref, ...rest } as any)
75
+ }
76
+ return (
77
+ <div ref={ref} {...rest}>
78
+ {children}
79
+ </div>
80
+ )
81
+ }
82
+ )
83
+ Overlay.displayName = "SelectPrimitive.Overlay"
84
+
85
+ export const Content = React.forwardRef<any, any>(
86
+ ({ asChild, children, ...rest }, ref) => {
87
+ if (asChild && React.isValidElement(children)) {
88
+ return React.cloneElement(children, { ref, ...rest } as any)
89
+ }
90
+ return (
91
+ <div ref={ref} role="listbox" {...rest}>
92
+ {children}
93
+ </div>
94
+ )
95
+ }
96
+ )
97
+ Content.displayName = "SelectPrimitive.Content"
98
+
99
+ export const Item = React.forwardRef<any, any>(
100
+ ({ value: _value, label, disabled, asChild, children, ...rest }, ref) => {
101
+ if (asChild && React.isValidElement(children)) {
102
+ return React.cloneElement(children, {
103
+ ref,
104
+ role: "option",
105
+ "aria-disabled": disabled,
106
+ ...rest
107
+ } as any)
108
+ }
109
+ return (
110
+ <div ref={ref} role="option" aria-disabled={disabled} {...rest}>
111
+ {children || label}
112
+ </div>
113
+ )
114
+ }
115
+ )
116
+ Item.displayName = "SelectPrimitive.Item"
@@ -0,0 +1,40 @@
1
+ import React from "react"
2
+
3
+ export const Root = React.forwardRef<any, any>(
4
+ ({ children, value, min, max, disabled, ..._rest }, ref) => (
5
+ <div
6
+ ref={ref}
7
+ role="slider"
8
+ aria-valuenow={value}
9
+ aria-valuemin={min}
10
+ aria-valuemax={max}
11
+ aria-disabled={disabled}
12
+ >
13
+ {children}
14
+ </div>
15
+ )
16
+ )
17
+ Root.displayName = "SliderPrimitive.Root"
18
+
19
+ export const Track = React.forwardRef<any, any>(
20
+ ({ children, ..._rest }, ref) => (
21
+ <div ref={ref} data-testid="slider-track">
22
+ {children}
23
+ </div>
24
+ )
25
+ )
26
+ Track.displayName = "SliderPrimitive.Track"
27
+
28
+ export const Range = React.forwardRef<any, any>(({ style, ..._rest }, ref) => (
29
+ <div ref={ref} data-testid="slider-range" style={style} />
30
+ ))
31
+ Range.displayName = "SliderPrimitive.Range"
32
+
33
+ export const Thumb = React.forwardRef<any, any>(
34
+ ({ children, style, ..._rest }, ref) => (
35
+ <div ref={ref} data-testid="slider-thumb" style={style}>
36
+ {children}
37
+ </div>
38
+ )
39
+ )
40
+ Thumb.displayName = "SliderPrimitive.Thumb"
@@ -0,0 +1,30 @@
1
+ import React from "react"
2
+
3
+ export const Slot = React.forwardRef<any, any>(
4
+ ({ children, ...slotProps }, ref) => {
5
+ if (React.isValidElement(children)) {
6
+ const childProps = children.props as Record<string, any>
7
+ const mergedProps = { ...slotProps, ...childProps }
8
+ if (slotProps.onPress && childProps.onPress) {
9
+ mergedProps.onPress = (...args: any[]) => {
10
+ slotProps.onPress(...args)
11
+ childProps.onPress(...args)
12
+ }
13
+ }
14
+ // Map onPress to onClick for DOM elements so fireEvent.click works in tests
15
+ if (slotProps.onPress && !mergedProps.onClick) {
16
+ mergedProps.onClick = (...args: any[]) => mergedProps.onPress?.(...args)
17
+ }
18
+ return React.cloneElement(children as React.ReactElement<any>, {
19
+ ...mergedProps,
20
+ ref
21
+ })
22
+ }
23
+ return (
24
+ <div ref={ref} {...slotProps}>
25
+ {children}
26
+ </div>
27
+ )
28
+ }
29
+ )
30
+ Slot.displayName = "Slot"
@@ -0,0 +1,24 @@
1
+ import React from "react"
2
+
3
+ export const Root = React.forwardRef<any, any>(
4
+ ({ checked, onCheckedChange, disabled, children, ..._rest }, ref) => (
5
+ <button
6
+ ref={ref}
7
+ role="switch"
8
+ aria-checked={checked}
9
+ disabled={disabled}
10
+ onClick={() => !disabled && onCheckedChange?.(!checked)}
11
+ >
12
+ {children}
13
+ </button>
14
+ )
15
+ )
16
+ Root.displayName = "SwitchPrimitive.Root"
17
+
18
+ export const Thumb = ({ children, asChild, ..._rest }: any) => {
19
+ if (asChild && React.isValidElement(children)) {
20
+ return children
21
+ }
22
+ return <div>{children}</div>
23
+ }
24
+ Thumb.displayName = "SwitchPrimitive.Thumb"
@@ -0,0 +1,16 @@
1
+ import React from "react"
2
+
3
+ export const Root = React.forwardRef<any, any>(
4
+ ({ children, pressed, onPressedChange, disabled, ..._rest }, ref) => (
5
+ <button
6
+ ref={ref}
7
+ role="switch"
8
+ aria-pressed={pressed}
9
+ disabled={disabled}
10
+ onClick={() => !disabled && onPressedChange?.(!pressed)}
11
+ >
12
+ {children}
13
+ </button>
14
+ )
15
+ )
16
+ Root.displayName = "TogglePrimitive.Root"
@@ -1,7 +1,6 @@
1
1
  import React from "react"
2
2
  import { View, StyleSheet } from "react-native"
3
3
  import { Avatar } from "./Avatar"
4
- import type { AvatarProps } from "./Avatar"
5
4
 
6
5
  const DOG_PHOTO_URL = "https://placedog.net/200/200"
7
6
 
@@ -20,12 +19,13 @@ export default {
20
19
  size: {
21
20
  control: { type: "select" },
22
21
  options: ["sm", "md", "lg"],
23
- description: "Size variant"
22
+ description: "Avatar size: sm (32px), md (40px), lg (48px)"
24
23
  },
25
24
  border: {
26
25
  control: { type: "select" },
27
26
  options: ["none", "sm", "md"],
28
- description: "Border width variant"
27
+ description:
28
+ "Border width. Small avatars: none/sm (1px). Medium/Large: none/md (2px)"
29
29
  },
30
30
  fallbackType: {
31
31
  control: { type: "select" },
@@ -39,13 +39,9 @@ export default {
39
39
  }
40
40
  }
41
41
 
42
- export const Default = (args: AvatarProps) => <Avatar {...args} />
43
- Default.args = {
44
- src: DOG_PHOTO_URL,
45
- alt: "User Photo",
46
- size: "md",
47
- border: "none"
48
- }
42
+ export const Default = () => (
43
+ <Avatar src={DOG_PHOTO_URL} alt="User Photo" size="md" />
44
+ )
49
45
 
50
46
  export const Sizes = () => (
51
47
  <View style={styles.row}>
@@ -55,71 +51,83 @@ export const Sizes = () => (
55
51
  </View>
56
52
  )
57
53
 
58
- export const Borders = () => (
59
- <View style={styles.row}>
60
- <Avatar src={DOG_PHOTO_URL} alt="No border" border="none" />
61
- <Avatar src={DOG_PHOTO_URL} alt="Small border" border="sm" />
62
- <Avatar src={DOG_PHOTO_URL} alt="Medium border" border="md" />
54
+ export const Image = () => (
55
+ <View style={styles.column}>
56
+ <View style={styles.row}>
57
+ <Avatar src={DOG_PHOTO_URL} alt="Small" size="sm" border="none" />
58
+ <Avatar src={DOG_PHOTO_URL} alt="Small" size="sm" border="sm" />
59
+ </View>
60
+ <View style={styles.row}>
61
+ <Avatar src={DOG_PHOTO_URL} alt="Medium" size="md" border="none" />
62
+ <Avatar src={DOG_PHOTO_URL} alt="Medium" size="md" border="md" />
63
+ </View>
64
+ <View style={styles.row}>
65
+ <Avatar src={DOG_PHOTO_URL} alt="Large" size="lg" border="none" />
66
+ <Avatar src={DOG_PHOTO_URL} alt="Large" size="lg" border="md" />
67
+ </View>
63
68
  </View>
64
69
  )
65
70
 
66
- export const FallbackString = (args: AvatarProps) => <Avatar {...args} />
67
- FallbackString.args = {
68
- alt: "User initials",
69
- fallbackString: "JD",
70
- size: "md"
71
- }
72
-
73
- export const FallbackStringSizes = () => (
74
- <View style={styles.row}>
75
- <Avatar alt="Small" fallbackString="AB" size="sm" />
76
- <Avatar alt="Medium" fallbackString="CD" size="md" />
77
- <Avatar alt="Large" fallbackString="EF" size="lg" />
71
+ export const FallbackIcon = () => (
72
+ <View style={styles.column}>
73
+ <View style={styles.row}>
74
+ <Avatar alt="Small" fallbackType="image" size="sm" border="none" />
75
+ <Avatar alt="Small" fallbackType="image" size="sm" border="sm" />
76
+ </View>
77
+ <View style={styles.row}>
78
+ <Avatar alt="Medium" fallbackType="image" size="md" border="none" />
79
+ <Avatar alt="Medium" fallbackType="image" size="md" border="md" />
80
+ </View>
81
+ <View style={styles.row}>
82
+ <Avatar alt="Large" fallbackType="image" size="lg" border="none" />
83
+ <Avatar alt="Large" fallbackType="image" size="lg" border="md" />
84
+ </View>
78
85
  </View>
79
86
  )
80
87
 
81
- export const FallbackImage = (args: AvatarProps) => <Avatar {...args} />
82
- FallbackImage.args = {
83
- alt: "Default avatar icon",
84
- fallbackType: "image",
85
- size: "md"
86
- }
88
+ export const FallbackString = () => (
89
+ <View style={styles.column}>
90
+ <View style={styles.row}>
91
+ <Avatar alt="Small" fallbackString="JD" size="sm" border="none" />
92
+ <Avatar alt="Small" fallbackString="JD" size="sm" border="sm" />
93
+ </View>
94
+ <View style={styles.row}>
95
+ <Avatar alt="Medium" fallbackString="JD" size="md" border="none" />
96
+ <Avatar alt="Medium" fallbackString="JD" size="md" border="md" />
97
+ </View>
98
+ <View style={styles.row}>
99
+ <Avatar alt="Large" fallbackString="JD" size="lg" border="none" />
100
+ <Avatar alt="Large" fallbackString="JD" size="lg" border="md" />
101
+ </View>
102
+ </View>
103
+ )
87
104
 
88
- export const BrokenImage = () => (
105
+ export const BrokenImageFallback = () => (
89
106
  <View style={styles.row}>
90
107
  <Avatar
91
108
  src="https://broken-link.com/photo.jpg"
92
109
  alt="Broken with string fallback"
93
110
  fallbackString="JD"
94
111
  fallbackType="string"
112
+ size="md"
95
113
  />
96
114
  <Avatar
97
115
  src="https://broken-link.com/photo.jpg"
98
116
  alt="Broken with image fallback"
99
117
  fallbackType="image"
118
+ size="md"
100
119
  />
101
120
  </View>
102
121
  )
103
122
 
104
- export const BordersWithFallback = () => (
105
- <View style={styles.row}>
106
- <Avatar alt="No border" fallbackString="AB" border="none" />
107
- <Avatar alt="Small border" fallbackString="CD" border="sm" />
108
- <Avatar alt="Medium border" fallbackString="EF" border="md" />
109
- </View>
110
- )
111
-
112
- export const LongInitials = (args: AvatarProps) => <Avatar {...args} />
113
- LongInitials.args = {
114
- alt: "Long initials",
115
- fallbackString: "ABC",
116
- size: "lg"
117
- }
118
-
119
123
  const styles = StyleSheet.create({
120
124
  row: {
121
125
  flexDirection: "row",
122
126
  gap: 16,
123
127
  alignItems: "center"
128
+ },
129
+ column: {
130
+ flexDirection: "column",
131
+ gap: 12
124
132
  }
125
133
  })
@@ -0,0 +1,269 @@
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 { Avatar } from "./Avatar"
6
+
7
+ describe("Avatar", () => {
8
+ describe("when rendering with image", () => {
9
+ it("renders image when src is provided", () => {
10
+ const { container } = renderWithTheme(
11
+ <Avatar src="https://example.com/photo.jpg" alt="User Photo" />
12
+ )
13
+
14
+ const img = container.querySelector("img")
15
+ expect(img).toBeInTheDocument()
16
+ expect(img).toHaveAttribute("src", "https://example.com/photo.jpg")
17
+ })
18
+
19
+ it("renders with alt text", () => {
20
+ renderWithTheme(
21
+ <Avatar
22
+ src="https://example.com/photo.jpg"
23
+ alt="John Doe Profile Picture"
24
+ />
25
+ )
26
+
27
+ expect(
28
+ screen.getByRole("img", { name: "John Doe Profile Picture" })
29
+ ).toBeInTheDocument()
30
+ })
31
+ })
32
+
33
+ describe("when rendering fallback", () => {
34
+ it("renders fallback icon when no src is provided", () => {
35
+ renderWithTheme(<Avatar alt="User avatar" fallbackType="image" />)
36
+
37
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
38
+ })
39
+
40
+ it("renders fallback string when fallbackString is provided", () => {
41
+ renderWithTheme(<Avatar alt="User" fallbackString="JD" />)
42
+
43
+ expect(screen.getByText("JD")).toBeInTheDocument()
44
+ })
45
+
46
+ it("automatically uses string fallback when fallbackString is provided without fallbackType", () => {
47
+ renderWithTheme(<Avatar alt="User" fallbackString="AB" />)
48
+
49
+ expect(screen.getByText("AB")).toBeInTheDocument()
50
+ })
51
+
52
+ it("renders icon fallback when no fallbackString is provided", () => {
53
+ renderWithTheme(<Avatar alt="User avatar" />)
54
+
55
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
56
+ })
57
+
58
+ it("renders string fallback even when src is provided but fails to load", () => {
59
+ renderWithTheme(
60
+ <Avatar
61
+ src="https://broken-link.com/photo.jpg"
62
+ alt="User"
63
+ fallbackString="JD"
64
+ fallbackType="string"
65
+ />
66
+ )
67
+
68
+ expect(screen.getByText("JD")).toBeInTheDocument()
69
+ })
70
+ })
71
+
72
+ describe("size variants", () => {
73
+ it("renders small size", () => {
74
+ const { container } = renderWithTheme(
75
+ <Avatar src="https://example.com/photo.jpg" alt="User" size="sm" />
76
+ )
77
+
78
+ expect(container.firstChild).toBeInTheDocument()
79
+ })
80
+
81
+ it("renders medium size (default)", () => {
82
+ const { container } = renderWithTheme(
83
+ <Avatar src="https://example.com/photo.jpg" alt="User" />
84
+ )
85
+
86
+ expect(container.firstChild).toBeInTheDocument()
87
+ })
88
+
89
+ it("renders large size", () => {
90
+ const { container } = renderWithTheme(
91
+ <Avatar src="https://example.com/photo.jpg" alt="User" size="lg" />
92
+ )
93
+
94
+ expect(container.firstChild).toBeInTheDocument()
95
+ })
96
+ })
97
+
98
+ describe("border variants", () => {
99
+ it("renders without border by default", () => {
100
+ const { container } = renderWithTheme(
101
+ <Avatar src="https://example.com/photo.jpg" alt="User" />
102
+ )
103
+
104
+ expect(container.firstChild).toBeInTheDocument()
105
+ })
106
+
107
+ it("renders small avatar with small border", () => {
108
+ const { container } = renderWithTheme(
109
+ <Avatar
110
+ src="https://example.com/photo.jpg"
111
+ alt="User"
112
+ size="sm"
113
+ border="sm"
114
+ />
115
+ )
116
+
117
+ expect(container.firstChild).toBeInTheDocument()
118
+ })
119
+
120
+ it("renders medium avatar with medium border", () => {
121
+ const { container } = renderWithTheme(
122
+ <Avatar
123
+ src="https://example.com/photo.jpg"
124
+ alt="User"
125
+ size="md"
126
+ border="md"
127
+ />
128
+ )
129
+
130
+ expect(container.firstChild).toBeInTheDocument()
131
+ })
132
+
133
+ it("renders large avatar with medium border", () => {
134
+ const { container } = renderWithTheme(
135
+ <Avatar
136
+ src="https://example.com/photo.jpg"
137
+ alt="User"
138
+ size="lg"
139
+ border="md"
140
+ />
141
+ )
142
+
143
+ expect(container.firstChild).toBeInTheDocument()
144
+ })
145
+ })
146
+
147
+ describe("fallback type combinations", () => {
148
+ it("renders image fallback for small size", () => {
149
+ renderWithTheme(<Avatar alt="User" fallbackType="image" size="sm" />)
150
+
151
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
152
+ })
153
+
154
+ it("renders image fallback for medium size", () => {
155
+ renderWithTheme(<Avatar alt="User" fallbackType="image" size="md" />)
156
+
157
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
158
+ })
159
+
160
+ it("renders image fallback for large size", () => {
161
+ renderWithTheme(<Avatar alt="User" fallbackType="image" size="lg" />)
162
+
163
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
164
+ })
165
+
166
+ it("renders string fallback for small size", () => {
167
+ renderWithTheme(<Avatar alt="User" fallbackString="SM" size="sm" />)
168
+
169
+ expect(screen.getByText("SM")).toBeInTheDocument()
170
+ })
171
+
172
+ it("renders string fallback for medium size", () => {
173
+ renderWithTheme(<Avatar alt="User" fallbackString="MD" size="md" />)
174
+
175
+ expect(screen.getByText("MD")).toBeInTheDocument()
176
+ })
177
+
178
+ it("renders string fallback for large size", () => {
179
+ renderWithTheme(<Avatar alt="User" fallbackString="LG" size="lg" />)
180
+
181
+ expect(screen.getByText("LG")).toBeInTheDocument()
182
+ })
183
+ })
184
+
185
+ describe("accessibility", () => {
186
+ it("has accessible image with alt text", () => {
187
+ renderWithTheme(
188
+ <Avatar src="https://example.com/photo.jpg" alt="User Avatar" />
189
+ )
190
+
191
+ expect(
192
+ screen.getByRole("img", { name: "User Avatar" })
193
+ ).toBeInTheDocument()
194
+ })
195
+
196
+ it("has accessible fallback icon with aria-label", () => {
197
+ renderWithTheme(<Avatar alt="User" fallbackType="image" />)
198
+
199
+ expect(screen.getByLabelText("Profile image")).toBeInTheDocument()
200
+ })
201
+
202
+ it("renders fallback text accessibly", () => {
203
+ renderWithTheme(<Avatar alt="User" fallbackString="JD" />)
204
+
205
+ expect(screen.getByText("JD")).toBeInTheDocument()
206
+ })
207
+ })
208
+
209
+ describe("type safety", () => {
210
+ it("allows small avatar with none border", () => {
211
+ const { container } = renderWithTheme(
212
+ <Avatar alt="User" size="sm" border="none" fallbackString="SM" />
213
+ )
214
+
215
+ expect(container.firstChild).toBeInTheDocument()
216
+ })
217
+
218
+ it("allows small avatar with sm border", () => {
219
+ const { container } = renderWithTheme(
220
+ <Avatar alt="User" size="sm" border="sm" fallbackString="SM" />
221
+ )
222
+
223
+ expect(container.firstChild).toBeInTheDocument()
224
+ })
225
+
226
+ it("allows medium avatar with none border", () => {
227
+ const { container } = renderWithTheme(
228
+ <Avatar alt="User" size="md" border="none" fallbackString="MD" />
229
+ )
230
+
231
+ expect(container.firstChild).toBeInTheDocument()
232
+ })
233
+
234
+ it("allows medium avatar with md border", () => {
235
+ const { container } = renderWithTheme(
236
+ <Avatar alt="User" size="md" border="md" fallbackString="MD" />
237
+ )
238
+
239
+ expect(container.firstChild).toBeInTheDocument()
240
+ })
241
+
242
+ it("allows large avatar with none border", () => {
243
+ const { container } = renderWithTheme(
244
+ <Avatar alt="User" size="lg" border="none" fallbackString="LG" />
245
+ )
246
+
247
+ expect(container.firstChild).toBeInTheDocument()
248
+ })
249
+
250
+ it("allows large avatar with md border", () => {
251
+ const { container } = renderWithTheme(
252
+ <Avatar alt="User" size="lg" border="md" fallbackString="LG" />
253
+ )
254
+
255
+ expect(container.firstChild).toBeInTheDocument()
256
+ })
257
+ })
258
+
259
+ describe("ref forwarding", () => {
260
+ it("forwards ref to the root element", () => {
261
+ const ref = React.createRef<any>()
262
+
263
+ renderWithTheme(<Avatar ref={ref} alt="User" />)
264
+
265
+ expect(ref.current).toBeTruthy()
266
+ expect(ref.current).toBeInTheDocument()
267
+ })
268
+ })
269
+ })