@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,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"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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,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,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 @@
|
|
|
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
package/tsup.config.ts
ADDED