@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,30 @@
1
+ CLI Building entry: src/index.ts
2
+ CLI Using tsconfig: tsconfig.json
3
+ CLI tsup v8.5.0
4
+ CLI Using tsup config: /home/runner/work/pawprint/pawprint/packages/pawprint-native/tsup.config.ts
5
+ CLI Target: es6
6
+ CLI Cleaning output folder
7
+ CJS Build start
8
+ ESM Build start
9
+ DTS Build start
10
+ CJS dist/ida-narrow-500-normal-C6I2PK4T.woff2 47.41 KB
11
+ CJS dist/ida-narrow-700-normal-UPHPRIN6.woff2 49.90 KB
12
+ CJS dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 19.33 KB
13
+ CJS dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 19.48 KB
14
+ CJS dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 19.35 KB
15
+ CJS dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 18.90 KB
16
+ CJS dist/index.cjs 120.33 KB
17
+ CJS dist/index.cjs.map 266.09 KB
18
+ CJS ⚡️ Build success in 2528ms
19
+ ESM dist/ida-narrow-500-normal-C6I2PK4T.woff2 47.41 KB
20
+ ESM dist/ida-narrow-700-normal-UPHPRIN6.woff2 49.90 KB
21
+ ESM dist/ibm-plex-sans-condensed-400-normal-I2XLJNNB.woff2 19.33 KB
22
+ ESM dist/ibm-plex-sans-condensed-500-normal-IEQBNVGX.woff2 19.48 KB
23
+ ESM dist/ibm-plex-sans-condensed-600-normal-UX5ZU5T6.woff2 19.35 KB
24
+ ESM dist/ibm-plex-sans-condensed-700-normal-4PFYFTSO.woff2 18.90 KB
25
+ ESM dist/index.js 113.16 KB
26
+ ESM dist/index.js.map 265.43 KB
27
+ ESM ⚡️ Build success in 2529ms
28
+ DTS ⚡️ Build success in 12741ms
29
+ DTS dist/index.d.cts 31.31 KB
30
+ DTS dist/index.d.ts 31.31 KB
@@ -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
+ ```