@butternutbox/pawprint-native 0.3.2 → 0.4.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 +10 -10
- package/CHANGELOG.md +14 -0
- package/dist/index.cjs +1012 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -5
- package/dist/index.d.ts +252 -5
- package/dist/index.js +1007 -173
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/atoms/Hint/Hint.tsx +1 -2
- package/src/components/atoms/Input/InputField.tsx +7 -1
- package/src/components/molecules/Animated/Animated.tsx +12 -3
- package/src/components/molecules/Countdown/Countdown.stories.tsx +218 -0
- package/src/components/molecules/Countdown/Countdown.tsx +315 -0
- package/src/components/molecules/Countdown/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +14 -24
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +275 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +292 -0
- package/src/components/molecules/ProductDisplayCard/index.ts +5 -0
- package/src/components/molecules/ProductListingCard/Badge.tsx +65 -0
- package/src/components/molecules/ProductListingCard/Grid.tsx +59 -0
- package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +209 -0
- package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +235 -0
- package/src/components/molecules/ProductListingCard/index.ts +2 -0
- package/src/components/molecules/TabNavigation/TabNavigation.stories.tsx +183 -0
- package/src/components/molecules/TabNavigation/TabNavigation.tsx +354 -0
- package/src/components/molecules/TabNavigation/index.ts +7 -0
- package/src/components/molecules/index.ts +4 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/token.ts +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewProps
|
|
8
|
+
} from "react-native"
|
|
9
|
+
import styled from "@emotion/native"
|
|
10
|
+
import { useTheme } from "@emotion/react"
|
|
11
|
+
import * as TabsPrimitive from "@rn-primitives/tabs"
|
|
12
|
+
import { Typography, type TypographyProps } from "../../atoms/Typography"
|
|
13
|
+
import { Animated, type AnimatedVariant } from "../Animated"
|
|
14
|
+
|
|
15
|
+
type TabNavigationLayout = "fixed" | "intrinsic"
|
|
16
|
+
|
|
17
|
+
// Typography's token-mode `color` is typed against the semantic text-colour
|
|
18
|
+
// set. The selected/unselected label colour (#a43260) is part of it, but the
|
|
19
|
+
// inactive label (Figma uses brown-5) is not — hence the cast below.
|
|
20
|
+
type TabLabelColour = Extract<TypographyProps, { token: object }>["color"]
|
|
21
|
+
|
|
22
|
+
type TabNavigationContextValue = {
|
|
23
|
+
layout: TabNavigationLayout
|
|
24
|
+
animation: AnimatedVariant
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TabNavigationContext = React.createContext<
|
|
28
|
+
TabNavigationContextValue | undefined
|
|
29
|
+
>(undefined)
|
|
30
|
+
|
|
31
|
+
const useTabNavigationContext = (): TabNavigationContextValue => {
|
|
32
|
+
const context = React.useContext(TabNavigationContext)
|
|
33
|
+
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"TabNavigation.List, TabNavigation.Tab and TabNavigation.Panel must be used within a TabNavigation component."
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return context
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
44
|
+
|
|
45
|
+
type TabNavigationOwnProps = {
|
|
46
|
+
value?: string
|
|
47
|
+
defaultValue?: string
|
|
48
|
+
onValueChange?: (value: string) => void
|
|
49
|
+
layout?: TabNavigationLayout
|
|
50
|
+
animation?: AnimatedVariant
|
|
51
|
+
children?: React.ReactNode
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type TabNavigationProps = TabNavigationOwnProps &
|
|
55
|
+
Omit<ViewProps, keyof TabNavigationOwnProps>
|
|
56
|
+
|
|
57
|
+
type TabNavigationListOwnProps = {
|
|
58
|
+
children?: React.ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type TabNavigationListProps = TabNavigationListOwnProps &
|
|
62
|
+
Omit<ViewProps, keyof TabNavigationListOwnProps>
|
|
63
|
+
|
|
64
|
+
type TabNavigationTabOwnProps = {
|
|
65
|
+
value: string
|
|
66
|
+
disabled?: boolean
|
|
67
|
+
children: React.ReactNode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TabNavigationTabProps = TabNavigationTabOwnProps &
|
|
71
|
+
Omit<ViewProps, keyof TabNavigationTabOwnProps>
|
|
72
|
+
|
|
73
|
+
type TabNavigationPanelOwnProps = {
|
|
74
|
+
value: string
|
|
75
|
+
children?: React.ReactNode
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type TabNavigationPanelProps = TabNavigationPanelOwnProps &
|
|
79
|
+
Omit<ViewProps, keyof TabNavigationPanelOwnProps>
|
|
80
|
+
|
|
81
|
+
const StyledFixedList = styled(View)({
|
|
82
|
+
flexDirection: "row",
|
|
83
|
+
alignItems: "stretch",
|
|
84
|
+
width: "100%"
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const StyledTab = styled(Pressable)<{
|
|
88
|
+
tabHeight: number
|
|
89
|
+
tabMinWidth: number
|
|
90
|
+
tabPaddingTop: number
|
|
91
|
+
tabPaddingBottom: number
|
|
92
|
+
tabPaddingHorizontal: number
|
|
93
|
+
tabGap: number
|
|
94
|
+
tabBorderWidth: number
|
|
95
|
+
tabBorderColor: string
|
|
96
|
+
tabFlex?: number
|
|
97
|
+
}>(
|
|
98
|
+
({
|
|
99
|
+
tabHeight,
|
|
100
|
+
tabMinWidth,
|
|
101
|
+
tabPaddingTop,
|
|
102
|
+
tabPaddingBottom,
|
|
103
|
+
tabPaddingHorizontal,
|
|
104
|
+
tabGap,
|
|
105
|
+
tabBorderWidth,
|
|
106
|
+
tabBorderColor,
|
|
107
|
+
tabFlex
|
|
108
|
+
}) => ({
|
|
109
|
+
flexDirection: "row",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "center",
|
|
112
|
+
gap: tabGap,
|
|
113
|
+
height: tabHeight,
|
|
114
|
+
minWidth: tabMinWidth,
|
|
115
|
+
paddingTop: tabPaddingTop,
|
|
116
|
+
paddingBottom: tabPaddingBottom,
|
|
117
|
+
paddingHorizontal: tabPaddingHorizontal,
|
|
118
|
+
borderBottomWidth: tabBorderWidth,
|
|
119
|
+
borderBottomColor: tabBorderColor,
|
|
120
|
+
...(tabFlex !== undefined ? { flex: tabFlex } : { flexShrink: 0 })
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wraps the tab buttons. In `fixed` layout the tabs share the available width
|
|
126
|
+
* equally; in `intrinsic` layout the tabs size to their content and the list
|
|
127
|
+
* scrolls horizontally when they overflow.
|
|
128
|
+
*
|
|
129
|
+
* Must be used within a `TabNavigation` component.
|
|
130
|
+
*/
|
|
131
|
+
const TabNavigationList = React.forwardRef<View, TabNavigationListProps>(
|
|
132
|
+
({ children, ...rest }, ref) => {
|
|
133
|
+
const { layout } = useTabNavigationContext()
|
|
134
|
+
|
|
135
|
+
if (layout === "intrinsic") {
|
|
136
|
+
return (
|
|
137
|
+
<TabsPrimitive.List asChild>
|
|
138
|
+
<View ref={ref} {...rest}>
|
|
139
|
+
<ScrollView
|
|
140
|
+
horizontal
|
|
141
|
+
showsHorizontalScrollIndicator={false}
|
|
142
|
+
contentContainerStyle={styles.scrollContent}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
</ScrollView>
|
|
146
|
+
</View>
|
|
147
|
+
</TabsPrimitive.List>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<TabsPrimitive.List asChild>
|
|
153
|
+
<StyledFixedList ref={ref} {...rest}>
|
|
154
|
+
{children}
|
|
155
|
+
</StyledFixedList>
|
|
156
|
+
</TabsPrimitive.List>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
TabNavigationList.displayName = "TabNavigation.List"
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* An individual tab button within a `TabNavigation`. Selected tabs render with
|
|
165
|
+
* a brand bottom border and bold label; unselected tabs use a neutral border
|
|
166
|
+
* and semibold label. Disabled tabs are muted and non-interactive.
|
|
167
|
+
*
|
|
168
|
+
* Wraps `@rn-primitives/tabs` `Trigger` for accessibility (role, selected and
|
|
169
|
+
* disabled state). Unlike the web version there are no hover or focus-visible
|
|
170
|
+
* states (not applicable on mobile).
|
|
171
|
+
*
|
|
172
|
+
* @param {string} value - Unique identifier; matches the `value` of a `TabNavigation.Panel`.
|
|
173
|
+
* @param {boolean} [disabled=false] - Whether this tab is disabled.
|
|
174
|
+
* @param {React.ReactNode} children - Tab label (and optional leading content).
|
|
175
|
+
*/
|
|
176
|
+
const TabNavigationTab = React.forwardRef<View, TabNavigationTabProps>(
|
|
177
|
+
({ value, disabled = false, children, ...rest }, ref) => {
|
|
178
|
+
const { layout } = useTabNavigationContext()
|
|
179
|
+
const { value: selectedValue } = TabsPrimitive.useRootContext()
|
|
180
|
+
const theme = useTheme()
|
|
181
|
+
const { tabItem, tabitem } = theme.tokens.components.tabNavigation
|
|
182
|
+
const { spacing, colour, borderWidth, typography } = tabItem
|
|
183
|
+
|
|
184
|
+
const selected = selectedValue === value
|
|
185
|
+
const token = selected ? typography.selected : typography.default
|
|
186
|
+
const borderColour = selected
|
|
187
|
+
? colour.border.default
|
|
188
|
+
: colour.border.disabled
|
|
189
|
+
// TODO: Add token — no component/semantic colour exists for the inactive
|
|
190
|
+
// tab label, so this falls back to the brown-5 primitive (the Figma value).
|
|
191
|
+
const textColour: TabLabelColour = disabled
|
|
192
|
+
? (theme.tokens.primitives.colour.brand.brown[
|
|
193
|
+
"5"
|
|
194
|
+
] as unknown as TabLabelColour)
|
|
195
|
+
: colour.text.default
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<TabsPrimitive.Trigger value={value} disabled={disabled} asChild>
|
|
199
|
+
<StyledTab
|
|
200
|
+
ref={ref}
|
|
201
|
+
tabHeight={parseTokenValue(tabitem.size.height)}
|
|
202
|
+
tabMinWidth={parseTokenValue(tabitem.size.minWidth)}
|
|
203
|
+
tabPaddingTop={parseTokenValue(spacing.topPadding)}
|
|
204
|
+
tabPaddingBottom={parseTokenValue(spacing.bottomPadding)}
|
|
205
|
+
tabPaddingHorizontal={parseTokenValue(spacing.horizontalPadding)}
|
|
206
|
+
tabGap={parseTokenValue(spacing.gap)}
|
|
207
|
+
tabBorderWidth={parseTokenValue(borderWidth.default)}
|
|
208
|
+
tabBorderColor={borderColour}
|
|
209
|
+
tabFlex={layout === "fixed" ? 1 : undefined}
|
|
210
|
+
{...rest}
|
|
211
|
+
>
|
|
212
|
+
<Typography token={token} color={textColour}>
|
|
213
|
+
{children}
|
|
214
|
+
</Typography>
|
|
215
|
+
</StyledTab>
|
|
216
|
+
</TabsPrimitive.Trigger>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
TabNavigationTab.displayName = "TabNavigation.Tab"
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Content area shown when the tab with the matching `value` is selected. Wraps
|
|
225
|
+
* `@rn-primitives/tabs` `Content` (which unmounts the inactive panels) and
|
|
226
|
+
* animates the content with the `Animated` atom (variant set by the parent
|
|
227
|
+
* `TabNavigation`'s `animation` prop). The enter animation replays each time
|
|
228
|
+
* the tab becomes active.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} value - Matches the `value` of a `TabNavigation.Tab`.
|
|
231
|
+
* @param {React.ReactNode} [children] - Panel content.
|
|
232
|
+
*/
|
|
233
|
+
const TabNavigationPanel = React.forwardRef<View, TabNavigationPanelProps>(
|
|
234
|
+
({ value, children, ...rest }, ref) => {
|
|
235
|
+
const { animation } = useTabNavigationContext()
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<TabsPrimitive.Content value={value} asChild>
|
|
239
|
+
<Animated ref={ref} variant={animation} exit={false} {...rest}>
|
|
240
|
+
{children}
|
|
241
|
+
</Animated>
|
|
242
|
+
</TabsPrimitive.Content>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
TabNavigationPanel.displayName = "TabNavigation.Panel"
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Top-level navigation that organises content into mutually exclusive views.
|
|
251
|
+
* Selecting a tab replaces the content area below it. Wraps
|
|
252
|
+
* `@rn-primitives/tabs` for accessibility.
|
|
253
|
+
*
|
|
254
|
+
* Compose with the compound parts:
|
|
255
|
+
* - `TabNavigation.List` — wraps the tab buttons.
|
|
256
|
+
* - `TabNavigation.Tab` — an individual tab button.
|
|
257
|
+
* - `TabNavigation.Panel` — content shown for the matching tab, animated via
|
|
258
|
+
* the `Animated` atom.
|
|
259
|
+
*
|
|
260
|
+
* Note: API parity with the web `TabNavigation`. The native version uses a
|
|
261
|
+
* horizontal `ScrollView` for the intrinsic layout and has no hover or
|
|
262
|
+
* focus-visible states (not applicable on mobile).
|
|
263
|
+
*
|
|
264
|
+
* @param {string} [value] - Controlled selected tab value.
|
|
265
|
+
* @param {string} [defaultValue] - Uncontrolled initial selected tab value. Provide this (or `value`) so a tab is selected on mount.
|
|
266
|
+
* @param {(value: string) => void} [onValueChange] - Called when the selected tab changes.
|
|
267
|
+
* @param {"fixed" | "intrinsic"} [layout="fixed"] - `fixed`: tabs share the width equally. `intrinsic`: tabs size to content and the list scrolls horizontally when they overflow.
|
|
268
|
+
* @param {AnimatedVariant} [animation="slideIn"] - Animation preset applied to each panel's content.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```tsx
|
|
272
|
+
* import { TabNavigation } from "@butternutbox/pawprint-native"
|
|
273
|
+
*
|
|
274
|
+
* <TabNavigation defaultValue="give">
|
|
275
|
+
* <TabNavigation.List>
|
|
276
|
+
* <TabNavigation.Tab value="give">Give & get</TabNavigation.Tab>
|
|
277
|
+
* <TabNavigation.Tab value="pack">My pack</TabNavigation.Tab>
|
|
278
|
+
* </TabNavigation.List>
|
|
279
|
+
* <TabNavigation.Panel value="give">Give content</TabNavigation.Panel>
|
|
280
|
+
* <TabNavigation.Panel value="pack">Pack content</TabNavigation.Panel>
|
|
281
|
+
* </TabNavigation>
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
const TabNavigationRoot = React.forwardRef<View, TabNavigationProps>(
|
|
285
|
+
(
|
|
286
|
+
{
|
|
287
|
+
value,
|
|
288
|
+
defaultValue,
|
|
289
|
+
onValueChange,
|
|
290
|
+
layout = "fixed",
|
|
291
|
+
animation = "slideIn",
|
|
292
|
+
children,
|
|
293
|
+
...rest
|
|
294
|
+
},
|
|
295
|
+
ref
|
|
296
|
+
) => {
|
|
297
|
+
const isControlled = typeof value === "string"
|
|
298
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(
|
|
299
|
+
() => defaultValue ?? ""
|
|
300
|
+
)
|
|
301
|
+
const currentValue = isControlled ? value : uncontrolledValue
|
|
302
|
+
|
|
303
|
+
const handleValueChange = React.useCallback(
|
|
304
|
+
(newValue: string) => {
|
|
305
|
+
if (!isControlled) {
|
|
306
|
+
setUncontrolledValue(newValue)
|
|
307
|
+
}
|
|
308
|
+
onValueChange?.(newValue)
|
|
309
|
+
},
|
|
310
|
+
[isControlled, onValueChange]
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const contextValue = React.useMemo<TabNavigationContextValue>(
|
|
314
|
+
() => ({ layout, animation }),
|
|
315
|
+
[layout, animation]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<TabNavigationContext.Provider value={contextValue}>
|
|
320
|
+
<TabsPrimitive.Root
|
|
321
|
+
ref={ref}
|
|
322
|
+
value={currentValue}
|
|
323
|
+
onValueChange={handleValueChange}
|
|
324
|
+
{...rest}
|
|
325
|
+
>
|
|
326
|
+
{children}
|
|
327
|
+
</TabsPrimitive.Root>
|
|
328
|
+
</TabNavigationContext.Provider>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
TabNavigationRoot.displayName = "TabNavigation"
|
|
334
|
+
|
|
335
|
+
const styles = StyleSheet.create({
|
|
336
|
+
scrollContent: {
|
|
337
|
+
flexDirection: "row",
|
|
338
|
+
alignItems: "stretch"
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
type TabNavigationComponent = React.ForwardRefExoticComponent<
|
|
343
|
+
TabNavigationProps & React.RefAttributes<View>
|
|
344
|
+
> & {
|
|
345
|
+
List: typeof TabNavigationList
|
|
346
|
+
Tab: typeof TabNavigationTab
|
|
347
|
+
Panel: typeof TabNavigationPanel
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const TabNavigation = Object.assign(TabNavigationRoot, {
|
|
351
|
+
List: TabNavigationList,
|
|
352
|
+
Tab: TabNavigationTab,
|
|
353
|
+
Panel: TabNavigationPanel
|
|
354
|
+
}) as TabNavigationComponent
|
|
@@ -19,3 +19,7 @@ export * from "./Tooltip"
|
|
|
19
19
|
export * from "./MessageCard"
|
|
20
20
|
export * from "./DatePicker"
|
|
21
21
|
export * from "./PictureSelector"
|
|
22
|
+
export * from "./Countdown"
|
|
23
|
+
export * from "./ProductListingCard"
|
|
24
|
+
export * from "./ProductDisplayCard"
|
|
25
|
+
export * from "./TabNavigation"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseTokenValue } from "./token"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const parseTokenValue = (value: string): number => parseFloat(value)
|