@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,200 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet, Text } from "react-native"
|
|
3
|
+
import { Icon } from "./Icon"
|
|
4
|
+
import type { IconProps, PawprintIcon, IconColour, IconSize } from "./Icon"
|
|
5
|
+
import * as core from "@butternutbox/pawprint-icons/core"
|
|
6
|
+
import * as marketing from "@butternutbox/pawprint-icons/marketing"
|
|
7
|
+
import * as payments from "@butternutbox/pawprint-icons/payments"
|
|
8
|
+
import * as flags from "@butternutbox/pawprint-icons/flags"
|
|
9
|
+
|
|
10
|
+
type IconCategory = "core" | "marketing" | "payments" | "flags"
|
|
11
|
+
|
|
12
|
+
function isComponent(value: unknown): value is PawprintIcon {
|
|
13
|
+
if (typeof value === "function") return true
|
|
14
|
+
if (value && typeof value === "object" && "$$typeof" in value) return true
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const categorised: Record<IconCategory, Record<string, unknown>> = {
|
|
19
|
+
core,
|
|
20
|
+
marketing,
|
|
21
|
+
payments,
|
|
22
|
+
flags
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const iconMap: Record<string, PawprintIcon> = {}
|
|
26
|
+
for (const [category, mod] of Object.entries(categorised)) {
|
|
27
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
28
|
+
if (isComponent(value)) {
|
|
29
|
+
iconMap[`${category}/${name}`] = value
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const iconNames = Object.keys(iconMap)
|
|
35
|
+
const defaultIcon = iconNames[0]!
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
title: "Atoms/Icon",
|
|
39
|
+
component: Icon,
|
|
40
|
+
argTypes: {
|
|
41
|
+
size: {
|
|
42
|
+
control: { type: "select" },
|
|
43
|
+
options: ["xs", "sm", "md", "lg", "xl", "2xl"] as IconSize[],
|
|
44
|
+
description: "Size variant based on design tokens"
|
|
45
|
+
},
|
|
46
|
+
colour: {
|
|
47
|
+
control: { type: "select" },
|
|
48
|
+
options: [
|
|
49
|
+
"primary",
|
|
50
|
+
"secondary",
|
|
51
|
+
"disabled",
|
|
52
|
+
"success",
|
|
53
|
+
"warning",
|
|
54
|
+
"error",
|
|
55
|
+
"promo",
|
|
56
|
+
"info",
|
|
57
|
+
"alt",
|
|
58
|
+
"action-default",
|
|
59
|
+
"action-inverse"
|
|
60
|
+
] as IconColour[],
|
|
61
|
+
description: "Semantic colour from icon tokens"
|
|
62
|
+
},
|
|
63
|
+
icon: {
|
|
64
|
+
control: { type: "select" },
|
|
65
|
+
options: iconNames,
|
|
66
|
+
mapping: iconMap,
|
|
67
|
+
description: "Icon component from pawprint-icons"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Default = (args: Omit<IconProps, "icon"> & { icon?: string }) => (
|
|
73
|
+
<Icon
|
|
74
|
+
icon={
|
|
75
|
+
(iconMap[args.icon ?? defaultIcon] ??
|
|
76
|
+
iconMap[defaultIcon]) as PawprintIcon
|
|
77
|
+
}
|
|
78
|
+
size={args.size}
|
|
79
|
+
colour={args.colour}
|
|
80
|
+
aria-label="Icon"
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
Default.args = {
|
|
84
|
+
icon: defaultIcon,
|
|
85
|
+
size: "md",
|
|
86
|
+
colour: "primary"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sizes: IconSize[] = ["xs", "sm", "md", "lg", "xl", "2xl"]
|
|
90
|
+
|
|
91
|
+
export const AllSizes = () => {
|
|
92
|
+
const sampleIcon = iconMap[defaultIcon] as PawprintIcon
|
|
93
|
+
return (
|
|
94
|
+
<View style={styles.column}>
|
|
95
|
+
<Text style={styles.label}>Sizes</Text>
|
|
96
|
+
<View style={styles.row}>
|
|
97
|
+
{sizes.map((size) => (
|
|
98
|
+
<View key={size} style={styles.cell}>
|
|
99
|
+
<Icon icon={sampleIcon} size={size} colour="primary" />
|
|
100
|
+
<Text style={styles.sizeLabel}>{size}</Text>
|
|
101
|
+
</View>
|
|
102
|
+
))}
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const colours: IconColour[] = [
|
|
109
|
+
"primary",
|
|
110
|
+
"secondary",
|
|
111
|
+
"disabled",
|
|
112
|
+
"success",
|
|
113
|
+
"warning",
|
|
114
|
+
"error",
|
|
115
|
+
"promo",
|
|
116
|
+
"info",
|
|
117
|
+
"alt",
|
|
118
|
+
"action-default",
|
|
119
|
+
"action-inverse"
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
export const AllColours = () => {
|
|
123
|
+
const sampleIcon = iconMap[defaultIcon] as PawprintIcon
|
|
124
|
+
return (
|
|
125
|
+
<View style={styles.column}>
|
|
126
|
+
<Text style={styles.label}>Colours</Text>
|
|
127
|
+
<View style={styles.row}>
|
|
128
|
+
{colours.map((colour) => (
|
|
129
|
+
<View key={colour} style={styles.cell}>
|
|
130
|
+
<Icon icon={sampleIcon} size="md" colour={colour} />
|
|
131
|
+
<Text style={styles.sizeLabel}>{colour}</Text>
|
|
132
|
+
</View>
|
|
133
|
+
))}
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const AllIcons = () => (
|
|
140
|
+
<View style={styles.column}>
|
|
141
|
+
{Object.entries(categorised).map(([category, mod]) => (
|
|
142
|
+
<View key={category} style={styles.section}>
|
|
143
|
+
<Text style={styles.label}>{category}</Text>
|
|
144
|
+
<View style={styles.grid}>
|
|
145
|
+
{Object.entries(mod)
|
|
146
|
+
.filter(([, value]) => isComponent(value))
|
|
147
|
+
.map(([name, Component]) => (
|
|
148
|
+
<View key={name} style={styles.cell}>
|
|
149
|
+
<Icon
|
|
150
|
+
icon={Component as PawprintIcon}
|
|
151
|
+
size="md"
|
|
152
|
+
colour="primary"
|
|
153
|
+
/>
|
|
154
|
+
<Text style={styles.sizeLabel} numberOfLines={1}>
|
|
155
|
+
{name}
|
|
156
|
+
</Text>
|
|
157
|
+
</View>
|
|
158
|
+
))}
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
))}
|
|
162
|
+
</View>
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const styles = StyleSheet.create({
|
|
166
|
+
column: {
|
|
167
|
+
flexDirection: "column",
|
|
168
|
+
gap: 24
|
|
169
|
+
},
|
|
170
|
+
section: {
|
|
171
|
+
flexDirection: "column",
|
|
172
|
+
gap: 8
|
|
173
|
+
},
|
|
174
|
+
row: {
|
|
175
|
+
flexDirection: "row",
|
|
176
|
+
gap: 16,
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
flexWrap: "wrap"
|
|
179
|
+
},
|
|
180
|
+
grid: {
|
|
181
|
+
flexDirection: "row",
|
|
182
|
+
gap: 12,
|
|
183
|
+
flexWrap: "wrap"
|
|
184
|
+
},
|
|
185
|
+
cell: {
|
|
186
|
+
alignItems: "center",
|
|
187
|
+
gap: 4,
|
|
188
|
+
minWidth: 48
|
|
189
|
+
},
|
|
190
|
+
label: {
|
|
191
|
+
fontSize: 13,
|
|
192
|
+
fontWeight: "600",
|
|
193
|
+
color: "#666"
|
|
194
|
+
},
|
|
195
|
+
sizeLabel: {
|
|
196
|
+
fontSize: 10,
|
|
197
|
+
color: "#999",
|
|
198
|
+
textAlign: "center"
|
|
199
|
+
}
|
|
200
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
|
|
6
|
+
type IconCategory = "core" | "marketing" | "payments" | "flags"
|
|
7
|
+
type IconSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
|
|
8
|
+
type IconColour =
|
|
9
|
+
| "primary"
|
|
10
|
+
| "secondary"
|
|
11
|
+
| "disabled"
|
|
12
|
+
| "success"
|
|
13
|
+
| "warning"
|
|
14
|
+
| "error"
|
|
15
|
+
| "promo"
|
|
16
|
+
| "info"
|
|
17
|
+
| "alt"
|
|
18
|
+
| "action-default"
|
|
19
|
+
| "action-inverse"
|
|
20
|
+
|
|
21
|
+
type PawprintIcon = React.ComponentType<{
|
|
22
|
+
width?: number
|
|
23
|
+
height?: number
|
|
24
|
+
color?: string
|
|
25
|
+
}> & {
|
|
26
|
+
category?: IconCategory
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type IconOwnProps = {
|
|
30
|
+
icon: PawprintIcon
|
|
31
|
+
size?: IconSize
|
|
32
|
+
colour?: IconColour
|
|
33
|
+
"aria-label"?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type IconProps = IconOwnProps & Omit<ViewProps, keyof IconOwnProps>
|
|
37
|
+
|
|
38
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
39
|
+
|
|
40
|
+
const StyledIconRoot = styled(View)<{
|
|
41
|
+
iconDimension: number
|
|
42
|
+
}>(({ iconDimension }) => ({
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
flexShrink: 0,
|
|
46
|
+
width: iconDimension,
|
|
47
|
+
height: iconDimension
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Renders an SVG icon with token-based sizing and colour.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* import { Icon } from "@butternutbox/pawprint-native"
|
|
56
|
+
* import { ArrowRight } from "@butternutbox/pawprint-icons/core"
|
|
57
|
+
*
|
|
58
|
+
* <Icon icon={ArrowRight} size="md" colour="primary" aria-label="Go forward" />
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @param icon - **(required)** Icon component from `@butternutbox/pawprint-icons/{category}`
|
|
62
|
+
* @param size - *(optional)* Size variant: xs, sm, md (default), lg, xl, 2xl
|
|
63
|
+
* @param colour - *(optional)* Colour variant from semantic icon tokens (default: primary)
|
|
64
|
+
* @param aria-label - *(optional)* Accessible label; omit for decorative icons
|
|
65
|
+
*/
|
|
66
|
+
const Icon = React.forwardRef<View, IconProps>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
icon: IconComponent,
|
|
70
|
+
size = "md",
|
|
71
|
+
colour = "primary",
|
|
72
|
+
"aria-label": ariaLabel,
|
|
73
|
+
...rest
|
|
74
|
+
},
|
|
75
|
+
ref
|
|
76
|
+
) => {
|
|
77
|
+
const theme = useTheme()
|
|
78
|
+
const category = (IconComponent as PawprintIcon).category || "core"
|
|
79
|
+
|
|
80
|
+
const iconTokens = theme.tokens.semantics.colour.icon
|
|
81
|
+
let color: string
|
|
82
|
+
if (colour === "action-default") {
|
|
83
|
+
color = iconTokens.action.default
|
|
84
|
+
} else if (colour === "action-inverse") {
|
|
85
|
+
color = iconTokens.action.inverse
|
|
86
|
+
} else {
|
|
87
|
+
color = iconTokens[colour]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sizing = theme.tokens.components.icons.sizing.icons
|
|
91
|
+
const categorySizing = sizing[category] as unknown as Record<string, string>
|
|
92
|
+
const dimension = parseTokenValue(categorySizing[size])
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<StyledIconRoot
|
|
96
|
+
ref={ref}
|
|
97
|
+
iconDimension={dimension}
|
|
98
|
+
accessibilityRole={ariaLabel ? "image" : undefined}
|
|
99
|
+
accessibilityLabel={ariaLabel}
|
|
100
|
+
accessible={!!ariaLabel}
|
|
101
|
+
{...rest}
|
|
102
|
+
>
|
|
103
|
+
<IconComponent width={dimension} height={dimension} color={color} />
|
|
104
|
+
</StyledIconRoot>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
Icon.displayName = "Icon"
|
|
110
|
+
|
|
111
|
+
export { Icon }
|
|
112
|
+
export type { IconProps, PawprintIcon, IconCategory, IconSize, IconColour }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { IconButton } from "./IconButton"
|
|
4
|
+
import type { IconButtonProps } from "./IconButton"
|
|
5
|
+
import { Typography } from "../Typography"
|
|
6
|
+
|
|
7
|
+
const PlaceholderIcon = ({
|
|
8
|
+
width,
|
|
9
|
+
height,
|
|
10
|
+
color
|
|
11
|
+
}: {
|
|
12
|
+
width?: number
|
|
13
|
+
height?: number
|
|
14
|
+
color?: string
|
|
15
|
+
}) => (
|
|
16
|
+
<View
|
|
17
|
+
style={{
|
|
18
|
+
width: width || 24,
|
|
19
|
+
height: height || 24,
|
|
20
|
+
borderRadius: 4,
|
|
21
|
+
backgroundColor: color || "#000"
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
title: "Atoms/IconButton",
|
|
28
|
+
component: IconButton,
|
|
29
|
+
argTypes: {
|
|
30
|
+
variant: {
|
|
31
|
+
control: { type: "select" },
|
|
32
|
+
options: ["filled", "outlined", "text"],
|
|
33
|
+
description: "Visual style variant"
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
control: { type: "select" },
|
|
37
|
+
options: ["sm", "md", "lg"],
|
|
38
|
+
description: "Size of the button"
|
|
39
|
+
},
|
|
40
|
+
colour: {
|
|
41
|
+
control: { type: "select" },
|
|
42
|
+
options: ["primary", "secondary", "tertiary"],
|
|
43
|
+
description: "Colour scheme"
|
|
44
|
+
},
|
|
45
|
+
loading: {
|
|
46
|
+
control: { type: "boolean" },
|
|
47
|
+
description: "Shows spinner and disables interaction"
|
|
48
|
+
},
|
|
49
|
+
disabled: {
|
|
50
|
+
control: { type: "boolean" },
|
|
51
|
+
description: "Prevents interaction"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Playground = (args: Partial<IconButtonProps>) => (
|
|
57
|
+
<IconButton
|
|
58
|
+
icon={PlaceholderIcon}
|
|
59
|
+
aria-label="Placeholder action"
|
|
60
|
+
{...args}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
Playground.args = {
|
|
64
|
+
variant: "filled",
|
|
65
|
+
size: "md",
|
|
66
|
+
colour: "primary",
|
|
67
|
+
loading: false,
|
|
68
|
+
disabled: false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const AllVariants = () => (
|
|
72
|
+
<View style={styles.column}>
|
|
73
|
+
{(["filled", "outlined", "text"] as const).map((variant) => (
|
|
74
|
+
<View key={variant} style={styles.section}>
|
|
75
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
76
|
+
{variant}
|
|
77
|
+
</Typography>
|
|
78
|
+
<View style={styles.row}>
|
|
79
|
+
{(["primary", "secondary", "tertiary"] as const).map((colour) => (
|
|
80
|
+
<IconButton
|
|
81
|
+
key={colour}
|
|
82
|
+
icon={PlaceholderIcon}
|
|
83
|
+
variant={variant}
|
|
84
|
+
colour={colour}
|
|
85
|
+
aria-label={`${variant} ${colour}`}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</View>
|
|
89
|
+
</View>
|
|
90
|
+
))}
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const AllSizes = () => (
|
|
95
|
+
<View style={styles.column}>
|
|
96
|
+
{(["filled", "outlined", "text"] as const).map((variant) => (
|
|
97
|
+
<View key={variant} style={styles.section}>
|
|
98
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
99
|
+
{variant}
|
|
100
|
+
</Typography>
|
|
101
|
+
<View style={styles.row}>
|
|
102
|
+
{(["sm", "md", "lg"] as const).map((size) => (
|
|
103
|
+
<IconButton
|
|
104
|
+
key={size}
|
|
105
|
+
icon={PlaceholderIcon}
|
|
106
|
+
variant={variant}
|
|
107
|
+
size={size}
|
|
108
|
+
aria-label={`${variant} ${size}`}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</View>
|
|
112
|
+
</View>
|
|
113
|
+
))}
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
export const AllStates = () => (
|
|
118
|
+
<View style={styles.column}>
|
|
119
|
+
{(["filled", "outlined", "text"] as const).map((variant) => (
|
|
120
|
+
<View key={variant} style={styles.section}>
|
|
121
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
122
|
+
{variant}
|
|
123
|
+
</Typography>
|
|
124
|
+
<View style={styles.row}>
|
|
125
|
+
<IconButton
|
|
126
|
+
icon={PlaceholderIcon}
|
|
127
|
+
variant={variant}
|
|
128
|
+
aria-label={`${variant} default`}
|
|
129
|
+
/>
|
|
130
|
+
<IconButton
|
|
131
|
+
icon={PlaceholderIcon}
|
|
132
|
+
variant={variant}
|
|
133
|
+
disabled
|
|
134
|
+
aria-label={`${variant} disabled`}
|
|
135
|
+
/>
|
|
136
|
+
<IconButton
|
|
137
|
+
icon={PlaceholderIcon}
|
|
138
|
+
variant={variant}
|
|
139
|
+
loading
|
|
140
|
+
aria-label={`${variant} loading`}
|
|
141
|
+
/>
|
|
142
|
+
</View>
|
|
143
|
+
</View>
|
|
144
|
+
))}
|
|
145
|
+
</View>
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
column: {
|
|
150
|
+
flexDirection: "column",
|
|
151
|
+
gap: 24
|
|
152
|
+
},
|
|
153
|
+
section: {
|
|
154
|
+
flexDirection: "column",
|
|
155
|
+
gap: 8
|
|
156
|
+
},
|
|
157
|
+
row: {
|
|
158
|
+
flexDirection: "row",
|
|
159
|
+
gap: 12,
|
|
160
|
+
alignItems: "center"
|
|
161
|
+
}
|
|
162
|
+
})
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Pressable, View, PressableProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Spinner } from "../Spinner"
|
|
6
|
+
import type { PawprintIcon } from "../Icon"
|
|
7
|
+
|
|
8
|
+
type IconButtonVariant = "filled" | "outlined" | "text"
|
|
9
|
+
type IconButtonSize = "sm" | "md" | "lg"
|
|
10
|
+
type IconButtonColour = "primary" | "secondary" | "tertiary"
|
|
11
|
+
|
|
12
|
+
type IconButtonOwnProps = {
|
|
13
|
+
icon: PawprintIcon
|
|
14
|
+
variant?: IconButtonVariant
|
|
15
|
+
size?: IconButtonSize
|
|
16
|
+
colour?: IconButtonColour
|
|
17
|
+
loading?: boolean
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
"aria-label": string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type IconButtonProps = IconButtonOwnProps &
|
|
23
|
+
Omit<PressableProps, keyof IconButtonOwnProps | "children">
|
|
24
|
+
|
|
25
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
26
|
+
|
|
27
|
+
const sizeToIconSizeToken = {
|
|
28
|
+
lg: "xl",
|
|
29
|
+
md: "lg",
|
|
30
|
+
sm: "md"
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
const StyledIconButton = styled(Pressable)<{
|
|
34
|
+
buttonDimension: number
|
|
35
|
+
buttonBorderRadius: number
|
|
36
|
+
buttonBgColor: string
|
|
37
|
+
buttonOpacity: number
|
|
38
|
+
buttonBorderWidth?: number
|
|
39
|
+
buttonBorderColor?: string
|
|
40
|
+
}>(
|
|
41
|
+
({
|
|
42
|
+
buttonDimension,
|
|
43
|
+
buttonBorderRadius,
|
|
44
|
+
buttonBgColor,
|
|
45
|
+
buttonOpacity,
|
|
46
|
+
buttonBorderWidth,
|
|
47
|
+
buttonBorderColor
|
|
48
|
+
}) => ({
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
position: "relative",
|
|
52
|
+
width: buttonDimension,
|
|
53
|
+
height: buttonDimension,
|
|
54
|
+
borderRadius: buttonBorderRadius,
|
|
55
|
+
backgroundColor: buttonBgColor,
|
|
56
|
+
opacity: buttonOpacity,
|
|
57
|
+
...(buttonBorderWidth
|
|
58
|
+
? { borderWidth: buttonBorderWidth, borderColor: buttonBorderColor }
|
|
59
|
+
: {})
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const StyledIconWrapper = styled(View)<{
|
|
64
|
+
iconDimension: number
|
|
65
|
+
iconOpacity: number
|
|
66
|
+
}>(({ iconDimension, iconOpacity }) => ({
|
|
67
|
+
width: iconDimension,
|
|
68
|
+
height: iconDimension,
|
|
69
|
+
opacity: iconOpacity
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
const StyledSpinnerWrapper = styled(View)({
|
|
73
|
+
position: "absolute",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "center"
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A circular icon-only button with token-based styling.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* import { IconButton } from "@butternutbox/pawprint-native"
|
|
84
|
+
* import { Add } from "@butternutbox/pawprint-icons/core"
|
|
85
|
+
*
|
|
86
|
+
* <IconButton icon={Add} aria-label="Add item" />
|
|
87
|
+
* <IconButton icon={Add} variant="outlined" aria-label="Add item" />
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @param icon - **(required)** Icon component
|
|
91
|
+
* @param variant - *(optional)* Visual variant: filled (default), outlined, or text.
|
|
92
|
+
* @param size - *(optional)* Size variant: sm, md (default), lg
|
|
93
|
+
* @param colour - *(optional)* Colour variant: primary (default), secondary, tertiary
|
|
94
|
+
* @param loading - *(optional)* Shows spinner and disables interaction
|
|
95
|
+
* @param aria-label - **(required)** Accessible label for the button
|
|
96
|
+
*/
|
|
97
|
+
const IconButton = React.forwardRef<View, IconButtonProps>(
|
|
98
|
+
(
|
|
99
|
+
{
|
|
100
|
+
icon: IconComponent,
|
|
101
|
+
variant = "filled",
|
|
102
|
+
size = "md",
|
|
103
|
+
colour = "primary",
|
|
104
|
+
loading = false,
|
|
105
|
+
disabled,
|
|
106
|
+
"aria-label": ariaLabel,
|
|
107
|
+
...rest
|
|
108
|
+
},
|
|
109
|
+
ref
|
|
110
|
+
) => {
|
|
111
|
+
const theme = useTheme()
|
|
112
|
+
const isDisabled = disabled || loading
|
|
113
|
+
const buttons = theme.tokens.components.buttons
|
|
114
|
+
const sizeTokens = buttons.size[size]
|
|
115
|
+
const dimension = parseTokenValue(sizeTokens.height)
|
|
116
|
+
const iconSizing = theme.tokens.components.icons.sizing.icons.core
|
|
117
|
+
const iconDimension = parseTokenValue(iconSizing[sizeToIconSizeToken[size]])
|
|
118
|
+
|
|
119
|
+
const [pressed, setPressed] = React.useState(false)
|
|
120
|
+
|
|
121
|
+
const getVariantStyles = (
|
|
122
|
+
isPressed: boolean
|
|
123
|
+
): {
|
|
124
|
+
backgroundColor: string
|
|
125
|
+
borderWidth?: number
|
|
126
|
+
borderColor?: string
|
|
127
|
+
iconColor: string
|
|
128
|
+
} => {
|
|
129
|
+
if (variant === "filled") {
|
|
130
|
+
const filled = buttons.iconButton.filledButton
|
|
131
|
+
const bgTokens = filled.colour.background[colour]
|
|
132
|
+
const iconTokens = filled.colour.icon[colour]
|
|
133
|
+
const hasStatefulIconColor =
|
|
134
|
+
"hover" in (iconTokens as unknown as Record<string, unknown>)
|
|
135
|
+
return {
|
|
136
|
+
backgroundColor: isDisabled
|
|
137
|
+
? bgTokens.disabled
|
|
138
|
+
: loading || isPressed
|
|
139
|
+
? bgTokens.selected
|
|
140
|
+
: bgTokens.default,
|
|
141
|
+
iconColor:
|
|
142
|
+
(loading || isPressed) && hasStatefulIconColor
|
|
143
|
+
? (iconTokens as unknown as Record<string, string>).selected
|
|
144
|
+
: (iconTokens as unknown as Record<string, string>).default
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (variant === "outlined") {
|
|
149
|
+
const outline = buttons.iconButton.outlineButton
|
|
150
|
+
return {
|
|
151
|
+
backgroundColor:
|
|
152
|
+
loading || isPressed
|
|
153
|
+
? outline.colour.background.selected
|
|
154
|
+
: "transparent",
|
|
155
|
+
borderWidth: parseTokenValue(outline.border.default),
|
|
156
|
+
borderColor: outline.colour.border.default,
|
|
157
|
+
iconColor:
|
|
158
|
+
loading || isPressed
|
|
159
|
+
? outline.colour.icon.selected
|
|
160
|
+
: outline.colour.icon.default
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// text variant
|
|
165
|
+
const text = buttons.iconButton.textButton
|
|
166
|
+
return {
|
|
167
|
+
backgroundColor:
|
|
168
|
+
loading || isPressed ? text.background.selected : "transparent",
|
|
169
|
+
iconColor: loading || isPressed ? text.icon.selected : text.icon.default
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const variantStyles = getVariantStyles(pressed)
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<StyledIconButton
|
|
177
|
+
ref={ref}
|
|
178
|
+
disabled={isDisabled}
|
|
179
|
+
accessibilityLabel={ariaLabel}
|
|
180
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
181
|
+
onPressIn={() => setPressed(true)}
|
|
182
|
+
onPressOut={() => setPressed(false)}
|
|
183
|
+
buttonDimension={dimension}
|
|
184
|
+
buttonBorderRadius={parseTokenValue(buttons.borderRadius.default)}
|
|
185
|
+
buttonBgColor={variantStyles.backgroundColor}
|
|
186
|
+
buttonOpacity={
|
|
187
|
+
isDisabled && !loading ? parseFloat(buttons.opacity.disabled) : 1
|
|
188
|
+
}
|
|
189
|
+
buttonBorderWidth={variantStyles.borderWidth}
|
|
190
|
+
buttonBorderColor={variantStyles.borderColor}
|
|
191
|
+
{...rest}
|
|
192
|
+
>
|
|
193
|
+
<StyledIconWrapper
|
|
194
|
+
iconDimension={iconDimension}
|
|
195
|
+
iconOpacity={loading ? 0 : 1}
|
|
196
|
+
>
|
|
197
|
+
<IconComponent
|
|
198
|
+
width={iconDimension}
|
|
199
|
+
height={iconDimension}
|
|
200
|
+
color={variantStyles.iconColor}
|
|
201
|
+
/>
|
|
202
|
+
</StyledIconWrapper>
|
|
203
|
+
{loading && (
|
|
204
|
+
<StyledSpinnerWrapper>
|
|
205
|
+
<Spinner
|
|
206
|
+
size={size}
|
|
207
|
+
style={{
|
|
208
|
+
borderColor: "transparent",
|
|
209
|
+
borderTopColor: "currentColor"
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
</StyledSpinnerWrapper>
|
|
213
|
+
)}
|
|
214
|
+
</StyledIconButton>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
IconButton.displayName = "IconButton"
|
|
220
|
+
|
|
221
|
+
export { IconButton }
|
|
222
|
+
export type {
|
|
223
|
+
IconButtonProps,
|
|
224
|
+
IconButtonVariant,
|
|
225
|
+
IconButtonSize,
|
|
226
|
+
IconButtonColour
|
|
227
|
+
}
|