@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,119 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, TextInput, TextInputProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
|
|
6
|
+
export type InputState = "default" | "error" | "success"
|
|
7
|
+
|
|
8
|
+
type InputFieldOwnProps = {
|
|
9
|
+
leadingIcon?: React.ReactNode
|
|
10
|
+
trailingIcon?: React.ReactNode
|
|
11
|
+
state?: InputState
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type InputFieldProps = InputFieldOwnProps &
|
|
15
|
+
Omit<TextInputProps, keyof InputFieldOwnProps>
|
|
16
|
+
|
|
17
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
18
|
+
|
|
19
|
+
const StyledInputWrapper = styled(View)<{
|
|
20
|
+
wrapperGap: number
|
|
21
|
+
wrapperMinWidth: number
|
|
22
|
+
wrapperHeight: number
|
|
23
|
+
wrapperPaddingVertical: number
|
|
24
|
+
wrapperPaddingHorizontal: number
|
|
25
|
+
wrapperBgColor: string
|
|
26
|
+
wrapperBorderWidth: number
|
|
27
|
+
wrapperBorderColor: string
|
|
28
|
+
wrapperBorderRadius: number
|
|
29
|
+
}>(
|
|
30
|
+
({
|
|
31
|
+
wrapperGap,
|
|
32
|
+
wrapperMinWidth,
|
|
33
|
+
wrapperHeight,
|
|
34
|
+
wrapperPaddingVertical,
|
|
35
|
+
wrapperPaddingHorizontal,
|
|
36
|
+
wrapperBgColor,
|
|
37
|
+
wrapperBorderWidth,
|
|
38
|
+
wrapperBorderColor,
|
|
39
|
+
wrapperBorderRadius
|
|
40
|
+
}) => ({
|
|
41
|
+
flexDirection: "row",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
gap: wrapperGap,
|
|
44
|
+
minWidth: wrapperMinWidth,
|
|
45
|
+
width: "100%",
|
|
46
|
+
height: wrapperHeight,
|
|
47
|
+
paddingVertical: wrapperPaddingVertical,
|
|
48
|
+
paddingHorizontal: wrapperPaddingHorizontal,
|
|
49
|
+
backgroundColor: wrapperBgColor,
|
|
50
|
+
borderWidth: wrapperBorderWidth,
|
|
51
|
+
borderColor: wrapperBorderColor,
|
|
52
|
+
borderRadius: wrapperBorderRadius
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const StyledInput = styled(TextInput)<{
|
|
57
|
+
inputColor: string
|
|
58
|
+
}>(({ inputColor }) => ({
|
|
59
|
+
flex: 1,
|
|
60
|
+
minWidth: 0,
|
|
61
|
+
minHeight: 0,
|
|
62
|
+
padding: 0,
|
|
63
|
+
color: inputColor
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const StyledIconSlot = styled(View)({
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
flexShrink: 0
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* InputField component - The input field wrapper with icons and visual states.
|
|
74
|
+
*
|
|
75
|
+
* @param {React.ReactNode} [leadingIcon] - Icon to display before the input
|
|
76
|
+
* @param {React.ReactNode} [trailingIcon] - Icon to display after the input
|
|
77
|
+
* @param {InputState} [state] - Visual state of the input (default, error, success)
|
|
78
|
+
*/
|
|
79
|
+
export const InputField = React.forwardRef<TextInput, InputFieldProps>(
|
|
80
|
+
({ leadingIcon, trailingIcon, state = "default", style, ...rest }, ref) => {
|
|
81
|
+
const theme = useTheme()
|
|
82
|
+
const { spacing, colour, borderWidth, borderRadius, size } =
|
|
83
|
+
theme.tokens.components.inputs
|
|
84
|
+
|
|
85
|
+
const stateBorderColorMap = {
|
|
86
|
+
error: colour.field.border.error,
|
|
87
|
+
success: colour.field.border.success,
|
|
88
|
+
default: colour.field.border.default
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<StyledInputWrapper
|
|
93
|
+
wrapperGap={parseTokenValue(spacing.field.gap)}
|
|
94
|
+
wrapperMinWidth={parseTokenValue(size.field.minWidth)}
|
|
95
|
+
wrapperHeight={parseTokenValue(size.field.height)}
|
|
96
|
+
wrapperPaddingVertical={parseTokenValue(spacing.field.verticalPadding)}
|
|
97
|
+
wrapperPaddingHorizontal={parseTokenValue(
|
|
98
|
+
spacing.field.horizontalPadding
|
|
99
|
+
)}
|
|
100
|
+
wrapperBgColor={colour.field.background.default}
|
|
101
|
+
wrapperBorderWidth={parseTokenValue(borderWidth.field.default)}
|
|
102
|
+
wrapperBorderColor={stateBorderColorMap[state]}
|
|
103
|
+
wrapperBorderRadius={parseTokenValue(borderRadius.field.default)}
|
|
104
|
+
>
|
|
105
|
+
{leadingIcon && <StyledIconSlot>{leadingIcon}</StyledIconSlot>}
|
|
106
|
+
<StyledInput
|
|
107
|
+
ref={ref}
|
|
108
|
+
inputColor={colour.field.text.default}
|
|
109
|
+
style={style}
|
|
110
|
+
placeholderTextColor={colour.field.text.placeholder}
|
|
111
|
+
{...rest}
|
|
112
|
+
/>
|
|
113
|
+
{trailingIcon && <StyledIconSlot>{trailingIcon}</StyledIconSlot>}
|
|
114
|
+
</StyledInputWrapper>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
InputField.displayName = "Input.Field"
|
|
@@ -0,0 +1,61 @@
|
|
|
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 type { InputState } from "./InputField"
|
|
7
|
+
|
|
8
|
+
type InputLabelOwnProps = {
|
|
9
|
+
optionalText?: string
|
|
10
|
+
state?: InputState
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type InputLabelProps = InputLabelOwnProps &
|
|
15
|
+
Omit<ViewProps, keyof InputLabelOwnProps>
|
|
16
|
+
|
|
17
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
18
|
+
|
|
19
|
+
const StyledLabelRow = styled(View)<{
|
|
20
|
+
labelGap: number
|
|
21
|
+
}>(({ labelGap }) => ({
|
|
22
|
+
flexDirection: "row",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
gap: labelGap
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Label component for Input fields.
|
|
29
|
+
*
|
|
30
|
+
* @param optionalText - Optional text to display next to label (e.g., "(optional)")
|
|
31
|
+
* @param state - Visual state of the input (affects label color for error state)
|
|
32
|
+
* @param children - Label text content
|
|
33
|
+
*/
|
|
34
|
+
export const InputLabel = React.forwardRef<View, InputLabelProps>(
|
|
35
|
+
({ optionalText, state = "default", children, ...rest }, ref) => {
|
|
36
|
+
const theme = useTheme()
|
|
37
|
+
const { colour, text, spacing } = theme.tokens.components.inputs
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<StyledLabelRow
|
|
41
|
+
ref={ref}
|
|
42
|
+
labelGap={parseTokenValue(spacing.label.gap)}
|
|
43
|
+
{...rest}
|
|
44
|
+
>
|
|
45
|
+
<Typography
|
|
46
|
+
token={text.label}
|
|
47
|
+
color={state === "error" ? colour.label.error : colour.label.default}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</Typography>
|
|
51
|
+
{optionalText && (
|
|
52
|
+
<Typography token={text.optional} color={colour.label.optional}>
|
|
53
|
+
{optionalText}
|
|
54
|
+
</Typography>
|
|
55
|
+
)}
|
|
56
|
+
</StyledLabelRow>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
InputLabel.displayName = "Input.Label"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Input } from "./Input"
|
|
2
|
+
export type { InputProps } from "./Input"
|
|
3
|
+
export { InputLabel } from "./InputLabel"
|
|
4
|
+
export type { InputLabelProps } from "./InputLabel"
|
|
5
|
+
export { InputField } from "./InputField"
|
|
6
|
+
export type { InputFieldProps } from "./InputField"
|
|
7
|
+
export { InputDescription } from "./InputDescription"
|
|
8
|
+
export type { InputDescriptionProps } from "./InputDescription"
|
|
9
|
+
export { InputError } from "./InputError"
|
|
10
|
+
export type { InputErrorProps } from "./InputError"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { Link } from "./Link"
|
|
4
|
+
import type { LinkProps } from "./Link"
|
|
5
|
+
import { Typography } from "../Typography"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "Atoms/Link",
|
|
9
|
+
component: Link,
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: {
|
|
12
|
+
control: { type: "select" },
|
|
13
|
+
options: ["sm", "md", "lg"],
|
|
14
|
+
description: "Size variant (standalone only)"
|
|
15
|
+
},
|
|
16
|
+
weight: {
|
|
17
|
+
control: { type: "select" },
|
|
18
|
+
options: ["medium", "semiBold", "bold"],
|
|
19
|
+
description: "Font weight"
|
|
20
|
+
},
|
|
21
|
+
standalone: {
|
|
22
|
+
control: { type: "boolean" },
|
|
23
|
+
description: "Shows trailing arrow indicator"
|
|
24
|
+
},
|
|
25
|
+
disabled: {
|
|
26
|
+
control: { type: "boolean" },
|
|
27
|
+
description: "Prevents interaction"
|
|
28
|
+
},
|
|
29
|
+
href: {
|
|
30
|
+
control: { type: "text" },
|
|
31
|
+
description: "URL to open when pressed"
|
|
32
|
+
},
|
|
33
|
+
children: {
|
|
34
|
+
control: { type: "text" },
|
|
35
|
+
description: "Link text"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Playground = (args: LinkProps) => <Link {...args} />
|
|
41
|
+
Playground.args = {
|
|
42
|
+
children: "Find out more",
|
|
43
|
+
size: "md",
|
|
44
|
+
weight: "semiBold",
|
|
45
|
+
standalone: true,
|
|
46
|
+
disabled: false,
|
|
47
|
+
href: "https://example.com"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const StandaloneAllSizes = () => (
|
|
51
|
+
<View style={styles.column}>
|
|
52
|
+
{(["sm", "md", "lg"] as const).map((size) => (
|
|
53
|
+
<View key={size} style={styles.section}>
|
|
54
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
55
|
+
{size}
|
|
56
|
+
</Typography>
|
|
57
|
+
<Link size={size} standalone href="https://example.com">
|
|
58
|
+
Find out more
|
|
59
|
+
</Link>
|
|
60
|
+
</View>
|
|
61
|
+
))}
|
|
62
|
+
</View>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export const AllStates = () => (
|
|
66
|
+
<View style={styles.column}>
|
|
67
|
+
<View style={styles.section}>
|
|
68
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
69
|
+
Standalone enabled
|
|
70
|
+
</Typography>
|
|
71
|
+
<Link standalone href="https://example.com">
|
|
72
|
+
Find out more
|
|
73
|
+
</Link>
|
|
74
|
+
</View>
|
|
75
|
+
<View style={styles.section}>
|
|
76
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
77
|
+
Standalone disabled
|
|
78
|
+
</Typography>
|
|
79
|
+
<Link standalone disabled href="https://example.com">
|
|
80
|
+
Find out more
|
|
81
|
+
</Link>
|
|
82
|
+
</View>
|
|
83
|
+
<View style={styles.section}>
|
|
84
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
85
|
+
Inline enabled
|
|
86
|
+
</Typography>
|
|
87
|
+
<View style={styles.row}>
|
|
88
|
+
<Typography size="md">Read our </Typography>
|
|
89
|
+
<Link href="https://example.com">terms and conditions</Link>
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
<View style={styles.section}>
|
|
93
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
94
|
+
Inline disabled
|
|
95
|
+
</Typography>
|
|
96
|
+
<View style={styles.row}>
|
|
97
|
+
<Typography size="md">Read our </Typography>
|
|
98
|
+
<Link disabled href="https://example.com">
|
|
99
|
+
terms and conditions
|
|
100
|
+
</Link>
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
column: {
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
gap: 24
|
|
110
|
+
},
|
|
111
|
+
section: {
|
|
112
|
+
flexDirection: "column",
|
|
113
|
+
gap: 8
|
|
114
|
+
},
|
|
115
|
+
row: {
|
|
116
|
+
flexDirection: "row",
|
|
117
|
+
alignItems: "center"
|
|
118
|
+
}
|
|
119
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
View,
|
|
5
|
+
PressableProps,
|
|
6
|
+
Linking,
|
|
7
|
+
ViewStyle
|
|
8
|
+
} from "react-native"
|
|
9
|
+
import { useTheme } from "@emotion/react"
|
|
10
|
+
import { Typography } from "../Typography"
|
|
11
|
+
|
|
12
|
+
export type LinkSize = "sm" | "md" | "lg"
|
|
13
|
+
export type LinkWeight = "medium" | "semiBold" | "bold"
|
|
14
|
+
|
|
15
|
+
type LinkOwnProps = {
|
|
16
|
+
weight?: LinkWeight
|
|
17
|
+
standalone?: boolean
|
|
18
|
+
size?: LinkSize
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
href?: string
|
|
21
|
+
children?: React.ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LinkProps = LinkOwnProps &
|
|
25
|
+
Omit<PressableProps, keyof LinkOwnProps | "children">
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Link component for navigational elements.
|
|
29
|
+
*
|
|
30
|
+
* - Inline (default) — underlined, inherits font size from surrounding context.
|
|
31
|
+
* - Standalone — includes a trailing arrow indicator.
|
|
32
|
+
*
|
|
33
|
+
* @param {"sm" | "md" | "lg"} [size="md"] - Size of standalone link text.
|
|
34
|
+
* @param {"medium" | "semiBold" | "bold"} [weight="semiBold"] - Font weight.
|
|
35
|
+
* @param {boolean} [standalone=false] - Renders with a trailing arrow.
|
|
36
|
+
* @param {boolean} [disabled=false] - Disables the link.
|
|
37
|
+
* @param {string} [href] - URL to open when pressed.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* <Link href="https://example.com" standalone>Find out more</Link>
|
|
41
|
+
*/
|
|
42
|
+
export const Link = React.forwardRef<View, LinkProps>(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
size = "md",
|
|
46
|
+
weight = "semiBold",
|
|
47
|
+
standalone = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
href,
|
|
50
|
+
children,
|
|
51
|
+
onPress,
|
|
52
|
+
style,
|
|
53
|
+
...rest
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => {
|
|
57
|
+
const theme = useTheme()
|
|
58
|
+
const { typography, colour } = theme.tokens.semantics
|
|
59
|
+
const linkColour = colour.text.link
|
|
60
|
+
|
|
61
|
+
const handlePress = (
|
|
62
|
+
e: Parameters<NonNullable<PressableProps["onPress"]>>[0]
|
|
63
|
+
) => {
|
|
64
|
+
if (onPress) {
|
|
65
|
+
onPress(e)
|
|
66
|
+
} else if (href) {
|
|
67
|
+
Linking.openURL(href)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Pressable
|
|
73
|
+
ref={ref}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
onPress={handlePress}
|
|
76
|
+
accessibilityRole="link"
|
|
77
|
+
accessibilityState={{ disabled }}
|
|
78
|
+
style={({ pressed }) => {
|
|
79
|
+
const baseStyle: ViewStyle = {
|
|
80
|
+
...(standalone && {
|
|
81
|
+
flexDirection: "row",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: 4
|
|
84
|
+
}),
|
|
85
|
+
opacity: disabled ? 0.5 : 1
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
baseStyle,
|
|
89
|
+
typeof style === "function" ? style({ pressed }) : style
|
|
90
|
+
]
|
|
91
|
+
}}
|
|
92
|
+
{...rest}
|
|
93
|
+
>
|
|
94
|
+
{({ pressed }) => (
|
|
95
|
+
<>
|
|
96
|
+
<Typography
|
|
97
|
+
token={typography.link[weight][size]}
|
|
98
|
+
color={pressed ? linkColour.hover : linkColour.default}
|
|
99
|
+
textDecoration={standalone ? "none" : "underline"}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</Typography>
|
|
103
|
+
{standalone && (
|
|
104
|
+
<Typography
|
|
105
|
+
token={typography.link[weight][size]}
|
|
106
|
+
color={pressed ? linkColour.hover : linkColour.default}
|
|
107
|
+
>
|
|
108
|
+
{" →"}
|
|
109
|
+
</Typography>
|
|
110
|
+
)}
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
</Pressable>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
Link.displayName = "Link"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { LogoBrand, LogoVariant } from "./Logo"
|
|
3
|
+
|
|
4
|
+
type LogoSvgComponent = React.ComponentType<{
|
|
5
|
+
width?: number
|
|
6
|
+
height?: number
|
|
7
|
+
}>
|
|
8
|
+
|
|
9
|
+
// Logo registry - consumers should register their logo components here
|
|
10
|
+
// or provide them via a registration function
|
|
11
|
+
const LOGOS: Record<
|
|
12
|
+
LogoBrand,
|
|
13
|
+
Partial<Record<LogoVariant, LogoSvgComponent>>
|
|
14
|
+
> = {
|
|
15
|
+
butternut: {},
|
|
16
|
+
psibufet: {},
|
|
17
|
+
bcorp: {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveLogo(
|
|
21
|
+
brand: LogoBrand,
|
|
22
|
+
variant: LogoVariant
|
|
23
|
+
): LogoSvgComponent | undefined {
|
|
24
|
+
return LOGOS[brand]?.[variant]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registerLogo(
|
|
28
|
+
brand: LogoBrand,
|
|
29
|
+
variant: LogoVariant,
|
|
30
|
+
component: LogoSvgComponent
|
|
31
|
+
): void {
|
|
32
|
+
if (!LOGOS[brand]) {
|
|
33
|
+
LOGOS[brand] = {}
|
|
34
|
+
}
|
|
35
|
+
LOGOS[brand][variant] = component
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { resolveLogo, registerLogo, LOGOS }
|
|
39
|
+
export type { LogoSvgComponent }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { resolveLogo } from "./Logo.registry"
|
|
5
|
+
|
|
6
|
+
type LogoBrand = "butternut" | "psibufet" | "bcorp"
|
|
7
|
+
type LogoVariant =
|
|
8
|
+
| "wordmark"
|
|
9
|
+
| "primary"
|
|
10
|
+
| "tabbed-top"
|
|
11
|
+
| "tabbed-bottom"
|
|
12
|
+
| "favicon"
|
|
13
|
+
|
|
14
|
+
type LogoOwnProps = {
|
|
15
|
+
brand: LogoBrand
|
|
16
|
+
variant?: LogoVariant
|
|
17
|
+
"aria-label"?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type LogoProps = LogoOwnProps & Omit<ViewProps, keyof LogoOwnProps>
|
|
21
|
+
|
|
22
|
+
const StyledRoot = styled(View)({
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
justifyContent: "center",
|
|
25
|
+
flexShrink: 0
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Renders a brand logo SVG.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { Logo } from "@butternutbox/pawprint-native"
|
|
34
|
+
*
|
|
35
|
+
* <Logo brand="butternut" variant="primary" aria-label="ButternutBox" />
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @param brand - **(required)** Brand: "butternut", "psibufet", or "bcorp"
|
|
39
|
+
* @param variant - *(optional)* Logo variant (default: "primary"). Ignored for "bcorp".
|
|
40
|
+
* @param aria-label - *(optional)* Accessible label
|
|
41
|
+
*/
|
|
42
|
+
const Logo = React.forwardRef<View, LogoProps>(
|
|
43
|
+
({ brand, variant = "primary", "aria-label": ariaLabel, ...rest }, ref) => {
|
|
44
|
+
const effectiveVariant = brand === "bcorp" ? "primary" : variant
|
|
45
|
+
const LogoComponent = resolveLogo(brand, effectiveVariant)
|
|
46
|
+
|
|
47
|
+
if (!LogoComponent) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<StyledRoot
|
|
53
|
+
ref={ref}
|
|
54
|
+
accessibilityRole={ariaLabel ? "image" : undefined}
|
|
55
|
+
accessibilityLabel={ariaLabel}
|
|
56
|
+
accessible={!!ariaLabel}
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
59
|
+
<LogoComponent />
|
|
60
|
+
</StyledRoot>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Logo.displayName = "Logo"
|
|
66
|
+
|
|
67
|
+
export { Logo }
|
|
68
|
+
export type { LogoProps, LogoBrand, LogoVariant }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet, Text } from "react-native"
|
|
3
|
+
import { Spinner } from "./Spinner"
|
|
4
|
+
import type { SpinnerProps } from "./Spinner"
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: "Atoms/Spinner",
|
|
8
|
+
component: Spinner,
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: {
|
|
11
|
+
control: { type: "select" },
|
|
12
|
+
options: ["sm", "md", "lg"],
|
|
13
|
+
description: "Size of the spinner"
|
|
14
|
+
},
|
|
15
|
+
variant: {
|
|
16
|
+
control: { type: "select" },
|
|
17
|
+
options: ["dark", "light"],
|
|
18
|
+
description: "Colour variant (dark for light backgrounds, light for dark)"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Playground = (args: SpinnerProps) => <Spinner {...args} />
|
|
24
|
+
Playground.args = {
|
|
25
|
+
size: "md",
|
|
26
|
+
variant: "dark"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spinnerSizes = ["sm", "md", "lg"] as const
|
|
30
|
+
|
|
31
|
+
export const AllSizes = () => (
|
|
32
|
+
<View style={styles.column}>
|
|
33
|
+
<View style={styles.section}>
|
|
34
|
+
<Text style={styles.label}>Dark variant</Text>
|
|
35
|
+
<View style={styles.row}>
|
|
36
|
+
{spinnerSizes.map((size) => (
|
|
37
|
+
<View key={size} style={styles.item}>
|
|
38
|
+
<Spinner size={size} variant="dark" />
|
|
39
|
+
<Text style={styles.sizeLabel}>{size}</Text>
|
|
40
|
+
</View>
|
|
41
|
+
))}
|
|
42
|
+
</View>
|
|
43
|
+
</View>
|
|
44
|
+
|
|
45
|
+
<View style={styles.section}>
|
|
46
|
+
<Text style={[styles.label, styles.lightText]}>Light variant</Text>
|
|
47
|
+
<View style={styles.row}>
|
|
48
|
+
{spinnerSizes.map((size) => (
|
|
49
|
+
<View key={size} style={styles.darkItem}>
|
|
50
|
+
<Spinner size={size} variant="light" />
|
|
51
|
+
<Text style={[styles.sizeLabel, styles.lightText]}>{size}</Text>
|
|
52
|
+
</View>
|
|
53
|
+
))}
|
|
54
|
+
</View>
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
column: {
|
|
61
|
+
flexDirection: "column",
|
|
62
|
+
gap: 32
|
|
63
|
+
},
|
|
64
|
+
section: {
|
|
65
|
+
flexDirection: "column",
|
|
66
|
+
gap: 12
|
|
67
|
+
},
|
|
68
|
+
row: {
|
|
69
|
+
flexDirection: "row",
|
|
70
|
+
gap: 24,
|
|
71
|
+
alignItems: "center"
|
|
72
|
+
},
|
|
73
|
+
item: {
|
|
74
|
+
flexDirection: "column",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
gap: 8
|
|
77
|
+
},
|
|
78
|
+
darkItem: {
|
|
79
|
+
flexDirection: "column",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
gap: 8,
|
|
82
|
+
backgroundColor: "#1a1a1a",
|
|
83
|
+
padding: 16,
|
|
84
|
+
borderRadius: 8
|
|
85
|
+
},
|
|
86
|
+
label: {
|
|
87
|
+
fontSize: 13,
|
|
88
|
+
fontWeight: "600",
|
|
89
|
+
color: "#666"
|
|
90
|
+
},
|
|
91
|
+
sizeLabel: {
|
|
92
|
+
fontSize: 12,
|
|
93
|
+
color: "#666"
|
|
94
|
+
},
|
|
95
|
+
lightText: {
|
|
96
|
+
color: "#ffffff"
|
|
97
|
+
}
|
|
98
|
+
})
|