@butternutbox/pawprint-native 0.0.1
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 +30 -0
- package/COMPONENT_GUIDELINES.md +610 -0
- package/README.md +72 -0
- package/dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 +0 -0
- package/dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 +0 -0
- package/dist/ida-narrow-500-normal-C6I2PK4T.woff2 +0 -0
- package/dist/ida-narrow-700-normal-UPHPRIN6.woff2 +0 -0
- package/dist/index.cjs +2686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +780 -0
- package/dist/index.d.ts +780 -0
- package/dist/index.js +2617 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +3 -0
- package/llms.txt +458 -0
- package/package.json +57 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +125 -0
- package/src/components/atoms/Avatar/Avatar.tsx +159 -0
- package/src/components/atoms/Avatar/index.ts +7 -0
- package/src/components/atoms/Badge/Badge.stories.tsx +231 -0
- package/src/components/atoms/Badge/Badge.tsx +184 -0
- package/src/components/atoms/Badge/index.ts +2 -0
- package/src/components/atoms/Button/Button.stories.tsx +145 -0
- package/src/components/atoms/Button/Button.tsx +261 -0
- package/src/components/atoms/Button/index.ts +7 -0
- package/src/components/atoms/Hint/Hint.stories.tsx +84 -0
- package/src/components/atoms/Hint/Hint.tsx +59 -0
- package/src/components/atoms/Hint/index.ts +2 -0
- package/src/components/atoms/Icon/Icon.stories.tsx +200 -0
- package/src/components/atoms/Icon/Icon.tsx +112 -0
- package/src/components/atoms/Icon/index.ts +8 -0
- package/src/components/atoms/IconButton/IconButton.stories.tsx +162 -0
- package/src/components/atoms/IconButton/IconButton.tsx +227 -0
- package/src/components/atoms/IconButton/index.ts +7 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +167 -0
- package/src/components/atoms/Illustration/Illustration.tsx +81 -0
- package/src/components/atoms/Illustration/index.ts +6 -0
- package/src/components/atoms/Input/Input.stories.tsx +142 -0
- package/src/components/atoms/Input/Input.tsx +110 -0
- package/src/components/atoms/Input/InputDescription.tsx +49 -0
- package/src/components/atoms/Input/InputError.tsx +39 -0
- package/src/components/atoms/Input/InputField.tsx +119 -0
- package/src/components/atoms/Input/InputLabel.tsx +61 -0
- package/src/components/atoms/Input/index.ts +10 -0
- package/src/components/atoms/Link/Link.stories.tsx +119 -0
- package/src/components/atoms/Link/Link.tsx +118 -0
- package/src/components/atoms/Link/index.ts +2 -0
- package/src/components/atoms/Logo/Logo.registry.ts +39 -0
- package/src/components/atoms/Logo/Logo.tsx +68 -0
- package/src/components/atoms/Logo/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.stories.tsx +98 -0
- package/src/components/atoms/Spinner/Spinner.tsx +91 -0
- package/src/components/atoms/Spinner/index.ts +2 -0
- package/src/components/atoms/Switch/Switch.stories.tsx +120 -0
- package/src/components/atoms/Switch/Switch.tsx +196 -0
- package/src/components/atoms/Switch/index.ts +2 -0
- package/src/components/atoms/Tag/Tag.stories.tsx +89 -0
- package/src/components/atoms/Tag/Tag.tsx +122 -0
- package/src/components/atoms/Tag/index.ts +2 -0
- package/src/components/atoms/Typography/Typography.stories.tsx +315 -0
- package/src/components/atoms/Typography/Typography.tsx +284 -0
- package/src/components/atoms/Typography/index.ts +2 -0
- package/src/components/atoms/index.ts +14 -0
- package/src/components/index.ts +2 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +95 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +148 -0
- package/src/components/molecules/ButtonDock/index.ts +2 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +82 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +94 -0
- package/src/components/molecules/ButtonGroup/index.ts +2 -0
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +148 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +279 -0
- package/src/components/molecules/Checkbox/CheckboxGroup.tsx +53 -0
- package/src/components/molecules/Checkbox/index.ts +4 -0
- package/src/components/molecules/Radio/Radio.stories.tsx +182 -0
- package/src/components/molecules/Radio/Radio.tsx +249 -0
- package/src/components/molecules/Radio/RadioGroup.tsx +142 -0
- package/src/components/molecules/Radio/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +151 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +323 -0
- package/src/components/molecules/SegmentedControl/index.ts +5 -0
- package/src/components/molecules/Slider/Slider.stories.tsx +144 -0
- package/src/components/molecules/Slider/Slider.tsx +303 -0
- package/src/components/molecules/Slider/index.ts +2 -0
- package/src/components/molecules/index.ts +6 -0
- package/src/fonts/ibm-plex-sans-condensed-400-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-500-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-600-normal.woff2 +0 -0
- package/src/fonts/ibm-plex-sans-condensed-700-normal.woff2 +0 -0
- package/src/fonts/ida-narrow-500-normal.woff2 +0 -0
- package/src/fonts/ida-narrow-700-normal.woff2 +0 -0
- package/src/fonts/index.ts +49 -0
- package/src/index.ts +9 -0
- package/src/theme/PawprintProvider.tsx +26 -0
- package/src/theme/ThemeProvider.tsx +63 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/theme.ts +3 -0
- package/src/theme/utils.ts +31 -0
- package/src/types/fonts.d.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/theme.ts +24 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import * as AvatarPrimitive from "@rn-primitives/avatar"
|
|
4
|
+
import styled from "@emotion/native"
|
|
5
|
+
import { Typography } from "../Typography"
|
|
6
|
+
|
|
7
|
+
export type AvatarSize = "sm" | "md" | "lg"
|
|
8
|
+
export type AvatarBorder = "none" | "sm" | "md"
|
|
9
|
+
export type AvatarFallbackType = "string" | "image"
|
|
10
|
+
|
|
11
|
+
export interface AvatarProps {
|
|
12
|
+
src?: string
|
|
13
|
+
alt: string
|
|
14
|
+
size?: AvatarSize
|
|
15
|
+
border?: AvatarBorder
|
|
16
|
+
fallbackType?: AvatarFallbackType
|
|
17
|
+
fallbackString?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SIZE_MAP = {
|
|
21
|
+
sm: "small",
|
|
22
|
+
md: "medium",
|
|
23
|
+
lg: "large"
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
27
|
+
|
|
28
|
+
const StyledRoot = styled(AvatarPrimitive.Root)<{
|
|
29
|
+
size: AvatarSize
|
|
30
|
+
border: AvatarBorder
|
|
31
|
+
}>(({ theme, size, border }) => {
|
|
32
|
+
const { avatar } = theme.tokens.components
|
|
33
|
+
const { borderRadius } = theme.tokens.semantics.dimensions
|
|
34
|
+
|
|
35
|
+
const tokenSize = SIZE_MAP[size]
|
|
36
|
+
const avatarSize = parseTokenValue(avatar.size[tokenSize])
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
overflow: "hidden",
|
|
43
|
+
borderRadius: parseTokenValue(borderRadius.round),
|
|
44
|
+
width: avatarSize,
|
|
45
|
+
height: avatarSize,
|
|
46
|
+
...(border !== "none" && {
|
|
47
|
+
borderWidth: parseTokenValue(
|
|
48
|
+
avatar.borderWidth[border === "sm" ? "small" : "medium"]
|
|
49
|
+
),
|
|
50
|
+
borderColor: avatar.colour.border.default,
|
|
51
|
+
borderStyle: "solid" as const
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const StyledImage = styled(AvatarPrimitive.Image)({
|
|
57
|
+
width: "100%",
|
|
58
|
+
height: "100%",
|
|
59
|
+
resizeMode: "cover"
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const StyledFallback = styled(AvatarPrimitive.Fallback)(({ theme }) => {
|
|
63
|
+
const { background, text } = theme.tokens.semantics.colour
|
|
64
|
+
return {
|
|
65
|
+
display: "flex",
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
justifyContent: "center",
|
|
68
|
+
width: "100%",
|
|
69
|
+
height: "100%",
|
|
70
|
+
backgroundColor: background.container.disabled,
|
|
71
|
+
color: text.disabled
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const AVATAR_TO_BODY_SIZE = {
|
|
76
|
+
sm: "xs",
|
|
77
|
+
md: "sm",
|
|
78
|
+
lg: "md"
|
|
79
|
+
} as const
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Avatar component for displaying user profile pictures or fallback initials.
|
|
83
|
+
* Built on @rn-primitives/avatar with design system styling.
|
|
84
|
+
*
|
|
85
|
+
* Features:
|
|
86
|
+
* - Three size variants (sm, md, lg)
|
|
87
|
+
* - Optional borders (none, sm, md)
|
|
88
|
+
* - Automatic fallback handling when image fails to load
|
|
89
|
+
* - String fallback (user initials) or image fallback (default icon)
|
|
90
|
+
* - Accessible with required alt text
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { Avatar } from "@butternutbox/pawprint-native"
|
|
95
|
+
*
|
|
96
|
+
* // With image
|
|
97
|
+
* <Avatar src="https://example.com/photo.jpg" alt="User Name" size="md" />
|
|
98
|
+
*
|
|
99
|
+
* // With initials fallback
|
|
100
|
+
* <Avatar alt="John Doe" fallbackString="JD" size="sm" border="sm" />
|
|
101
|
+
*
|
|
102
|
+
* // With icon fallback
|
|
103
|
+
* <Avatar alt="User avatar" fallbackType="image" size="lg" />
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @param {string} [src] - Image source URL.
|
|
107
|
+
* @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).
|
|
112
|
+
*/
|
|
113
|
+
const AvatarRoot = React.forwardRef<View, AvatarProps>(
|
|
114
|
+
(
|
|
115
|
+
{
|
|
116
|
+
src,
|
|
117
|
+
alt,
|
|
118
|
+
size = "md",
|
|
119
|
+
border = "none",
|
|
120
|
+
fallbackType,
|
|
121
|
+
fallbackString,
|
|
122
|
+
...rest
|
|
123
|
+
},
|
|
124
|
+
ref
|
|
125
|
+
) => {
|
|
126
|
+
// Auto-detect fallback type based on whether fallbackString is provided
|
|
127
|
+
const effectiveFallbackType =
|
|
128
|
+
fallbackType ?? (fallbackString ? "string" : "image")
|
|
129
|
+
|
|
130
|
+
const showStringFallback =
|
|
131
|
+
effectiveFallbackType === "string" && fallbackString
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<StyledRoot ref={ref} alt={alt} size={size} border={border} {...rest}>
|
|
135
|
+
{src && <StyledImage source={{ uri: src }} />}
|
|
136
|
+
<StyledFallback>
|
|
137
|
+
{showStringFallback ? (
|
|
138
|
+
<Typography
|
|
139
|
+
size={AVATAR_TO_BODY_SIZE[size]}
|
|
140
|
+
weight="bold"
|
|
141
|
+
color="disabled"
|
|
142
|
+
>
|
|
143
|
+
{fallbackString}
|
|
144
|
+
</Typography>
|
|
145
|
+
) : (
|
|
146
|
+
// TODO: replace with the Icon when available
|
|
147
|
+
<Typography size={AVATAR_TO_BODY_SIZE[size]} color="disabled">
|
|
148
|
+
👤
|
|
149
|
+
</Typography>
|
|
150
|
+
)}
|
|
151
|
+
</StyledFallback>
|
|
152
|
+
</StyledRoot>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
AvatarRoot.displayName = "Avatar"
|
|
158
|
+
|
|
159
|
+
export const Avatar = AvatarRoot
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet, Text } from "react-native"
|
|
3
|
+
import { Badge } from "./Badge"
|
|
4
|
+
import type { BadgeProps } from "./Badge"
|
|
5
|
+
|
|
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
|
+
export default {
|
|
27
|
+
title: "Atoms/Badge",
|
|
28
|
+
component: Badge,
|
|
29
|
+
parameters: {
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Badge component for displaying labels with optional icons. Used to highlight status, categories, or notifications."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
argTypes: {
|
|
38
|
+
variant: {
|
|
39
|
+
control: { type: "select" },
|
|
40
|
+
options: ["primary", "promo", "success", "warning", "error"],
|
|
41
|
+
description: "Visual style variant"
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
control: { type: "select" },
|
|
45
|
+
options: ["small", "medium", "large"],
|
|
46
|
+
description: "Size of the badge"
|
|
47
|
+
},
|
|
48
|
+
pinned: {
|
|
49
|
+
control: { type: "boolean" },
|
|
50
|
+
description: "If true, applies pinned styling (left border radius = 0)"
|
|
51
|
+
},
|
|
52
|
+
children: {
|
|
53
|
+
control: { type: "text" },
|
|
54
|
+
description: "Badge label text"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const variants = ["primary", "promo", "success", "warning", "error"] as const
|
|
60
|
+
const sizes = ["small", "medium", "large"] as const
|
|
61
|
+
|
|
62
|
+
export const Default = (args: BadgeProps) => <Badge {...args} />
|
|
63
|
+
Default.args = {
|
|
64
|
+
children: "Badge",
|
|
65
|
+
variant: "primary",
|
|
66
|
+
size: "medium"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const AllVariants = () => (
|
|
70
|
+
<View style={styles.column}>
|
|
71
|
+
{variants.map((variant) => (
|
|
72
|
+
<View key={variant} style={styles.section}>
|
|
73
|
+
<Text style={styles.label}>{variant}</Text>
|
|
74
|
+
<View style={styles.row}>
|
|
75
|
+
{sizes.map((size) => (
|
|
76
|
+
<Badge key={size} variant={variant} size={size}>
|
|
77
|
+
{size}
|
|
78
|
+
</Badge>
|
|
79
|
+
))}
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
))}
|
|
83
|
+
</View>
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
export const WithIcons = () => (
|
|
87
|
+
<View style={styles.column}>
|
|
88
|
+
{variants.map((variant) => (
|
|
89
|
+
<View key={variant} style={styles.section}>
|
|
90
|
+
<Text style={styles.label}>{variant} with Icon</Text>
|
|
91
|
+
<View style={styles.row}>
|
|
92
|
+
{sizes.map((size) => (
|
|
93
|
+
<Badge
|
|
94
|
+
key={size}
|
|
95
|
+
variant={variant}
|
|
96
|
+
size={size}
|
|
97
|
+
icon={PlaceholderIcon}
|
|
98
|
+
>
|
|
99
|
+
{size}
|
|
100
|
+
</Badge>
|
|
101
|
+
))}
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
))}
|
|
105
|
+
</View>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const Pinned = () => (
|
|
109
|
+
<View style={styles.column}>
|
|
110
|
+
<View style={styles.section}>
|
|
111
|
+
<Text style={styles.label}>Pinned Badges - Top Positioning</Text>
|
|
112
|
+
<View style={styles.row}>
|
|
113
|
+
{variants.map((variant) => (
|
|
114
|
+
<View key={variant} style={styles.pinnedContainer}>
|
|
115
|
+
<Badge
|
|
116
|
+
variant={variant}
|
|
117
|
+
size="medium"
|
|
118
|
+
pinned
|
|
119
|
+
top={16}
|
|
120
|
+
icon={PlaceholderIcon}
|
|
121
|
+
>
|
|
122
|
+
{variant}
|
|
123
|
+
</Badge>
|
|
124
|
+
</View>
|
|
125
|
+
))}
|
|
126
|
+
</View>
|
|
127
|
+
</View>
|
|
128
|
+
<View style={styles.section}>
|
|
129
|
+
<Text style={styles.label}>Pinned Badges - Bottom Positioning</Text>
|
|
130
|
+
<View style={styles.row}>
|
|
131
|
+
{variants.map((variant) => (
|
|
132
|
+
<View key={variant} style={styles.pinnedContainer}>
|
|
133
|
+
<Badge
|
|
134
|
+
variant={variant}
|
|
135
|
+
size="medium"
|
|
136
|
+
pinned
|
|
137
|
+
bottom={16}
|
|
138
|
+
icon={PlaceholderIcon}
|
|
139
|
+
>
|
|
140
|
+
{variant}
|
|
141
|
+
</Badge>
|
|
142
|
+
</View>
|
|
143
|
+
))}
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
</View>
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
export const UsageExamples = () => (
|
|
150
|
+
<View style={styles.column}>
|
|
151
|
+
<View style={styles.section}>
|
|
152
|
+
<Text style={styles.label}>Status indicators</Text>
|
|
153
|
+
<View style={styles.row}>
|
|
154
|
+
<Badge variant="success" size="small">
|
|
155
|
+
Active
|
|
156
|
+
</Badge>
|
|
157
|
+
<Badge variant="warning" size="small">
|
|
158
|
+
Pending
|
|
159
|
+
</Badge>
|
|
160
|
+
<Badge variant="error" size="small">
|
|
161
|
+
Expired
|
|
162
|
+
</Badge>
|
|
163
|
+
</View>
|
|
164
|
+
</View>
|
|
165
|
+
<View style={styles.section}>
|
|
166
|
+
<Text style={styles.label}>Category labels</Text>
|
|
167
|
+
<View style={styles.row}>
|
|
168
|
+
<Badge variant="primary" size="medium">
|
|
169
|
+
New
|
|
170
|
+
</Badge>
|
|
171
|
+
<Badge variant="promo" size="medium">
|
|
172
|
+
Offer
|
|
173
|
+
</Badge>
|
|
174
|
+
<Badge variant="success" size="medium">
|
|
175
|
+
Verified
|
|
176
|
+
</Badge>
|
|
177
|
+
</View>
|
|
178
|
+
</View>
|
|
179
|
+
<View style={styles.section}>
|
|
180
|
+
<Text style={styles.label}>Promotional tags</Text>
|
|
181
|
+
<View style={styles.row}>
|
|
182
|
+
<Badge variant="promo" size="large" icon={PlaceholderIcon}>
|
|
183
|
+
Premium
|
|
184
|
+
</Badge>
|
|
185
|
+
<Badge variant="primary" size="large">
|
|
186
|
+
New
|
|
187
|
+
</Badge>
|
|
188
|
+
<Badge variant="warning" size="large">
|
|
189
|
+
Sale
|
|
190
|
+
</Badge>
|
|
191
|
+
</View>
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
export const Playground = (args: BadgeProps) => <Badge {...args} />
|
|
197
|
+
Playground.args = {
|
|
198
|
+
variant: "primary",
|
|
199
|
+
size: "medium",
|
|
200
|
+
pinned: false,
|
|
201
|
+
children: "Badge"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const styles = StyleSheet.create({
|
|
205
|
+
column: {
|
|
206
|
+
flexDirection: "column",
|
|
207
|
+
gap: 24
|
|
208
|
+
},
|
|
209
|
+
section: {
|
|
210
|
+
flexDirection: "column",
|
|
211
|
+
gap: 8
|
|
212
|
+
},
|
|
213
|
+
row: {
|
|
214
|
+
flexDirection: "row",
|
|
215
|
+
gap: 12,
|
|
216
|
+
alignItems: "center"
|
|
217
|
+
},
|
|
218
|
+
label: {
|
|
219
|
+
fontSize: 13,
|
|
220
|
+
fontWeight: "600",
|
|
221
|
+
color: "#666"
|
|
222
|
+
},
|
|
223
|
+
pinnedContainer: {
|
|
224
|
+
position: "relative",
|
|
225
|
+
width: 160,
|
|
226
|
+
height: 100,
|
|
227
|
+
backgroundColor: "#f5f5f5",
|
|
228
|
+
borderRadius: 8,
|
|
229
|
+
overflow: "hidden"
|
|
230
|
+
}
|
|
231
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Typography } from "../Typography"
|
|
6
|
+
import { Icon, type PawprintIcon } from "../Icon"
|
|
7
|
+
|
|
8
|
+
type BadgeVariant = "primary" | "promo" | "success" | "warning" | "error"
|
|
9
|
+
type BadgeSize = "small" | "medium" | "large"
|
|
10
|
+
|
|
11
|
+
type BaseBadgeProps = {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
variant?: BadgeVariant
|
|
14
|
+
size?: BadgeSize
|
|
15
|
+
icon?: PawprintIcon
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PinnedBadgeProps = BaseBadgeProps & {
|
|
19
|
+
pinned: true
|
|
20
|
+
top?: number
|
|
21
|
+
bottom?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type UnpinnedBadgeProps = BaseBadgeProps & {
|
|
25
|
+
pinned?: false
|
|
26
|
+
top?: never
|
|
27
|
+
bottom?: never
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type BadgeOwnProps = PinnedBadgeProps | UnpinnedBadgeProps
|
|
31
|
+
|
|
32
|
+
export type BadgeProps = BadgeOwnProps & Omit<ViewProps, keyof BadgeOwnProps>
|
|
33
|
+
|
|
34
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
35
|
+
|
|
36
|
+
const StyledBadge = styled(View)<{
|
|
37
|
+
badgeVariant: BadgeVariant
|
|
38
|
+
badgeSize: BadgeSize
|
|
39
|
+
pinned: boolean
|
|
40
|
+
top?: number
|
|
41
|
+
bottom?: number
|
|
42
|
+
}>(({ theme, badgeVariant, badgeSize, pinned, top, bottom }) => {
|
|
43
|
+
const { sizing, spacing, colour, primary, pinnedBadge } =
|
|
44
|
+
theme.tokens.components.badges.badge
|
|
45
|
+
|
|
46
|
+
const backgroundColorMap = {
|
|
47
|
+
primary: colour.background.default,
|
|
48
|
+
promo: colour.background.promo,
|
|
49
|
+
success: colour.background.success,
|
|
50
|
+
warning: colour.background.warning,
|
|
51
|
+
error: colour.background.error
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
const borderRadiusStyles = pinned
|
|
55
|
+
? {
|
|
56
|
+
borderTopLeftRadius: parseTokenValue(
|
|
57
|
+
pinnedBadge.borderRadius.leftRadius
|
|
58
|
+
),
|
|
59
|
+
borderBottomLeftRadius: parseTokenValue(
|
|
60
|
+
pinnedBadge.borderRadius.leftRadius
|
|
61
|
+
),
|
|
62
|
+
borderTopRightRadius: parseTokenValue(
|
|
63
|
+
pinnedBadge.borderRadius.rightRadius
|
|
64
|
+
),
|
|
65
|
+
borderBottomRightRadius: parseTokenValue(
|
|
66
|
+
pinnedBadge.borderRadius.rightRadius
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
: {
|
|
70
|
+
borderRadius: parseTokenValue(primary.borderRadius.default)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const positionStyles = pinned
|
|
74
|
+
? {
|
|
75
|
+
position: "absolute" as const,
|
|
76
|
+
left: 0,
|
|
77
|
+
...(top !== undefined && { top }),
|
|
78
|
+
...(bottom !== undefined && { bottom })
|
|
79
|
+
}
|
|
80
|
+
: {}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
flexDirection: "row" as const,
|
|
84
|
+
alignItems: "center" as const,
|
|
85
|
+
justifyContent: "center" as const,
|
|
86
|
+
height: parseTokenValue(sizing[badgeSize].height),
|
|
87
|
+
minWidth: parseTokenValue(sizing[badgeSize].minWidth),
|
|
88
|
+
paddingHorizontal: parseTokenValue(spacing[badgeSize].horizontalPadding),
|
|
89
|
+
gap: parseTokenValue(spacing[badgeSize].gap),
|
|
90
|
+
backgroundColor: backgroundColorMap[badgeVariant],
|
|
91
|
+
...borderRadiusStyles,
|
|
92
|
+
...positionStyles
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Badge component for displaying labels with optional icons.
|
|
98
|
+
* Used to highlight status, categories, or notifications.
|
|
99
|
+
*
|
|
100
|
+
* @param {React.ReactNode} children - Badge label text.
|
|
101
|
+
* @param {"primary" | "promo" | "success" | "warning" | "error"} [variant="primary"] - Visual style variant.
|
|
102
|
+
* @param {"small" | "medium" | "large"} [size="medium"] - Size of the badge.
|
|
103
|
+
* @param {boolean} [pinned=false] - If true, applies pinned styling and positions absolutely to the left edge.
|
|
104
|
+
* @param {PawprintIcon} [icon] - Optional icon from @butternutbox/pawprint-icons.
|
|
105
|
+
* @param {number} [top] - Top position when pinned.
|
|
106
|
+
* @param {number} [bottom] - Bottom position when pinned.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* <Badge>New</Badge>
|
|
110
|
+
* <Badge variant="success" size="large" icon={CheckCircle}>Verified</Badge>
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* <View style={{ position: "relative" }}>
|
|
114
|
+
* <Badge variant="promo" pinned top={16}>Special</Badge>
|
|
115
|
+
* </View>
|
|
116
|
+
*/
|
|
117
|
+
export const Badge = React.forwardRef<View, BadgeProps>(
|
|
118
|
+
(
|
|
119
|
+
{
|
|
120
|
+
variant = "primary",
|
|
121
|
+
size = "medium",
|
|
122
|
+
pinned = false,
|
|
123
|
+
icon,
|
|
124
|
+
children,
|
|
125
|
+
top,
|
|
126
|
+
bottom,
|
|
127
|
+
...rest
|
|
128
|
+
},
|
|
129
|
+
ref
|
|
130
|
+
) => {
|
|
131
|
+
const theme = useTheme()
|
|
132
|
+
const { typography, colour } = theme.tokens.components.badges.badge
|
|
133
|
+
|
|
134
|
+
const iconSizeMap = {
|
|
135
|
+
small: "xs",
|
|
136
|
+
medium: "xs",
|
|
137
|
+
large: "sm"
|
|
138
|
+
} as const
|
|
139
|
+
|
|
140
|
+
const iconColourMap = {
|
|
141
|
+
primary: "primary",
|
|
142
|
+
promo: "promo",
|
|
143
|
+
success: "alt",
|
|
144
|
+
warning: "alt",
|
|
145
|
+
error: "alt"
|
|
146
|
+
} as const
|
|
147
|
+
|
|
148
|
+
const textColorMap = {
|
|
149
|
+
primary: colour.text.primary,
|
|
150
|
+
promo: colour.text.promo,
|
|
151
|
+
success: colour.text.default,
|
|
152
|
+
warning: colour.text.default,
|
|
153
|
+
error: colour.text.default
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<StyledBadge
|
|
158
|
+
ref={ref}
|
|
159
|
+
badgeVariant={variant}
|
|
160
|
+
badgeSize={size}
|
|
161
|
+
pinned={pinned}
|
|
162
|
+
top={top}
|
|
163
|
+
bottom={bottom}
|
|
164
|
+
{...rest}
|
|
165
|
+
>
|
|
166
|
+
{icon && (
|
|
167
|
+
<Icon
|
|
168
|
+
icon={icon}
|
|
169
|
+
size={iconSizeMap[size]}
|
|
170
|
+
colour={iconColourMap[variant]}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
<Typography
|
|
174
|
+
token={typography[size].default}
|
|
175
|
+
color={textColorMap[variant]}
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</Typography>
|
|
179
|
+
</StyledBadge>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
Badge.displayName = "Badge"
|