@butternutbox/pawprint-native 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +30 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12413 -1459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1111 -13
- package/dist/index.d.ts +1111 -13
- package/dist/index.js +12365 -1457
- package/dist/index.js.map +1 -1
- package/package.json +29 -11
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Illustration/Illustration.tsx +3 -3
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Link/Link.tsx +7 -6
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +28 -17
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- 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
|
-
|
|
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:
|
|
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.
|
|
71
|
-
color: text.
|
|
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: "
|
|
77
|
-
md: "
|
|
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"] -
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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,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={
|
|
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={
|
|
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={
|
|
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.
|
|
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
|
+
})
|