@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,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,2 @@
1
+ export { Link } from "./Link"
2
+ export type { LinkProps, LinkSize, LinkWeight } from "./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,4 @@
1
+ export { Logo } from "./Logo"
2
+ export type { LogoProps, LogoBrand, LogoVariant } from "./Logo"
3
+ export { resolveLogo, registerLogo } from "./Logo.registry"
4
+ export type { LogoSvgComponent } from "./Logo.registry"
@@ -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
+ })