@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,30 @@
|
|
|
1
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
2
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
3
|
+
[34mCLI[39m tsup v8.5.0
|
|
4
|
+
[34mCLI[39m Using tsup config: /home/runner/work/pawprint/pawprint/packages/pawprint-native/tsup.config.ts
|
|
5
|
+
[34mCLI[39m Target: es6
|
|
6
|
+
[34mCLI[39m Cleaning output folder
|
|
7
|
+
[34mCJS[39m Build start
|
|
8
|
+
[34mESM[39m Build start
|
|
9
|
+
[34mDTS[39m Build start
|
|
10
|
+
[32mCJS[39m [1mdist/ida-narrow-500-normal-C6I2PK4T.woff2 [22m[32m47.41 KB[39m
|
|
11
|
+
[32mCJS[39m [1mdist/ida-narrow-700-normal-UPHPRIN6.woff2 [22m[32m49.90 KB[39m
|
|
12
|
+
[32mCJS[39m [1mdist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 [22m[32m19.33 KB[39m
|
|
13
|
+
[32mCJS[39m [1mdist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 [22m[32m19.48 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 [22m[32m19.35 KB[39m
|
|
15
|
+
[32mCJS[39m [1mdist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 [22m[32m18.90 KB[39m
|
|
16
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m120.33 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m266.09 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 2528ms
|
|
19
|
+
[32mESM[39m [1mdist/ida-narrow-500-normal-C6I2PK4T.woff2 [22m[32m47.41 KB[39m
|
|
20
|
+
[32mESM[39m [1mdist/ida-narrow-700-normal-UPHPRIN6.woff2 [22m[32m49.90 KB[39m
|
|
21
|
+
[32mESM[39m [1mdist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 [22m[32m19.33 KB[39m
|
|
22
|
+
[32mESM[39m [1mdist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 [22m[32m19.48 KB[39m
|
|
23
|
+
[32mESM[39m [1mdist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 [22m[32m19.35 KB[39m
|
|
24
|
+
[32mESM[39m [1mdist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 [22m[32m18.90 KB[39m
|
|
25
|
+
[32mESM[39m [1mdist/index.js [22m[32m113.16 KB[39m
|
|
26
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m265.43 KB[39m
|
|
27
|
+
[32mESM[39m ⚡️ Build success in 2529ms
|
|
28
|
+
[32mDTS[39m ⚡️ Build success in 12741ms
|
|
29
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m31.31 KB[39m
|
|
30
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m31.31 KB[39m
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# Component Development Guidelines (React Native)
|
|
2
|
+
|
|
3
|
+
> **Quick Start:** Type `Create a new component following llms.txt`
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
### **1. Component Structure Template**
|
|
8
|
+
|
|
9
|
+
Use `@emotion/native` `styled()` for styling and `@rn-primitives/*` for accessible foundations where available.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import React from "react"
|
|
13
|
+
import { View, ViewProps, Pressable, PressableProps } from "react-native"
|
|
14
|
+
import styled from "@emotion/native"
|
|
15
|
+
import { useTheme } from "@emotion/react"
|
|
16
|
+
|
|
17
|
+
// Define prop types based on Figma design
|
|
18
|
+
type [Component]OwnProps = {
|
|
19
|
+
variant?: "default" | "alt"
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
children?: React.ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type [Component]Props = [Component]OwnProps &
|
|
25
|
+
Omit<ViewProps, keyof [Component]OwnProps>
|
|
26
|
+
|
|
27
|
+
// Helper for parsing string token values to numbers
|
|
28
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
29
|
+
|
|
30
|
+
// Styled component — Emotion native forwards all props by default,
|
|
31
|
+
// so prefix internal-only props (e.g. badgeVariant) to avoid clashes.
|
|
32
|
+
const Styled[Component] = styled(View)<{
|
|
33
|
+
componentVariant: string
|
|
34
|
+
}>(({ theme, componentVariant }) => {
|
|
35
|
+
const { [componentName] } = theme.tokens.components
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
// Use design tokens for all values
|
|
39
|
+
padding: parseTokenValue([componentName].spacing.padding),
|
|
40
|
+
backgroundColor: [componentName].colour.background[componentVariant]
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Component with forwardRef
|
|
45
|
+
export const [Component] = React.forwardRef<View, [Component]Props>(
|
|
46
|
+
({ variant = "default", disabled = false, children, ...rest }, ref) => {
|
|
47
|
+
const theme = useTheme()
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Styled[Component]
|
|
51
|
+
ref={ref}
|
|
52
|
+
componentVariant={variant}
|
|
53
|
+
accessible
|
|
54
|
+
accessibilityRole="button"
|
|
55
|
+
{...rest}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Styled[Component]>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
[Component].displayName = "[Component]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### **2. When to Use RN Primitives**
|
|
67
|
+
|
|
68
|
+
**Use @rn-primitives when available:**
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// ✅ GOOD: Use rn-primitives for accessible foundation
|
|
72
|
+
import * as CheckboxPrimitive from "@rn-primitives/checkbox"
|
|
73
|
+
import * as SwitchPrimitive from "@rn-primitives/switch"
|
|
74
|
+
import * as SliderPrimitive from "@rn-primitives/slider"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Build custom when rn-primitives doesn't have the component:**
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// ✅ GOOD: Custom component with Pressable for interactivity
|
|
81
|
+
import { Pressable, PressableProps } from "react-native"
|
|
82
|
+
import styled from "@emotion/native"
|
|
83
|
+
|
|
84
|
+
const StyledButton = styled(Pressable)<{ buttonVariant: string }>(({
|
|
85
|
+
theme,
|
|
86
|
+
buttonVariant
|
|
87
|
+
}) => {
|
|
88
|
+
const { button } = theme.tokens.components.buttons
|
|
89
|
+
// Token-driven styles
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const Button = React.forwardRef<View, ButtonProps>((props, ref) => {
|
|
93
|
+
return <StyledButton ref={ref} {...props} />
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**When building custom components:**
|
|
98
|
+
|
|
99
|
+
- Use `Pressable` for interactive elements (not `TouchableOpacity`)
|
|
100
|
+
- Add `accessible`, `accessibilityRole`, and `accessibilityLabel` props
|
|
101
|
+
- Use `accessibilityState` for disabled, checked, selected states
|
|
102
|
+
- Test with VoiceOver (iOS) and TalkBack (Android)
|
|
103
|
+
|
|
104
|
+
### **3. Design Token Usage**
|
|
105
|
+
|
|
106
|
+
**Token Access Pattern:**
|
|
107
|
+
|
|
108
|
+
Tokens are string values — use `parseTokenValue()` to convert to numbers for React Native styles.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
112
|
+
|
|
113
|
+
// Inside a styled component:
|
|
114
|
+
const Styled = styled(View)(({ theme }) => {
|
|
115
|
+
const { badge } = theme.tokens.components.badges
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
height: parseTokenValue(badge.sizing.medium.height),
|
|
119
|
+
paddingHorizontal: parseTokenValue(badge.spacing.medium.horizontalPadding),
|
|
120
|
+
borderRadius: parseTokenValue(badge.primary.borderRadius.default),
|
|
121
|
+
backgroundColor: badge.colour.background.default
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Inside a component body (via useTheme):
|
|
126
|
+
const theme = useTheme()
|
|
127
|
+
const color = theme.tokens.semantics.colour.text.primary
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Token Fallback Strategy:**
|
|
131
|
+
|
|
132
|
+
1. **First choice:** Component tokens — `theme.tokens.components.[componentName]`
|
|
133
|
+
2. **Second choice:** Semantic tokens — `theme.tokens.semantics.[category]`
|
|
134
|
+
3. **Last resort:** Primitive tokens — `theme.tokens.primitives.[category]`
|
|
135
|
+
4. **Temporary only:** Hardcoded values (document with TODO comment)
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// ✅ PREFERRED: Component tokens
|
|
139
|
+
const { button } = theme.tokens.components.buttons
|
|
140
|
+
backgroundColor: button.colour.background.primary
|
|
141
|
+
|
|
142
|
+
// ✅ GOOD: Semantic tokens when component tokens don't exist
|
|
143
|
+
backgroundColor: theme.tokens.semantics.colour.interactive.primary.default
|
|
144
|
+
|
|
145
|
+
// ⚠️ ACCEPTABLE: Primitive tokens as fallback
|
|
146
|
+
backgroundColor: theme.tokens.primitives.colour.brand.primary[500]
|
|
147
|
+
|
|
148
|
+
// ❌ TEMPORARY ONLY: Hardcoded values (add TODO)
|
|
149
|
+
backgroundColor: "#6200EA" // TODO: Replace with component token when available
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### **4. Accessibility Requirements**
|
|
153
|
+
|
|
154
|
+
**Essential a11y features:**
|
|
155
|
+
|
|
156
|
+
- ✅ Use `React.forwardRef` for ref forwarding
|
|
157
|
+
- ✅ Set `accessible={true}` on interactive elements
|
|
158
|
+
- ✅ Use `accessibilityRole` — `"button"`, `"checkbox"`, `"image"`, etc.
|
|
159
|
+
- ✅ Use `accessibilityLabel` for icon-only or image components
|
|
160
|
+
- ✅ Use `accessibilityState={{ disabled, checked, selected }}` for stateful components
|
|
161
|
+
- ✅ Handle `disabled` prop — prevent press handlers and apply disabled tokens
|
|
162
|
+
- ✅ Dev-mode `console.warn` when `aria-label` is missing on icon-only components
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// ✅ GOOD: Proper accessibility on a toggle
|
|
166
|
+
<Pressable
|
|
167
|
+
accessible
|
|
168
|
+
accessibilityRole="switch"
|
|
169
|
+
accessibilityLabel={label}
|
|
170
|
+
accessibilityState={{ checked: isOn, disabled }}
|
|
171
|
+
onPress={disabled ? undefined : toggle}
|
|
172
|
+
>
|
|
173
|
+
{/* content */}
|
|
174
|
+
</Pressable>
|
|
175
|
+
|
|
176
|
+
// ✅ GOOD: Decorative vs meaningful images
|
|
177
|
+
// Meaningful:
|
|
178
|
+
<StyledRoot accessibilityRole="image" accessibilityLabel={ariaLabel}>
|
|
179
|
+
<IconComponent />
|
|
180
|
+
</StyledRoot>
|
|
181
|
+
|
|
182
|
+
// Decorative (omit label):
|
|
183
|
+
<StyledRoot accessible={false}>
|
|
184
|
+
<IconComponent />
|
|
185
|
+
</StyledRoot>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### **5. Interactive States**
|
|
189
|
+
|
|
190
|
+
React Native does not have CSS pseudo-classes. Handle states in the component body or via `Pressable`'s style callback:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// Approach 1: Pressable style callback
|
|
194
|
+
<Pressable
|
|
195
|
+
disabled={disabled}
|
|
196
|
+
style={({ pressed }) => [
|
|
197
|
+
styles.base,
|
|
198
|
+
pressed && styles.pressed,
|
|
199
|
+
disabled && styles.disabled
|
|
200
|
+
]}
|
|
201
|
+
>
|
|
202
|
+
|
|
203
|
+
// Approach 2: Styled component with state props
|
|
204
|
+
const StyledButton = styled(Pressable)<{
|
|
205
|
+
isPressed: boolean
|
|
206
|
+
isDisabled: boolean
|
|
207
|
+
}>(({ theme, isPressed, isDisabled }) => ({
|
|
208
|
+
backgroundColor: isDisabled
|
|
209
|
+
? tokens.colour.disabled
|
|
210
|
+
: isPressed
|
|
211
|
+
? tokens.colour.pressed
|
|
212
|
+
: tokens.colour.default,
|
|
213
|
+
opacity: isDisabled ? 0.5 : 1
|
|
214
|
+
}))
|
|
215
|
+
|
|
216
|
+
// Approach 3: useTheme + inline style for dynamic values
|
|
217
|
+
const Button = ({ disabled, ...rest }) => {
|
|
218
|
+
const theme = useTheme()
|
|
219
|
+
const tokens = theme.tokens.components.buttons.button
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<Pressable
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
style={({ pressed }) => ({
|
|
225
|
+
backgroundColor: disabled
|
|
226
|
+
? tokens.colour.background.disabled
|
|
227
|
+
: pressed
|
|
228
|
+
? tokens.colour.background.pressed
|
|
229
|
+
: tokens.colour.background.default
|
|
230
|
+
})}
|
|
231
|
+
{...rest}
|
|
232
|
+
/>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### **6. Storybook Stories**
|
|
238
|
+
|
|
239
|
+
**File location:** `[ComponentName].stories.tsx` (co-located with component)
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import React from "react"
|
|
243
|
+
import { View, StyleSheet } from "react-native"
|
|
244
|
+
import { [Component] } from "./[Component]"
|
|
245
|
+
import type { [Component]Props } from "./[Component]"
|
|
246
|
+
|
|
247
|
+
export default {
|
|
248
|
+
title: "Atoms/[Component]", // or Molecules/ based on type
|
|
249
|
+
component: [Component],
|
|
250
|
+
argTypes: {
|
|
251
|
+
variant: {
|
|
252
|
+
control: { type: "select" },
|
|
253
|
+
options: ["default", "alt"],
|
|
254
|
+
description: "Visual style variant"
|
|
255
|
+
},
|
|
256
|
+
disabled: {
|
|
257
|
+
control: { type: "boolean" },
|
|
258
|
+
description: "Prevents interaction"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Interactive playground with controls
|
|
264
|
+
export const Playground = (args: [Component]Props) => <[Component] {...args} />
|
|
265
|
+
Playground.args = {
|
|
266
|
+
variant: "default",
|
|
267
|
+
disabled: false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Showcase all variants
|
|
271
|
+
export const AllVariants = () => (
|
|
272
|
+
<View style={styles.column}>
|
|
273
|
+
{variants.map((variant) => (
|
|
274
|
+
<[Component] key={variant} variant={variant}>
|
|
275
|
+
{variant}
|
|
276
|
+
</[Component]>
|
|
277
|
+
))}
|
|
278
|
+
</View>
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const styles = StyleSheet.create({
|
|
282
|
+
column: { flexDirection: "column", gap: 16 }
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Story essentials:**
|
|
287
|
+
|
|
288
|
+
- `Playground` — interactive story with args for every controllable prop
|
|
289
|
+
- `AllVariants` — visual catalogue of every variant/size combination
|
|
290
|
+
- `argTypes` — every prop must have a `description` field for the controls panel
|
|
291
|
+
- Use `StyleSheet.create` for story layout styles (not inline objects)
|
|
292
|
+
|
|
293
|
+
### **7. File Structure**
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
packages/pawprint-native/src/components/atoms/[ComponentName]/
|
|
297
|
+
├── [ComponentName].tsx # Main component
|
|
298
|
+
├── [ComponentName].stories.tsx # Storybook stories (co-located)
|
|
299
|
+
└── index.ts # Exports
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**index.ts:**
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
export { [Component] } from "./[Component]"
|
|
306
|
+
export type { [Component]Props } from "./[Component]"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Update barrel exports through the chain:**
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
atoms/[ComponentName]/index.ts → atoms/index.ts → components/index.ts → src/index.ts
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Advanced Best Practices
|
|
318
|
+
|
|
319
|
+
### **8. Styling: @emotion/native vs StyleSheet**
|
|
320
|
+
|
|
321
|
+
**Use `@emotion/native` `styled()` for token-driven component styles:**
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// ✅ GOOD: Styled component with theme access
|
|
325
|
+
const StyledBadge = styled(View)<{ badgeVariant: string }>(
|
|
326
|
+
({ theme, badgeVariant }) => ({
|
|
327
|
+
backgroundColor:
|
|
328
|
+
theme.tokens.components.badges.badge.colour.background[badgeVariant],
|
|
329
|
+
borderRadius: parseTokenValue(
|
|
330
|
+
theme.tokens.components.badges.badge.primary.borderRadius.default
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Use `StyleSheet.create` for static layout styles in stories and wrappers:**
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
// ✅ GOOD: Static layout styles
|
|
340
|
+
const styles = StyleSheet.create({
|
|
341
|
+
row: { flexDirection: "row", gap: 12, alignItems: "center" },
|
|
342
|
+
column: { flexDirection: "column", gap: 24 }
|
|
343
|
+
})
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Avoid inline style objects on hot paths** — they create new references on every render.
|
|
347
|
+
|
|
348
|
+
### **9. Prop Naming for Styled Components**
|
|
349
|
+
|
|
350
|
+
Emotion native forwards all props to the underlying RN component. To prevent clashes with native props, prefix internal styling props:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
// ✅ GOOD: Prefixed prop names avoid clashing with native View/Pressable props
|
|
354
|
+
const StyledBadge = styled(View)<{
|
|
355
|
+
badgeVariant: BadgeVariant // not "variant" (could clash)
|
|
356
|
+
badgeSize: BadgeSize // not "size" (clashes with Image)
|
|
357
|
+
pinned: boolean // safe — not a native prop
|
|
358
|
+
}>((props) => ({ /* ... */ }))
|
|
359
|
+
|
|
360
|
+
// Then in the component:
|
|
361
|
+
<StyledBadge badgeVariant={variant} badgeSize={size} pinned={pinned} />
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### **10. Controlled & Uncontrolled Components**
|
|
365
|
+
|
|
366
|
+
Many native components support both modes. Use a discriminated union or simple optional props:
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
type SwitchProps = {
|
|
370
|
+
checked?: boolean // controlled
|
|
371
|
+
defaultChecked?: boolean // uncontrolled (default: false)
|
|
372
|
+
onCheckedChange?: (checked: boolean) => void // callback
|
|
373
|
+
disabled?: boolean
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const Switch = React.forwardRef<View, SwitchProps>(
|
|
377
|
+
({ checked, defaultChecked = false, onCheckedChange, ...rest }, ref) => {
|
|
378
|
+
// Internal state for uncontrolled mode
|
|
379
|
+
const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
|
|
380
|
+
const isControlled = checked !== undefined
|
|
381
|
+
const isChecked = isControlled ? checked : internalChecked
|
|
382
|
+
|
|
383
|
+
const handleChange = (value: boolean) => {
|
|
384
|
+
if (!isControlled) setInternalChecked(value)
|
|
385
|
+
onCheckedChange?.(value)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return /* ... */
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### **11. Compound Components Pattern**
|
|
394
|
+
|
|
395
|
+
Use for complex components with multiple related sub-components:
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
// Input compound component
|
|
399
|
+
const InputRoot = styled(View)(({ theme }) => ({ /* ... */ }))
|
|
400
|
+
const InputLabel = ({ children }) => <Typography>{children}</Typography>
|
|
401
|
+
const InputField = React.forwardRef<TextInput, InputFieldProps>((props, ref) => /* ... */)
|
|
402
|
+
const InputDescription = ({ children }) => <Typography>{children}</Typography>
|
|
403
|
+
const InputError = ({ children }) => <Typography>{children}</Typography>
|
|
404
|
+
|
|
405
|
+
// Attach sub-components
|
|
406
|
+
const Input = Object.assign(
|
|
407
|
+
React.forwardRef<TextInput, InputProps>((props, ref) => /* simple API */),
|
|
408
|
+
{
|
|
409
|
+
Root: InputRoot,
|
|
410
|
+
Label: InputLabel,
|
|
411
|
+
Field: InputField,
|
|
412
|
+
Description: InputDescription,
|
|
413
|
+
Error: InputError
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
// Usage — simple API:
|
|
418
|
+
<Input label="Email" placeholder="you@example.com" />
|
|
419
|
+
|
|
420
|
+
// Usage — compound API:
|
|
421
|
+
<Input.Root>
|
|
422
|
+
<Input.Label>Email</Input.Label>
|
|
423
|
+
<Input.Field placeholder="you@example.com" />
|
|
424
|
+
<Input.Description>We'll never share this</Input.Description>
|
|
425
|
+
</Input.Root>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### **12. Typography via the Typography Component**
|
|
429
|
+
|
|
430
|
+
Never use raw `<Text>` in components — always use the `Typography` component for token-driven text:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
// ✅ GOOD: Token-driven text
|
|
434
|
+
<Typography variant="body" size="md" color="primary">
|
|
435
|
+
Hello world
|
|
436
|
+
</Typography>
|
|
437
|
+
|
|
438
|
+
// ✅ GOOD: Component-owned text via token mode
|
|
439
|
+
<Typography token={tokens.typography.medium.default} color={tokens.colour.text.primary}>
|
|
440
|
+
Badge label
|
|
441
|
+
</Typography>
|
|
442
|
+
|
|
443
|
+
// ❌ BAD: Raw Text with manual styles
|
|
444
|
+
<Text style={{ fontSize: 14, fontFamily: "IBMPlexSansCondensed-Medium" }}>
|
|
445
|
+
Hello world
|
|
446
|
+
</Text>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### **13. Icons & Illustrations**
|
|
450
|
+
|
|
451
|
+
Icons and illustrations come from shared packages with platform-specific builds:
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
// Icons — used via the Icon wrapper component
|
|
455
|
+
import { Icon } from "../Icon"
|
|
456
|
+
import type { PawprintIcon } from "../Icon"
|
|
457
|
+
|
|
458
|
+
type MyComponentProps = {
|
|
459
|
+
icon?: PawprintIcon // component from @butternutbox/pawprint-icons
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Inside JSX:
|
|
463
|
+
{
|
|
464
|
+
icon && (
|
|
465
|
+
<Icon
|
|
466
|
+
icon={icon}
|
|
467
|
+
size={iconSizeMap[size]}
|
|
468
|
+
colour={iconColourMap[variant]}
|
|
469
|
+
/>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Illustrations — used via the Illustration wrapper
|
|
474
|
+
import { Illustration } from "../Illustration"
|
|
475
|
+
import type { PawprintIllustration } from "../Illustration"
|
|
476
|
+
|
|
477
|
+
{
|
|
478
|
+
illustration && <Illustration illustration={illustration} size="sm" />
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Key differences from web:**
|
|
483
|
+
|
|
484
|
+
- Icons use `react-native-svg` under the hood (auto-resolved via package exports)
|
|
485
|
+
- Import paths are the same: `@butternutbox/pawprint-icons/core` — Metro resolves the `.native.js` build automatically
|
|
486
|
+
- No `fill="currentColor"` — native icons receive `color` as a prop
|
|
487
|
+
|
|
488
|
+
### **14. Performance Considerations**
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
// ✅ GOOD: Stable style references
|
|
492
|
+
const StyledButton = styled(Pressable)(({ theme }) => ({
|
|
493
|
+
// Computed once per theme change
|
|
494
|
+
}))
|
|
495
|
+
|
|
496
|
+
// ❌ BAD: Inline style objects on every render
|
|
497
|
+
<View style={{ padding: parseTokenValue(tokens.spacing.md) }} />
|
|
498
|
+
|
|
499
|
+
// ✅ GOOD: Memoize expensive lookups
|
|
500
|
+
const styles = useMemo(() => ({
|
|
501
|
+
container: { backgroundColor: tokens.colour.background.default }
|
|
502
|
+
}), [tokens])
|
|
503
|
+
|
|
504
|
+
// ✅ GOOD: Use Animated API for animations, not state re-renders
|
|
505
|
+
const opacity = useRef(new Animated.Value(0)).current
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**Performance Tips:**
|
|
509
|
+
|
|
510
|
+
- Use `StyleSheet.create` for static styles
|
|
511
|
+
- Use `styled()` for theme-dependent styles
|
|
512
|
+
- Avoid anonymous functions in `style` props on lists
|
|
513
|
+
- Use `React.memo` sparingly — only for expensive renders with stable props
|
|
514
|
+
- Use `Animated` or `Reanimated` for animations, not `setState`
|
|
515
|
+
|
|
516
|
+
### **15. Error Handling & Validation**
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
export const Icon = React.forwardRef<View, IconProps>(
|
|
520
|
+
({ icon: IconComponent, "aria-label": ariaLabel, ...rest }, ref) => {
|
|
521
|
+
// Dev-mode accessibility warning
|
|
522
|
+
if (process.env.NODE_ENV === "development" && !ariaLabel) {
|
|
523
|
+
console.warn(
|
|
524
|
+
"Icon: Consider providing an aria-label for meaningful icons. " +
|
|
525
|
+
"Omit intentionally for decorative icons only."
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<StyledRoot
|
|
531
|
+
ref={ref}
|
|
532
|
+
accessibilityRole={ariaLabel ? "image" : undefined}
|
|
533
|
+
accessibilityLabel={ariaLabel}
|
|
534
|
+
accessible={!!ariaLabel}
|
|
535
|
+
{...rest}
|
|
536
|
+
>
|
|
537
|
+
<IconComponent width={dimension} height={dimension} color={color} />
|
|
538
|
+
</StyledRoot>
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### **16. Parity with pawprint-web**
|
|
545
|
+
|
|
546
|
+
Native components should mirror the web API where possible:
|
|
547
|
+
|
|
548
|
+
- **Same prop names** — `variant`, `size`, `colour`, `disabled`, `loading`, `icon`
|
|
549
|
+
- **Same variants** — `"filled" | "outlined" | "text"` for Button on both platforms
|
|
550
|
+
- **Same tokens** — read from the same `theme.tokens.components` structure
|
|
551
|
+
- **Same sizes** — `"sm" | "md" | "lg"` etc.
|
|
552
|
+
|
|
553
|
+
**Document platform differences** when the APIs must diverge:
|
|
554
|
+
|
|
555
|
+
- Web uses `string | number` for positioning (`top`, `bottom`) — native uses `number` only
|
|
556
|
+
- Web has CSS pseudo-classes (`:hover`, `:focus-visible`) — native uses `Pressable` callbacks
|
|
557
|
+
- Web uses `styled()` from `@mui/system` — native uses `styled()` from `@emotion/native`
|
|
558
|
+
- Web has `shouldForwardProp` — native relies on prop name prefixing
|
|
559
|
+
|
|
560
|
+
### **17. Documentation Standards**
|
|
561
|
+
|
|
562
|
+
**Add JSDoc comments for all exported components:**
|
|
563
|
+
|
|
564
|
+
````tsx
|
|
565
|
+
/**
|
|
566
|
+
* Brief description of what the component does.
|
|
567
|
+
*
|
|
568
|
+
* @param {"variant1" | "variant2"} [variant="variant1"] - Visual style variant.
|
|
569
|
+
* @param {"sm" | "md" | "lg"} [size="md"] - Size of the component.
|
|
570
|
+
* @param {boolean} [disabled=false] - Prevents interaction.
|
|
571
|
+
* @param {React.ReactNode} children - Component content.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```tsx
|
|
575
|
+
* import { Component } from "@butternutbox/pawprint-native"
|
|
576
|
+
*
|
|
577
|
+
* <Component variant="default" size="md">Content</Component>
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
export const Component = React.forwardRef<View, ComponentProps>(
|
|
581
|
+
(props, ref) => {
|
|
582
|
+
// Implementation
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
````
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Anti-Patterns to Avoid
|
|
590
|
+
|
|
591
|
+
❌ **Don't use raw `<Text>`** — always use `Typography` for token-driven text
|
|
592
|
+
❌ **Don't hardcode colours or sizes** — always use design tokens
|
|
593
|
+
❌ **Don't skip `forwardRef`** — breaks ref forwarding
|
|
594
|
+
❌ **Don't use `TouchableOpacity`** — use `Pressable` instead
|
|
595
|
+
❌ **Don't forget `displayName`** — helps with debugging
|
|
596
|
+
❌ **Don't use inline style objects in loops** — creates new references per render
|
|
597
|
+
❌ **Don't skip `accessibilityRole`** — required for screen readers
|
|
598
|
+
❌ **Don't skip TypeScript types** — maintain type safety
|
|
599
|
+
❌ **Don't use `StyleSheet.create` for theme-dependent styles** — use `styled()` instead
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Quick Reference: Architecture Decision Tree
|
|
604
|
+
|
|
605
|
+
- **Same behaviour + different styles** → Use variants (e.g., Button with filled/outlined/text)
|
|
606
|
+
- **Different behaviour** → Create separate components (e.g., Button vs IconButton)
|
|
607
|
+
- **Multiple related sub-components** → Use compound pattern (e.g., Input.Root, Input.Field)
|
|
608
|
+
- **Accessible primitive exists** → Use `@rn-primitives/*` as foundation
|
|
609
|
+
- **No primitive available** → Build custom with `Pressable` + accessibility props
|
|
610
|
+
- **Controlled + uncontrolled** → Support both with internal state fallback
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @butternutbox/pawprint-native
|
|
2
|
+
|
|
3
|
+
ButternutBox Pawprint Design System - React Native Components
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @butternutbox/pawprint-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Theme Provider
|
|
14
|
+
|
|
15
|
+
Wrap your app with the `PawprintProvider`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { PawprintProvider } from "@butternutbox/pawprint-native"
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return <PawprintProvider>{/* Your app */}</PawprintProvider>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Using Tokens
|
|
26
|
+
|
|
27
|
+
Access design tokens via the `usePawprint` hook:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { usePawprint } from "@butternutbox/pawprint-native"
|
|
31
|
+
|
|
32
|
+
function MyComponent() {
|
|
33
|
+
const tokens = usePawprint()
|
|
34
|
+
|
|
35
|
+
// Access tokens
|
|
36
|
+
const buttonColor =
|
|
37
|
+
tokens.components.buttons.filledButton.colour.background.primary.default
|
|
38
|
+
|
|
39
|
+
return <View />
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Styled Components
|
|
44
|
+
|
|
45
|
+
Use the `styled` utility for creating styled React Native components:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { styled } from "@butternutbox/pawprint-native"
|
|
49
|
+
import { View } from "react-native"
|
|
50
|
+
|
|
51
|
+
const StyledView = styled(View)(({ theme }) => ({
|
|
52
|
+
backgroundColor:
|
|
53
|
+
theme.components.buttons.filledButton.colour.background.primary.default,
|
|
54
|
+
padding: 16
|
|
55
|
+
}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Build the package
|
|
62
|
+
yarn build
|
|
63
|
+
|
|
64
|
+
# Watch mode
|
|
65
|
+
yarn dev
|
|
66
|
+
|
|
67
|
+
# Type check
|
|
68
|
+
yarn type:check
|
|
69
|
+
|
|
70
|
+
# Lint
|
|
71
|
+
yarn lint:check
|
|
72
|
+
```
|