@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.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +30 -0
  2. package/COMPONENT_GUIDELINES.md +610 -0
  3. package/README.md +72 -0
  4. package/dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 +0 -0
  5. package/dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 +0 -0
  6. package/dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 +0 -0
  7. package/dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 +0 -0
  8. package/dist/ida-narrow-500-normal-C6I2PK4T.woff2 +0 -0
  9. package/dist/ida-narrow-700-normal-UPHPRIN6.woff2 +0 -0
  10. package/dist/index.cjs +2686 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +780 -0
  13. package/dist/index.d.ts +780 -0
  14. package/dist/index.js +2617 -0
  15. package/dist/index.js.map +1 -0
  16. package/eslint.config.js +3 -0
  17. package/llms.txt +458 -0
  18. package/package.json +57 -0
  19. package/src/components/atoms/Avatar/Avatar.stories.tsx +125 -0
  20. package/src/components/atoms/Avatar/Avatar.tsx +159 -0
  21. package/src/components/atoms/Avatar/index.ts +7 -0
  22. package/src/components/atoms/Badge/Badge.stories.tsx +231 -0
  23. package/src/components/atoms/Badge/Badge.tsx +184 -0
  24. package/src/components/atoms/Badge/index.ts +2 -0
  25. package/src/components/atoms/Button/Button.stories.tsx +145 -0
  26. package/src/components/atoms/Button/Button.tsx +261 -0
  27. package/src/components/atoms/Button/index.ts +7 -0
  28. package/src/components/atoms/Hint/Hint.stories.tsx +84 -0
  29. package/src/components/atoms/Hint/Hint.tsx +59 -0
  30. package/src/components/atoms/Hint/index.ts +2 -0
  31. package/src/components/atoms/Icon/Icon.stories.tsx +200 -0
  32. package/src/components/atoms/Icon/Icon.tsx +112 -0
  33. package/src/components/atoms/Icon/index.ts +8 -0
  34. package/src/components/atoms/IconButton/IconButton.stories.tsx +162 -0
  35. package/src/components/atoms/IconButton/IconButton.tsx +227 -0
  36. package/src/components/atoms/IconButton/index.ts +7 -0
  37. package/src/components/atoms/Illustration/Illustration.stories.tsx +167 -0
  38. package/src/components/atoms/Illustration/Illustration.tsx +81 -0
  39. package/src/components/atoms/Illustration/index.ts +6 -0
  40. package/src/components/atoms/Input/Input.stories.tsx +142 -0
  41. package/src/components/atoms/Input/Input.tsx +110 -0
  42. package/src/components/atoms/Input/InputDescription.tsx +49 -0
  43. package/src/components/atoms/Input/InputError.tsx +39 -0
  44. package/src/components/atoms/Input/InputField.tsx +119 -0
  45. package/src/components/atoms/Input/InputLabel.tsx +61 -0
  46. package/src/components/atoms/Input/index.ts +10 -0
  47. package/src/components/atoms/Link/Link.stories.tsx +119 -0
  48. package/src/components/atoms/Link/Link.tsx +118 -0
  49. package/src/components/atoms/Link/index.ts +2 -0
  50. package/src/components/atoms/Logo/Logo.registry.ts +39 -0
  51. package/src/components/atoms/Logo/Logo.tsx +68 -0
  52. package/src/components/atoms/Logo/index.ts +4 -0
  53. package/src/components/atoms/Spinner/Spinner.stories.tsx +98 -0
  54. package/src/components/atoms/Spinner/Spinner.tsx +91 -0
  55. package/src/components/atoms/Spinner/index.ts +2 -0
  56. package/src/components/atoms/Switch/Switch.stories.tsx +120 -0
  57. package/src/components/atoms/Switch/Switch.tsx +196 -0
  58. package/src/components/atoms/Switch/index.ts +2 -0
  59. package/src/components/atoms/Tag/Tag.stories.tsx +89 -0
  60. package/src/components/atoms/Tag/Tag.tsx +122 -0
  61. package/src/components/atoms/Tag/index.ts +2 -0
  62. package/src/components/atoms/Typography/Typography.stories.tsx +315 -0
  63. package/src/components/atoms/Typography/Typography.tsx +284 -0
  64. package/src/components/atoms/Typography/index.ts +2 -0
  65. package/src/components/atoms/index.ts +14 -0
  66. package/src/components/index.ts +2 -0
  67. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +95 -0
  68. package/src/components/molecules/ButtonDock/ButtonDock.tsx +148 -0
  69. package/src/components/molecules/ButtonDock/index.ts +2 -0
  70. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +82 -0
  71. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +94 -0
  72. package/src/components/molecules/ButtonGroup/index.ts +2 -0
  73. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +148 -0
  74. package/src/components/molecules/Checkbox/Checkbox.tsx +279 -0
  75. package/src/components/molecules/Checkbox/CheckboxGroup.tsx +53 -0
  76. package/src/components/molecules/Checkbox/index.ts +4 -0
  77. package/src/components/molecules/Radio/Radio.stories.tsx +182 -0
  78. package/src/components/molecules/Radio/Radio.tsx +249 -0
  79. package/src/components/molecules/Radio/RadioGroup.tsx +142 -0
  80. package/src/components/molecules/Radio/index.ts +4 -0
  81. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +151 -0
  82. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +323 -0
  83. package/src/components/molecules/SegmentedControl/index.ts +5 -0
  84. package/src/components/molecules/Slider/Slider.stories.tsx +144 -0
  85. package/src/components/molecules/Slider/Slider.tsx +303 -0
  86. package/src/components/molecules/Slider/index.ts +2 -0
  87. package/src/components/molecules/index.ts +6 -0
  88. package/src/fonts/ibm-plex-sans-condensed-400-normal.woff2 +0 -0
  89. package/src/fonts/ibm-plex-sans-condensed-500-normal.woff2 +0 -0
  90. package/src/fonts/ibm-plex-sans-condensed-600-normal.woff2 +0 -0
  91. package/src/fonts/ibm-plex-sans-condensed-700-normal.woff2 +0 -0
  92. package/src/fonts/ida-narrow-500-normal.woff2 +0 -0
  93. package/src/fonts/ida-narrow-700-normal.woff2 +0 -0
  94. package/src/fonts/index.ts +49 -0
  95. package/src/index.ts +9 -0
  96. package/src/theme/PawprintProvider.tsx +26 -0
  97. package/src/theme/ThemeProvider.tsx +63 -0
  98. package/src/theme/index.ts +5 -0
  99. package/src/theme/theme.ts +3 -0
  100. package/src/theme/utils.ts +31 -0
  101. package/src/types/fonts.d.ts +4 -0
  102. package/src/types/index.ts +1 -0
  103. package/src/types/theme.ts +24 -0
  104. package/tsconfig.json +5 -0
  105. package/tsup.config.ts +11 -0
@@ -0,0 +1,91 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import { Animated, Easing, View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+
6
+ type SpinnerSize = "sm" | "md" | "lg"
7
+ type SpinnerVariant = "dark" | "light"
8
+
9
+ type SpinnerOwnProps = {
10
+ size?: SpinnerSize
11
+ variant?: SpinnerVariant
12
+ }
13
+
14
+ type SpinnerProps = SpinnerOwnProps & Omit<ViewProps, keyof SpinnerOwnProps>
15
+
16
+ const parseTokenValue = (value: string): number => parseFloat(value)
17
+
18
+ const StyledSpinner = styled(View)({})
19
+
20
+ /**
21
+ * A spinner component for indicating indeterminate loading states.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { Spinner } from "@butternutbox/pawprint-native"
26
+ *
27
+ * <Spinner />
28
+ * <Spinner size="lg" variant="light" />
29
+ * ```
30
+ *
31
+ * @param size - *(optional)* Size variant: sm, md (default), lg
32
+ * @param variant - *(optional)* Colour variant: dark (default), light
33
+ */
34
+ const Spinner = React.forwardRef<View, SpinnerProps>(
35
+ ({ size = "md", variant = "dark", style, ...rest }, ref) => {
36
+ const theme = useTheme()
37
+ const spinAnim = useRef(new Animated.Value(0)).current
38
+
39
+ const { size: sizeTokens, colour } = theme.tokens.components.spinner
40
+ const borderWidth = parseTokenValue(
41
+ theme.tokens.semantics.dimensions.borderWidth.md
42
+ )
43
+ const baseColor = colour.background.base[variant]
44
+ const progressColor = colour.background.progress[variant]
45
+ const dimension = parseTokenValue(sizeTokens[size])
46
+
47
+ useEffect(() => {
48
+ const animation = Animated.loop(
49
+ Animated.timing(spinAnim, {
50
+ toValue: 1,
51
+ duration: 600,
52
+ easing: Easing.linear,
53
+ useNativeDriver: true
54
+ })
55
+ )
56
+ animation.start()
57
+ return () => animation.stop()
58
+ }, [spinAnim])
59
+
60
+ const spin = spinAnim.interpolate({
61
+ inputRange: [0, 1],
62
+ outputRange: ["0deg", "360deg"]
63
+ })
64
+
65
+ return (
66
+ <StyledSpinner ref={ref} {...rest}>
67
+ <Animated.View
68
+ accessibilityRole="progressbar"
69
+ accessibilityLabel="Loading"
70
+ style={[
71
+ {
72
+ width: dimension,
73
+ height: dimension,
74
+ borderRadius: dimension / 2,
75
+ borderWidth,
76
+ borderColor: baseColor,
77
+ borderTopColor: progressColor,
78
+ transform: [{ rotate: spin }]
79
+ },
80
+ style
81
+ ]}
82
+ />
83
+ </StyledSpinner>
84
+ )
85
+ }
86
+ )
87
+
88
+ Spinner.displayName = "Spinner"
89
+
90
+ export { Spinner }
91
+ export type { SpinnerProps, SpinnerSize, SpinnerVariant }
@@ -0,0 +1,2 @@
1
+ export { Spinner } from "./Spinner"
2
+ export type { SpinnerProps, SpinnerSize, SpinnerVariant } from "./Spinner"
@@ -0,0 +1,120 @@
1
+ import React, { useState } from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { Switch } from "./Switch"
4
+ import type { SwitchProps } from "./Switch"
5
+ import { Typography } from "../Typography"
6
+
7
+ export default {
8
+ title: "Atoms/Switch",
9
+ component: Switch,
10
+ argTypes: {
11
+ label: {
12
+ control: { type: "text" },
13
+ description: "Main label text"
14
+ },
15
+ subText: {
16
+ control: { type: "text" },
17
+ description: "Optional descriptive subtext"
18
+ },
19
+ disabled: {
20
+ control: { type: "boolean" },
21
+ description: "Prevents interaction"
22
+ },
23
+ defaultChecked: {
24
+ control: { type: "boolean" },
25
+ description: "Initial checked state (uncontrolled)"
26
+ }
27
+ }
28
+ }
29
+
30
+ export const Playground = (args: SwitchProps) => <Switch {...args} />
31
+ Playground.args = {
32
+ label: "Notifications",
33
+ subText: "Receive push notifications",
34
+ disabled: false,
35
+ defaultChecked: false
36
+ }
37
+
38
+ export const ContentVariants = () => (
39
+ <View style={styles.column}>
40
+ <View style={styles.section}>
41
+ <Typography size="sm" weight="semiBold" color="tertiary">
42
+ No label
43
+ </Typography>
44
+ <Switch />
45
+ </View>
46
+ <View style={styles.section}>
47
+ <Typography size="sm" weight="semiBold" color="tertiary">
48
+ Label only
49
+ </Typography>
50
+ <Switch label="SMS Delivery Notifications" />
51
+ </View>
52
+ <View style={styles.section}>
53
+ <Typography size="sm" weight="semiBold" color="tertiary">
54
+ Label + subtext
55
+ </Typography>
56
+ <Switch
57
+ label="SMS Delivery Notifications"
58
+ subText="I want to receive SMS notifications about my deliveries"
59
+ />
60
+ </View>
61
+ </View>
62
+ )
63
+
64
+ export const States = () => (
65
+ <View style={styles.column}>
66
+ <View style={styles.section}>
67
+ <Typography size="sm" weight="semiBold" color="tertiary">
68
+ Enabled (off)
69
+ </Typography>
70
+ <Switch label="Enabled switch" />
71
+ </View>
72
+ <View style={styles.section}>
73
+ <Typography size="sm" weight="semiBold" color="tertiary">
74
+ Enabled (on)
75
+ </Typography>
76
+ <Switch label="Enabled switch" defaultChecked />
77
+ </View>
78
+ <View style={styles.section}>
79
+ <Typography size="sm" weight="semiBold" color="tertiary">
80
+ Disabled (off)
81
+ </Typography>
82
+ <Switch label="Disabled switch" disabled />
83
+ </View>
84
+ <View style={styles.section}>
85
+ <Typography size="sm" weight="semiBold" color="tertiary">
86
+ Disabled (on)
87
+ </Typography>
88
+ <Switch label="Disabled switch" disabled defaultChecked />
89
+ </View>
90
+ </View>
91
+ )
92
+
93
+ export const Controlled = () => {
94
+ const [checked, setChecked] = useState(false)
95
+
96
+ return (
97
+ <View style={styles.column}>
98
+ <Typography size="sm" weight="semiBold" color="tertiary">
99
+ Controlled: {checked ? "ON" : "OFF"}
100
+ </Typography>
101
+ <Switch
102
+ label="Controlled switch"
103
+ subText="Toggle me to see state change"
104
+ checked={checked}
105
+ onCheckedChange={setChecked}
106
+ />
107
+ </View>
108
+ )
109
+ }
110
+
111
+ const styles = StyleSheet.create({
112
+ column: {
113
+ flexDirection: "column",
114
+ gap: 24
115
+ },
116
+ section: {
117
+ flexDirection: "column",
118
+ gap: 8
119
+ }
120
+ })
@@ -0,0 +1,196 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import { View, Animated, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import * as SwitchPrimitive from "@rn-primitives/switch"
6
+ import { Typography } from "../Typography"
7
+
8
+ type SwitchOwnProps = {
9
+ label?: React.ReactNode
10
+ subText?: React.ReactNode
11
+ disabled?: boolean
12
+ checked?: boolean
13
+ defaultChecked?: boolean
14
+ onCheckedChange?: (checked: boolean) => void
15
+ }
16
+
17
+ export type SwitchProps = SwitchOwnProps & Omit<ViewProps, keyof SwitchOwnProps>
18
+
19
+ const parseTokenValue = (value: string): number => parseFloat(value)
20
+
21
+ const StyledContainer = styled(View)<{
22
+ switchGap: number
23
+ switchOpacity: number
24
+ }>(({ switchGap, switchOpacity }) => ({
25
+ flexDirection: "row",
26
+ alignItems: "flex-start",
27
+ gap: switchGap,
28
+ opacity: switchOpacity
29
+ }))
30
+
31
+ const StyledControl = styled(SwitchPrimitive.Root)<{
32
+ switchChecked: boolean
33
+ controlWidth: number
34
+ controlHeight: number
35
+ controlBorderWidth: number
36
+ controlBorderColor: string
37
+ controlBgChecked: string
38
+ controlBgDefault: string
39
+ }>(
40
+ ({
41
+ switchChecked,
42
+ controlWidth,
43
+ controlHeight,
44
+ controlBorderWidth,
45
+ controlBorderColor,
46
+ controlBgChecked,
47
+ controlBgDefault
48
+ }) => ({
49
+ position: "relative",
50
+ width: controlWidth,
51
+ height: controlHeight,
52
+ minWidth: controlWidth,
53
+ minHeight: controlHeight,
54
+ borderRadius: controlHeight,
55
+ borderWidth: switchChecked ? 0 : controlBorderWidth,
56
+ borderColor: switchChecked ? "transparent" : controlBorderColor,
57
+ backgroundColor: switchChecked ? controlBgChecked : controlBgDefault,
58
+ justifyContent: "center"
59
+ })
60
+ )
61
+
62
+ const StyledThumb = styled(Animated.View)<{
63
+ thumbSize: number
64
+ thumbBgColor: string
65
+ }>(({ thumbSize, thumbBgColor }) => ({
66
+ width: thumbSize,
67
+ height: thumbSize,
68
+ borderRadius: thumbSize / 2,
69
+ backgroundColor: thumbBgColor,
70
+ position: "absolute"
71
+ }))
72
+
73
+ const StyledContent = styled(View)<{
74
+ contentGap: number
75
+ }>(({ contentGap }) => ({
76
+ gap: contentGap
77
+ }))
78
+
79
+ /**
80
+ * Switch component for toggling between two states, on and off.
81
+ *
82
+ * @param {React.ReactNode} [label] - Main label text or content.
83
+ * @param {React.ReactNode} [subText] - Optional descriptive subtext.
84
+ * @param {boolean} [disabled=false] - Whether the switch is disabled.
85
+ * @param {boolean} [checked] - Controlled checked state.
86
+ * @param {(checked: boolean) => void} [onCheckedChange] - Controlled change handler.
87
+ *
88
+ * @example
89
+ * <Switch label="SMS Delivery Notifications" subText="I want to receive SMS notifications" />
90
+ */
91
+ export const Switch = React.forwardRef<View, SwitchProps>(
92
+ (
93
+ {
94
+ label,
95
+ subText,
96
+ disabled = false,
97
+ checked: controlledChecked,
98
+ defaultChecked = false,
99
+ onCheckedChange,
100
+ ...rest
101
+ },
102
+ ref
103
+ ) => {
104
+ const theme = useTheme()
105
+ const { size, colour, opacity, borderWidth, spacing, typography } =
106
+ theme.tokens.components.switch
107
+
108
+ const isControlled = controlledChecked !== undefined
109
+ const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
110
+ const isChecked = isControlled ? controlledChecked : internalChecked
111
+
112
+ const controlWidth = parseTokenValue(size.control.width)
113
+ const controlHeight = parseTokenValue(size.control.height)
114
+ const thumbSize = parseTokenValue(size.thumb.default)
115
+ const borderWidthValue = parseTokenValue(borderWidth.control.default)
116
+ const outerInset = 5
117
+ const inset = outerInset - borderWidthValue
118
+ const translateX = controlWidth - thumbSize - outerInset * 2
119
+
120
+ const animValue = useRef(new Animated.Value(isChecked ? 1 : 0)).current
121
+
122
+ useEffect(() => {
123
+ Animated.timing(animValue, {
124
+ toValue: isChecked ? 1 : 0,
125
+ duration: 200,
126
+ useNativeDriver: true
127
+ }).start()
128
+ }, [isChecked, animValue])
129
+
130
+ const thumbTranslateX = animValue.interpolate({
131
+ inputRange: [0, 1],
132
+ outputRange: [inset, inset + translateX]
133
+ })
134
+
135
+ const handleCheckedChange = (checked: boolean) => {
136
+ if (!isControlled) {
137
+ setInternalChecked(checked)
138
+ }
139
+ onCheckedChange?.(checked)
140
+ }
141
+
142
+ return (
143
+ <StyledContainer
144
+ ref={ref}
145
+ switchGap={parseTokenValue(spacing.gap)}
146
+ switchOpacity={disabled ? parseFloat(opacity.disabled) : 1}
147
+ {...rest}
148
+ >
149
+ <StyledControl
150
+ checked={isChecked}
151
+ onCheckedChange={handleCheckedChange}
152
+ disabled={disabled}
153
+ switchChecked={isChecked}
154
+ controlWidth={controlWidth}
155
+ controlHeight={controlHeight}
156
+ controlBorderWidth={borderWidthValue}
157
+ controlBorderColor={colour.control.border.default}
158
+ controlBgChecked={colour.control.background.selected}
159
+ controlBgDefault={colour.control.background.default}
160
+ >
161
+ <SwitchPrimitive.Thumb asChild>
162
+ <StyledThumb
163
+ thumbSize={thumbSize}
164
+ thumbBgColor={
165
+ isChecked
166
+ ? colour.thumb.background.selected
167
+ : colour.thumb.background.default
168
+ }
169
+ style={{ transform: [{ translateX: thumbTranslateX }] }}
170
+ />
171
+ </SwitchPrimitive.Thumb>
172
+ </StyledControl>
173
+
174
+ {(label || subText) && (
175
+ <StyledContent contentGap={parseTokenValue(spacing.content.gap)}>
176
+ {label && (
177
+ <Typography token={typography.label} color={colour.text.title}>
178
+ {label}
179
+ </Typography>
180
+ )}
181
+ {subText && (
182
+ <Typography
183
+ token={typography.subText}
184
+ color={colour.text.subtext}
185
+ >
186
+ {subText}
187
+ </Typography>
188
+ )}
189
+ </StyledContent>
190
+ )}
191
+ </StyledContainer>
192
+ )
193
+ }
194
+ )
195
+
196
+ Switch.displayName = "Switch"
@@ -0,0 +1,2 @@
1
+ export { Switch } from "./Switch"
2
+ export type { SwitchProps } from "./Switch"
@@ -0,0 +1,89 @@
1
+ import React from "react"
2
+ import { View, StyleSheet, Text } from "react-native"
3
+ import { Tag } from "./Tag"
4
+ import type { TagProps } from "./Tag"
5
+
6
+ export default {
7
+ title: "Atoms/Tag",
8
+ component: Tag,
9
+ argTypes: {
10
+ variant: {
11
+ control: { type: "select" },
12
+ options: [
13
+ "primary",
14
+ "secondary",
15
+ "tertiary",
16
+ "promo",
17
+ "success",
18
+ "warning",
19
+ "error"
20
+ ],
21
+ description: "Visual style variant"
22
+ },
23
+ size: {
24
+ control: { type: "select" },
25
+ options: ["small", "medium", "large"],
26
+ description: "Size of the tag"
27
+ },
28
+ children: {
29
+ control: { type: "text" },
30
+ description: "Tag label text"
31
+ }
32
+ }
33
+ }
34
+
35
+ export const Playground = (args: TagProps) => <Tag {...args} />
36
+ Playground.args = {
37
+ children: "Tag",
38
+ variant: "primary",
39
+ size: "medium"
40
+ }
41
+
42
+ const tagVariants = [
43
+ "primary",
44
+ "secondary",
45
+ "tertiary",
46
+ "promo",
47
+ "success",
48
+ "warning",
49
+ "error"
50
+ ] as const
51
+ const tagSizes = ["small", "medium", "large"] as const
52
+
53
+ export const AllVariants = () => (
54
+ <View style={styles.column}>
55
+ {tagVariants.map((variant) => (
56
+ <View key={variant} style={styles.section}>
57
+ <Text style={styles.label}>{variant}</Text>
58
+ <View style={styles.row}>
59
+ {tagSizes.map((size) => (
60
+ <Tag key={size} variant={variant} size={size}>
61
+ {size}
62
+ </Tag>
63
+ ))}
64
+ </View>
65
+ </View>
66
+ ))}
67
+ </View>
68
+ )
69
+
70
+ const styles = StyleSheet.create({
71
+ column: {
72
+ flexDirection: "column",
73
+ gap: 24
74
+ },
75
+ section: {
76
+ flexDirection: "column",
77
+ gap: 8
78
+ },
79
+ row: {
80
+ flexDirection: "row",
81
+ gap: 12,
82
+ alignItems: "center"
83
+ },
84
+ label: {
85
+ fontSize: 13,
86
+ fontWeight: "600",
87
+ color: "#666"
88
+ }
89
+ })
@@ -0,0 +1,122 @@
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 TagVariant =
9
+ | "primary"
10
+ | "secondary"
11
+ | "tertiary"
12
+ | "promo"
13
+ | "success"
14
+ | "warning"
15
+ | "error"
16
+
17
+ type TagSize = "small" | "medium" | "large"
18
+
19
+ type TagOwnProps = {
20
+ children: React.ReactNode
21
+ variant?: TagVariant
22
+ size?: TagSize
23
+ icon?: PawprintIcon
24
+ }
25
+
26
+ export type TagProps = TagOwnProps & Omit<ViewProps, keyof TagOwnProps>
27
+
28
+ const parseTokenValue = (value: string): number => parseFloat(value)
29
+
30
+ const StyledTag = styled(View)<{
31
+ tagVariant: TagVariant
32
+ tagSize: TagSize
33
+ }>(({ theme, tagVariant, tagSize }) => {
34
+ const { sizing, spacing, colour, badge } = theme.tokens.components.tags
35
+
36
+ const backgroundColorMap = {
37
+ primary: colour.primary.background,
38
+ secondary: colour.primary.secondary,
39
+ tertiary: colour.primary.tertiery,
40
+ promo: colour.primary.promo,
41
+ success: colour.primary.success,
42
+ warning: colour.primary.warning,
43
+ error: colour.primary.error
44
+ } as const
45
+
46
+ return {
47
+ flexDirection: "row",
48
+ alignItems: "center",
49
+ justifyContent: "center",
50
+ height: parseTokenValue(sizing[tagSize].height),
51
+ minWidth: parseTokenValue(sizing[tagSize].minWidth),
52
+ paddingHorizontal: parseTokenValue(spacing.horizontalPadding),
53
+ gap: parseTokenValue(spacing[tagSize].gap),
54
+ borderRadius: parseTokenValue(badge.borderRadius.default),
55
+ backgroundColor: backgroundColorMap[tagVariant]
56
+ }
57
+ })
58
+
59
+ /**
60
+ * Tag component for labelling and categorising content.
61
+ *
62
+ * @param {React.ReactNode} children - Tag label text.
63
+ * @param {"primary" | "secondary" | "tertiary" | "promo" | "success" | "warning" | "error"} [variant="primary"] - Visual style variant.
64
+ * @param {"small" | "medium" | "large"} [size="medium"] - Size of the tag.
65
+ * @param {PawprintIcon} [icon] - Optional leading icon from @butternutbox/pawprint-icons.
66
+ *
67
+ * @example
68
+ * <Tag>Company news</Tag>
69
+ * <Tag variant="success" size="large" icon={CheckCircle}>Verified</Tag>
70
+ */
71
+ export const Tag = React.forwardRef<View, TagProps>(
72
+ ({ variant = "primary", size = "medium", icon, children, ...rest }, ref) => {
73
+ const theme = useTheme()
74
+ const { typography, colour } = theme.tokens.components.tags
75
+
76
+ const iconSizeMap = {
77
+ small: "xs",
78
+ medium: "xs",
79
+ large: "sm"
80
+ } as const
81
+
82
+ const iconColourMap = {
83
+ primary: "primary",
84
+ secondary: "primary",
85
+ tertiary: "primary",
86
+ promo: "promo",
87
+ success: "success",
88
+ warning: "warning",
89
+ error: "error"
90
+ } as const
91
+
92
+ const textColorMap = {
93
+ primary: colour.text.default,
94
+ secondary: colour.text.default,
95
+ tertiary: colour.text.default,
96
+ promo: colour.text.promo,
97
+ success: colour.text.success,
98
+ warning: colour.text.warning,
99
+ error: colour.text.error
100
+ }
101
+
102
+ return (
103
+ <StyledTag ref={ref} tagVariant={variant} tagSize={size} {...rest}>
104
+ {icon && (
105
+ <Icon
106
+ icon={icon}
107
+ size={iconSizeMap[size]}
108
+ colour={iconColourMap[variant]}
109
+ />
110
+ )}
111
+ <Typography
112
+ token={typography[size].default}
113
+ color={textColorMap[variant]}
114
+ >
115
+ {children}
116
+ </Typography>
117
+ </StyledTag>
118
+ )
119
+ }
120
+ )
121
+
122
+ Tag.displayName = "Tag"
@@ -0,0 +1,2 @@
1
+ export { Tag } from "./Tag"
2
+ export type { TagProps } from "./Tag"