@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,303 @@
1
+ import React, { useCallback, useRef } from "react"
2
+ import { View, ViewProps, PanResponder, LayoutChangeEvent } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import * as SliderPrimitive from "@rn-primitives/slider"
6
+ import { Typography } from "../../atoms/Typography"
7
+
8
+ type SliderOwnProps = {
9
+ value?: number
10
+ defaultValue?: number
11
+ onValueChange?: (value: number) => void
12
+ onValueCommitted?: (value: number) => void
13
+ min?: number
14
+ max?: number
15
+ step?: number
16
+ disabled?: boolean
17
+ leadingLabel?: string
18
+ trailingLabel?: string
19
+ leadingIcon?: React.ReactNode
20
+ trailingIcon?: React.ReactNode
21
+ description?: string
22
+ }
23
+
24
+ export type SliderProps = SliderOwnProps & Omit<ViewProps, keyof SliderOwnProps>
25
+
26
+ const parseTokenValue = (value: string): number => parseFloat(value)
27
+
28
+ const clamp = (val: number, min: number, max: number): number =>
29
+ Math.min(Math.max(val, min), max)
30
+
31
+ const snap = (val: number, step: number, min: number): number => {
32
+ const steps = Math.round((val - min) / step)
33
+ return min + steps * step
34
+ }
35
+
36
+ const StyledSliderRoot = styled(SliderPrimitive.Root)<{
37
+ sliderGap: number
38
+ sliderMinWidth: number
39
+ sliderOpacity: number
40
+ }>(({ sliderGap, sliderMinWidth, sliderOpacity }) => ({
41
+ gap: sliderGap,
42
+ minWidth: sliderMinWidth,
43
+ opacity: sliderOpacity
44
+ }))
45
+
46
+ const StyledLabelRow = styled(View)<{
47
+ labelRowGap: number
48
+ }>(({ labelRowGap }) => ({
49
+ flexDirection: "row",
50
+ alignItems: "center",
51
+ gap: labelRowGap
52
+ }))
53
+
54
+ const StyledLabelSlot = styled(View)<{
55
+ labelSlotGap: number
56
+ }>(({ labelSlotGap }) => ({
57
+ flexDirection: "row",
58
+ alignItems: "center",
59
+ gap: labelSlotGap,
60
+ flexShrink: 0
61
+ }))
62
+
63
+ const StyledTrackArea = styled(View)<{
64
+ trackAreaHeight: number
65
+ }>(({ trackAreaHeight }) => ({
66
+ flex: 1,
67
+ height: trackAreaHeight,
68
+ justifyContent: "center"
69
+ }))
70
+
71
+ const StyledTrack = styled(SliderPrimitive.Track)<{
72
+ trackHeight: number
73
+ trackBorderRadius: number
74
+ trackBgColor: string
75
+ trackBorderWidth: number
76
+ trackBorderColor: string
77
+ }>(
78
+ ({
79
+ trackHeight,
80
+ trackBorderRadius,
81
+ trackBgColor,
82
+ trackBorderWidth,
83
+ trackBorderColor
84
+ }) => ({
85
+ height: trackHeight,
86
+ borderRadius: trackBorderRadius,
87
+ backgroundColor: trackBgColor,
88
+ borderWidth: trackBorderWidth,
89
+ borderColor: trackBorderColor,
90
+ overflow: "hidden"
91
+ })
92
+ )
93
+
94
+ const StyledRange = styled(SliderPrimitive.Range)<{
95
+ rangeBgColor: string
96
+ rangeBorderRadius: number
97
+ }>(({ rangeBgColor, rangeBorderRadius }) => ({
98
+ height: "100%",
99
+ backgroundColor: rangeBgColor,
100
+ borderRadius: rangeBorderRadius
101
+ }))
102
+
103
+ const StyledThumb = styled(SliderPrimitive.Thumb)<{
104
+ thumbSize: number
105
+ thumbBorderRadius: number
106
+ thumbBgColor: string
107
+ }>(({ thumbSize, thumbBorderRadius, thumbBgColor }) => ({
108
+ position: "absolute",
109
+ width: thumbSize,
110
+ height: thumbSize,
111
+ borderRadius: thumbBorderRadius,
112
+ backgroundColor: thumbBgColor,
113
+ alignItems: "center",
114
+ justifyContent: "center"
115
+ }))
116
+
117
+ /**
118
+ * Slider component for selecting a numeric value within a min/max range.
119
+ *
120
+ * @param {number} [value] - Controlled value.
121
+ * @param {number} [defaultValue] - Uncontrolled initial value.
122
+ * @param {(value: number) => void} [onValueChange] - Callback fired on every value change.
123
+ * @param {(value: number) => void} [onValueCommitted] - Callback fired when the user finishes dragging.
124
+ * @param {number} [min=0] - Minimum value.
125
+ * @param {number} [max=100] - Maximum value.
126
+ * @param {number} [step=1] - Step increment.
127
+ * @param {boolean} [disabled=false] - Prevents interaction.
128
+ * @param {string} [leadingLabel] - Label to the left of the track.
129
+ * @param {string} [trailingLabel] - Label to the right of the track.
130
+ * @param {React.ReactNode} [leadingIcon] - Icon before the leading label.
131
+ * @param {React.ReactNode} [trailingIcon] - Icon after the trailing label.
132
+ * @param {string} [description] - Descriptive text below the control.
133
+ *
134
+ * @example
135
+ * <Slider defaultValue={25} leadingLabel="0kg" trailingLabel="50kg+" />
136
+ */
137
+ export const Slider = React.forwardRef<View, SliderProps>(
138
+ (
139
+ {
140
+ value: controlledValue,
141
+ defaultValue = 0,
142
+ onValueChange,
143
+ onValueCommitted,
144
+ min = 0,
145
+ max = 100,
146
+ step = 1,
147
+ disabled = false,
148
+ leadingLabel,
149
+ trailingLabel,
150
+ leadingIcon,
151
+ trailingIcon,
152
+ description,
153
+ ...rest
154
+ },
155
+ ref
156
+ ) => {
157
+ const theme = useTheme()
158
+ const { slider, buttons } = theme.tokens.components
159
+ const { dimensions } = theme.tokens.semantics
160
+
161
+ const isControlled = controlledValue !== undefined
162
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
163
+ const currentValue = isControlled ? controlledValue : internalValue
164
+
165
+ const trackWidth = useRef(0)
166
+ const thumbSize = parseTokenValue(buttons.size.md.height)
167
+ const trackHeight = parseTokenValue(slider.sizing.track.height)
168
+
169
+ const fraction = max > min ? (currentValue - min) / (max - min) : 0
170
+
171
+ const handleLayout = (e: LayoutChangeEvent) => {
172
+ trackWidth.current = e.nativeEvent.layout.width
173
+ }
174
+
175
+ const updateValue = useCallback(
176
+ (locationX: number) => {
177
+ if (disabled || trackWidth.current === 0) return
178
+ const ratio = clamp(locationX / trackWidth.current, 0, 1)
179
+ const raw = min + ratio * (max - min)
180
+ const snapped = snap(clamp(raw, min, max), step, min)
181
+ if (!isControlled) {
182
+ setInternalValue(snapped)
183
+ }
184
+ onValueChange?.(snapped)
185
+ },
186
+ [disabled, min, max, step, isControlled, onValueChange]
187
+ )
188
+
189
+ const panResponder = useRef(
190
+ PanResponder.create({
191
+ onStartShouldSetPanResponder: () => !disabled,
192
+ onMoveShouldSetPanResponder: () => !disabled,
193
+ onPanResponderGrant: (e) => {
194
+ updateValue(e.nativeEvent.locationX)
195
+ },
196
+ onPanResponderMove: (e) => {
197
+ updateValue(e.nativeEvent.locationX)
198
+ },
199
+ onPanResponderRelease: () => {
200
+ onValueCommitted?.(currentValue)
201
+ }
202
+ })
203
+ ).current
204
+
205
+ const hasLeading = leadingIcon !== undefined || leadingLabel !== undefined
206
+ const hasTrailing =
207
+ trailingIcon !== undefined || trailingLabel !== undefined
208
+
209
+ const labelGap = parseTokenValue(slider.sliderField.spacing.label.gap)
210
+ const roundBorderRadius = parseTokenValue(dimensions.borderRadius.round)
211
+
212
+ return (
213
+ <StyledSliderRoot
214
+ ref={ref}
215
+ value={currentValue}
216
+ min={min}
217
+ max={max}
218
+ disabled={disabled}
219
+ sliderGap={parseTokenValue(slider.sliderField.spacing.gap)}
220
+ sliderMinWidth={parseTokenValue(slider.sizing.minWidth)}
221
+ sliderOpacity={disabled ? parseFloat(buttons.opacity.disabled) : 1}
222
+ {...rest}
223
+ >
224
+ <StyledLabelRow labelRowGap={labelGap}>
225
+ {hasLeading && (
226
+ <StyledLabelSlot labelSlotGap={labelGap}>
227
+ {leadingIcon}
228
+ {leadingLabel && (
229
+ <Typography
230
+ token={slider.sliderField.typography.label}
231
+ color={slider.sliderField.colour.text.label}
232
+ >
233
+ {leadingLabel}
234
+ </Typography>
235
+ )}
236
+ </StyledLabelSlot>
237
+ )}
238
+
239
+ <StyledTrackArea
240
+ trackAreaHeight={thumbSize}
241
+ onLayout={handleLayout}
242
+ {...panResponder.panHandlers}
243
+ >
244
+ <StyledTrack
245
+ trackHeight={trackHeight}
246
+ trackBorderRadius={roundBorderRadius}
247
+ trackBgColor={slider.colour.track.background.default}
248
+ trackBorderWidth={parseTokenValue(dimensions.borderWidth.sm)}
249
+ trackBorderColor={slider.colour.track.border.default}
250
+ >
251
+ <StyledRange
252
+ rangeBgColor={slider.colour.indicator.background.default}
253
+ rangeBorderRadius={roundBorderRadius}
254
+ style={{ width: `${fraction * 100}%` }}
255
+ />
256
+ </StyledTrack>
257
+ <StyledThumb
258
+ thumbSize={thumbSize}
259
+ style={{
260
+ left: `${fraction * 100}%`,
261
+ marginLeft: -thumbSize / 2
262
+ }}
263
+ thumbBorderRadius={parseTokenValue(buttons.borderRadius.default)}
264
+ thumbBgColor={
265
+ buttons.iconButton.filledButton.colour.background.primary
266
+ .default
267
+ }
268
+ >
269
+ <Typography size="xs" color="alt">
270
+ ⟨⟩
271
+ </Typography>
272
+ </StyledThumb>
273
+ </StyledTrackArea>
274
+
275
+ {hasTrailing && (
276
+ <StyledLabelSlot labelSlotGap={labelGap}>
277
+ {trailingLabel && (
278
+ <Typography
279
+ token={slider.sliderField.typography.label}
280
+ color={slider.sliderField.colour.text.label}
281
+ >
282
+ {trailingLabel}
283
+ </Typography>
284
+ )}
285
+ {trailingIcon}
286
+ </StyledLabelSlot>
287
+ )}
288
+ </StyledLabelRow>
289
+
290
+ {description && (
291
+ <Typography
292
+ token={slider.sliderField.typography.description}
293
+ color={slider.sliderField.colour.text.description}
294
+ >
295
+ {description}
296
+ </Typography>
297
+ )}
298
+ </StyledSliderRoot>
299
+ )
300
+ }
301
+ )
302
+
303
+ Slider.displayName = "Slider"
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./Slider"
2
+ export type { SliderProps } from "./Slider"
@@ -0,0 +1,6 @@
1
+ export * from "./ButtonDock"
2
+ export * from "./ButtonGroup"
3
+ export * from "./Checkbox"
4
+ export * from "./Radio"
5
+ export * from "./SegmentedControl"
6
+ export * from "./Slider"
@@ -0,0 +1,49 @@
1
+ import { Brand, BrandName } from "@butternutbox/pawprint-tokens"
2
+ import type { FontSource } from "expo-font"
3
+
4
+ import idaNarrow500 from "./ida-narrow-500-normal.woff2"
5
+ import idaNarrow700 from "./ida-narrow-700-normal.woff2"
6
+ import ibmPlexSansCondensed400 from "./ibm-plex-sans-condensed-400-normal.woff2"
7
+ import ibmPlexSansCondensed500 from "./ibm-plex-sans-condensed-500-normal.woff2"
8
+ import ibmPlexSansCondensed600 from "./ibm-plex-sans-condensed-600-normal.woff2"
9
+ import ibmPlexSansCondensed700 from "./ibm-plex-sans-condensed-700-normal.woff2"
10
+
11
+ // Keys passed to useFonts — each key becomes the exact @font-face family name
12
+ // that expo-font registers on web, and the postscript font name on iOS/Android.
13
+ export const BRAND_FONTS: Partial<
14
+ Record<BrandName, Record<string, FontSource>>
15
+ > = {
16
+ [Brand.Butternutbox]: {
17
+ "IdaNarrow-Medium": idaNarrow500 as FontSource,
18
+ "IdaNarrow-Bold": idaNarrow700 as FontSource,
19
+ "IBMPlexSansCondensed-Regular": ibmPlexSansCondensed400 as FontSource,
20
+ "IBMPlexSansCondensed-Medium": ibmPlexSansCondensed500 as FontSource,
21
+ "IBMPlexSansCondensed-SemiBold": ibmPlexSansCondensed600 as FontSource,
22
+ "IBMPlexSansCondensed-Bold": ibmPlexSansCondensed700 as FontSource
23
+ }
24
+ }
25
+
26
+ // Maps token fontFamily + fontWeight → the expo-font registered key.
27
+ // The key is used as fontFamily on both web and native — useFonts registers
28
+ // it as the @font-face family name on web so it matches Text styles directly.
29
+ // fontWeight is dropped because the weight is already encoded in the key name.
30
+ const FONT_MAP: Record<string, Record<string, string>> = {
31
+ "IBM Plex Sans Condensed": {
32
+ "400": "IBMPlexSansCondensed-Regular",
33
+ "500": "IBMPlexSansCondensed-Medium",
34
+ "600": "IBMPlexSansCondensed-SemiBold",
35
+ "700": "IBMPlexSansCondensed-Bold"
36
+ },
37
+ "Ida Narrow": {
38
+ "500": "IdaNarrow-Medium",
39
+ "700": "IdaNarrow-Bold"
40
+ }
41
+ }
42
+
43
+ export const resolveFont = (
44
+ fontFamily: string,
45
+ fontWeight: string
46
+ ): { fontFamily: string } => {
47
+ const resolved = FONT_MAP[fontFamily]?.[fontWeight]
48
+ return { fontFamily: resolved ?? fontFamily }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import "./types/theme"
2
+
3
+ export { tokens } from "@butternutbox/pawprint-tokens"
4
+
5
+ export { default as styled } from "@emotion/native"
6
+
7
+ export * from "./fonts"
8
+ export * from "./theme"
9
+ export * from "./components"
@@ -0,0 +1,26 @@
1
+ import React from "react"
2
+ import { ThemeProvider } from "./ThemeProvider"
3
+ import {
4
+ Brand,
5
+ Theme,
6
+ BrandName,
7
+ ThemeName
8
+ } from "@butternutbox/pawprint-tokens"
9
+
10
+ export interface PawprintProviderProps {
11
+ brand?: BrandName
12
+ initialTheme?: ThemeName
13
+ children: React.ReactNode
14
+ }
15
+
16
+ export const PawprintProvider: React.FC<PawprintProviderProps> = ({
17
+ brand = Brand.Butternutbox,
18
+ initialTheme = Theme.Default,
19
+ children
20
+ }) => {
21
+ return (
22
+ <ThemeProvider brand={brand} initialTheme={initialTheme}>
23
+ {children}
24
+ </ThemeProvider>
25
+ )
26
+ }
@@ -0,0 +1,63 @@
1
+ import React, { useState, createContext, useContext, useMemo } from "react"
2
+ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"
3
+ import type { Theme as EmotionTheme } from "@emotion/react"
4
+ import {
5
+ Brand,
6
+ Theme,
7
+ BrandName,
8
+ ThemeName
9
+ } from "@butternutbox/pawprint-tokens"
10
+ import { createPawprintTheme } from "./utils"
11
+
12
+ type ThemeContextType = {
13
+ brand: BrandName
14
+ theme: ThemeName
15
+ setTheme: (theme: ThemeName) => void
16
+ emotionTheme: EmotionTheme
17
+ }
18
+
19
+ type Props = {
20
+ brand?: BrandName
21
+ initialTheme?: ThemeName
22
+ children: React.ReactNode
23
+ }
24
+
25
+ const ThemeContext = createContext<ThemeContextType>({
26
+ brand: Brand.Butternutbox,
27
+ theme: Theme.Default,
28
+ setTheme: (_theme: ThemeName) => {},
29
+ emotionTheme: createPawprintTheme(Brand.Butternutbox, Theme.Default)
30
+ })
31
+
32
+ export const ThemeProvider = ({
33
+ children,
34
+ brand = Brand.Butternutbox,
35
+ initialTheme = Theme.Default
36
+ }: Props): React.JSX.Element => {
37
+ const [theme, setTheme] = useState<ThemeName>(initialTheme)
38
+
39
+ const emotionTheme = useMemo(
40
+ () => createPawprintTheme(brand, theme),
41
+ [brand, theme]
42
+ )
43
+
44
+ const contextValue = useMemo(
45
+ () => ({
46
+ brand,
47
+ theme,
48
+ setTheme,
49
+ emotionTheme
50
+ }),
51
+ [brand, theme, emotionTheme]
52
+ )
53
+
54
+ return (
55
+ <ThemeContext.Provider value={contextValue}>
56
+ <EmotionThemeProvider theme={emotionTheme}>
57
+ {children}
58
+ </EmotionThemeProvider>
59
+ </ThemeContext.Provider>
60
+ )
61
+ }
62
+
63
+ export const usePawprint = () => useContext(ThemeContext)
@@ -0,0 +1,5 @@
1
+ export { PawprintProvider } from "./PawprintProvider"
2
+ export { ThemeProvider, usePawprint } from "./ThemeProvider"
3
+ export { createPawprintTheme } from "./utils"
4
+ export { theme } from "./theme"
5
+ export type { PawprintProviderProps } from "./PawprintProvider"
@@ -0,0 +1,3 @@
1
+ import { tokens } from "@butternutbox/pawprint-tokens"
2
+
3
+ export const theme = tokens
@@ -0,0 +1,31 @@
1
+ import {
2
+ DEFAULT_THEME_OPTIONS,
3
+ THEME_OPTIONS_MAP,
4
+ Brand,
5
+ Theme,
6
+ BrandName,
7
+ ThemeName
8
+ } from "@butternutbox/pawprint-tokens"
9
+ import type { Theme as EmotionTheme } from "@emotion/react"
10
+
11
+ /**
12
+ * Creates a pawprint theme based on brand and theme name for React Native
13
+ *
14
+ * @param brand - The brand to use (butternutbox, marro, etc.)
15
+ * @param theme - The theme name to use (default, dark, etc.)
16
+ * @returns A theme object with the selected brand and theme options
17
+ */
18
+ export const createPawprintTheme = (
19
+ brand: BrandName = Brand.Butternutbox,
20
+ theme: ThemeName = Theme.Default
21
+ ): EmotionTheme => {
22
+ const brandThemes = THEME_OPTIONS_MAP[brand]
23
+
24
+ // Check if the requested theme exists for this brand, otherwise use default
25
+ const themeOptions =
26
+ brandThemes && theme in brandThemes
27
+ ? brandThemes[theme as keyof typeof brandThemes]
28
+ : DEFAULT_THEME_OPTIONS
29
+
30
+ return themeOptions as EmotionTheme
31
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.woff2" {
2
+ const src: string
3
+ export default src
4
+ }
@@ -0,0 +1 @@
1
+ import "./theme"
@@ -0,0 +1,24 @@
1
+ import "@emotion/react"
2
+
3
+ import type {
4
+ BrandThemeSemantics,
5
+ BrandThemeComponents,
6
+ TokensCorePrimitives
7
+ } from "@butternutbox/pawprint-tokens"
8
+
9
+ // Augment @emotion/react module with our custom theme properties for React Native
10
+ declare module "@emotion/react" {
11
+ export interface Theme {
12
+ tokens: {
13
+ primitives: TokensCorePrimitives
14
+ semantics: BrandThemeSemantics
15
+ components: BrandThemeComponents
16
+ }
17
+ fontFaces?: string
18
+ }
19
+
20
+ interface ThemeOptions {
21
+ tokens: Theme["tokens"]
22
+ fontFaces?: string
23
+ }
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "@repo/typescript-config/react-library.json",
3
+ "include": ["src"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsup"
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ sourcemap: true,
8
+ clean: true,
9
+ external: ["react", "react-native"],
10
+ treeshake: true
11
+ })