@butternutbox/pawprint-native 0.3.2 → 0.4.0
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 +9 -9
- package/CHANGELOG.md +8 -0
- package/dist/index.cjs +965 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -5
- package/dist/index.d.ts +246 -5
- package/dist/index.js +960 -151
- 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/ProductDisplayCard/ProductDisplayCard.stories.tsx +248 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +243 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@butternutbox/pawprint-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ButternutBox Pawprint Design System - React Native Components",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@rn-primitives/slider": "^1.4.0",
|
|
34
34
|
"@rn-primitives/slot": "^1.0.5",
|
|
35
35
|
"@rn-primitives/switch": "^1.4.0",
|
|
36
|
+
"@rn-primitives/tabs": "^1.4.0",
|
|
36
37
|
"@rn-primitives/tooltip": "^1.4.0",
|
|
37
38
|
"date-fns": "^4.1.0",
|
|
38
39
|
"expo": "^55.0.9"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { View, ViewProps } from "react-native"
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
|
+
import { parseTokenValue } from "../../../utils"
|
|
4
5
|
|
|
5
6
|
type HintVariant = "default" | "success" | "warning" | "error"
|
|
6
7
|
|
|
@@ -10,8 +11,6 @@ type HintOwnProps = {
|
|
|
10
11
|
|
|
11
12
|
export type HintProps = HintOwnProps & Omit<ViewProps, keyof HintOwnProps>
|
|
12
13
|
|
|
13
|
-
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
14
|
-
|
|
15
14
|
const StyledHint = styled(View)<{ hintVariant: HintVariant }>(({
|
|
16
15
|
theme,
|
|
17
16
|
hintVariant
|
|
@@ -60,13 +60,19 @@ const StyledInputWrapper = styled(Animated.View)<{
|
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
const StyledInput = styled(TextInput)(({ theme }) => {
|
|
63
|
-
const { colour } = theme.tokens.components.inputs
|
|
63
|
+
const { colour, field } = theme.tokens.components.inputs
|
|
64
|
+
const placeholderTypography = field.placeholder.default
|
|
64
65
|
return {
|
|
65
66
|
flex: 1,
|
|
66
67
|
minWidth: 0,
|
|
67
68
|
minHeight: 0,
|
|
68
69
|
padding: 0,
|
|
69
70
|
color: colour.field.text.default,
|
|
71
|
+
fontSize: parseTokenValue(placeholderTypography.fontSize),
|
|
72
|
+
fontFamily: placeholderTypography.fontFamily,
|
|
73
|
+
fontWeight: placeholderTypography.fontWeight,
|
|
74
|
+
lineHeight: parseTokenValue(placeholderTypography.lineHeight),
|
|
75
|
+
letterSpacing: parseTokenValue(placeholderTypography.letterSpacing),
|
|
70
76
|
// Suppress the browser default focus outline on React Native Web —
|
|
71
77
|
// the focus indicator is rendered on the wrapper instead.
|
|
72
78
|
...({
|
|
@@ -224,6 +224,13 @@ export type AnimatedProps = {
|
|
|
224
224
|
* ```
|
|
225
225
|
*/
|
|
226
226
|
delay?: number
|
|
227
|
+
/**
|
|
228
|
+
* Whether to play the exit animation when the component unmounts. Defaults
|
|
229
|
+
* to `true`. Set to `false` for an enter-only animation — useful when a
|
|
230
|
+
* parent unmounts the element directly (e.g. tab panels). Mirrors the web
|
|
231
|
+
* `Animated`, whose exit only runs inside `AnimatePresence`.
|
|
232
|
+
*/
|
|
233
|
+
exit?: boolean
|
|
227
234
|
} & ViewProps
|
|
228
235
|
|
|
229
236
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
@@ -232,7 +239,9 @@ export type AnimatedProps = {
|
|
|
232
239
|
* Reanimated wrapper that applies a named animation preset to its children.
|
|
233
240
|
*
|
|
234
241
|
* Exit animations run automatically when the component unmounts — no wrapper
|
|
235
|
-
* component needed (unlike `AnimatePresence` on web).
|
|
242
|
+
* component needed (unlike `AnimatePresence` on web). Pass `exit={false}` for
|
|
243
|
+
* an enter-only animation (matching the web default, where exit requires
|
|
244
|
+
* `AnimatePresence`).
|
|
236
245
|
*
|
|
237
246
|
* Respects the OS reduce-motion setting automatically — Reanimated builders
|
|
238
247
|
* default to `ReduceMotion.System` so no extra hook is needed.
|
|
@@ -260,7 +269,7 @@ export type AnimatedProps = {
|
|
|
260
269
|
* ```
|
|
261
270
|
*/
|
|
262
271
|
export const Animated = React.forwardRef<View, AnimatedProps>(
|
|
263
|
-
({ variant = "fade", delay, children, ...props }, ref) => {
|
|
272
|
+
({ variant = "fade", delay, exit = true, children, ...props }, ref) => {
|
|
264
273
|
const delayMs =
|
|
265
274
|
delay !== undefined && delay > 0 ? Math.round(delay * 1000) : 0
|
|
266
275
|
|
|
@@ -271,7 +280,7 @@ export const Animated = React.forwardRef<View, AnimatedProps>(
|
|
|
271
280
|
<Reanimated.View
|
|
272
281
|
ref={ref}
|
|
273
282
|
entering={entering}
|
|
274
|
-
exiting={EXITING[variant]}
|
|
283
|
+
exiting={exit ? EXITING[variant] : undefined}
|
|
275
284
|
{...props}
|
|
276
285
|
>
|
|
277
286
|
{children}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { Countdown } from "./Countdown"
|
|
4
|
+
import type { CountdownProps } from "./Countdown"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "Molecules/Countdown",
|
|
9
|
+
component: Countdown,
|
|
10
|
+
argTypes: {
|
|
11
|
+
layout: {
|
|
12
|
+
control: { type: "select" },
|
|
13
|
+
options: ["stacked", "inline"],
|
|
14
|
+
description: "Visual layout"
|
|
15
|
+
},
|
|
16
|
+
days: {
|
|
17
|
+
control: { type: "number", min: 0 },
|
|
18
|
+
description: "Days remaining"
|
|
19
|
+
},
|
|
20
|
+
hours: {
|
|
21
|
+
control: { type: "number", min: 0 },
|
|
22
|
+
description: "Hours remaining"
|
|
23
|
+
},
|
|
24
|
+
minutes: {
|
|
25
|
+
control: { type: "number", min: 0 },
|
|
26
|
+
description: "Minutes remaining"
|
|
27
|
+
},
|
|
28
|
+
seconds: {
|
|
29
|
+
control: { type: "number", min: 0 },
|
|
30
|
+
description: "Seconds remaining"
|
|
31
|
+
},
|
|
32
|
+
showDays: {
|
|
33
|
+
control: { type: "boolean" },
|
|
34
|
+
description: "Render the days unit and its separator"
|
|
35
|
+
},
|
|
36
|
+
showSeconds: {
|
|
37
|
+
control: { type: "boolean" },
|
|
38
|
+
description: "Render the seconds unit and its separator"
|
|
39
|
+
},
|
|
40
|
+
label: {
|
|
41
|
+
control: { type: "text" },
|
|
42
|
+
description: "Customizable prefix label (inline layout only)"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Playground = {
|
|
48
|
+
args: {
|
|
49
|
+
layout: "stacked",
|
|
50
|
+
days: 0,
|
|
51
|
+
hours: 0,
|
|
52
|
+
minutes: 0,
|
|
53
|
+
seconds: 0,
|
|
54
|
+
showDays: true,
|
|
55
|
+
showSeconds: true,
|
|
56
|
+
label: "Label:"
|
|
57
|
+
},
|
|
58
|
+
render: (args: CountdownProps) => (
|
|
59
|
+
<View style={styles.container}>
|
|
60
|
+
<Countdown {...args} />
|
|
61
|
+
</View>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Stacked = {
|
|
66
|
+
name: "Stacked",
|
|
67
|
+
render: () => (
|
|
68
|
+
<View style={styles.container}>
|
|
69
|
+
<Countdown
|
|
70
|
+
layout="stacked"
|
|
71
|
+
days={2}
|
|
72
|
+
hours={9}
|
|
73
|
+
minutes={48}
|
|
74
|
+
seconds={38}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const Inline = {
|
|
81
|
+
name: "Inline",
|
|
82
|
+
render: () => (
|
|
83
|
+
<View style={styles.container}>
|
|
84
|
+
<Countdown
|
|
85
|
+
layout="inline"
|
|
86
|
+
label="Label:"
|
|
87
|
+
days={0}
|
|
88
|
+
hours={0}
|
|
89
|
+
minutes={0}
|
|
90
|
+
seconds={0}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const WithoutDays = {
|
|
97
|
+
name: "Without Days (under 24h)",
|
|
98
|
+
render: () => (
|
|
99
|
+
<View style={styles.column}>
|
|
100
|
+
<Countdown
|
|
101
|
+
layout="stacked"
|
|
102
|
+
showDays={false}
|
|
103
|
+
hours={3}
|
|
104
|
+
minutes={49}
|
|
105
|
+
seconds={38}
|
|
106
|
+
/>
|
|
107
|
+
<Countdown
|
|
108
|
+
layout="inline"
|
|
109
|
+
label="Delivery ends in:"
|
|
110
|
+
showDays={false}
|
|
111
|
+
hours={3}
|
|
112
|
+
minutes={49}
|
|
113
|
+
seconds={38}
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const WithoutSeconds = {
|
|
120
|
+
name: "Without Seconds",
|
|
121
|
+
render: () => (
|
|
122
|
+
<View style={styles.column}>
|
|
123
|
+
<Countdown
|
|
124
|
+
layout="stacked"
|
|
125
|
+
showSeconds={false}
|
|
126
|
+
days={1}
|
|
127
|
+
hours={6}
|
|
128
|
+
minutes={20}
|
|
129
|
+
/>
|
|
130
|
+
<Countdown
|
|
131
|
+
layout="inline"
|
|
132
|
+
label="Sale ends:"
|
|
133
|
+
showSeconds={false}
|
|
134
|
+
days={1}
|
|
135
|
+
hours={6}
|
|
136
|
+
minutes={20}
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const AllVariants = {
|
|
143
|
+
name: "All Variants",
|
|
144
|
+
render: () => (
|
|
145
|
+
<View style={styles.column}>
|
|
146
|
+
<View style={styles.section}>
|
|
147
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
148
|
+
Stacked
|
|
149
|
+
</Typography>
|
|
150
|
+
<Countdown
|
|
151
|
+
layout="stacked"
|
|
152
|
+
days={2}
|
|
153
|
+
hours={9}
|
|
154
|
+
minutes={48}
|
|
155
|
+
seconds={38}
|
|
156
|
+
/>
|
|
157
|
+
<Countdown
|
|
158
|
+
layout="stacked"
|
|
159
|
+
showDays={false}
|
|
160
|
+
hours={9}
|
|
161
|
+
minutes={48}
|
|
162
|
+
seconds={38}
|
|
163
|
+
/>
|
|
164
|
+
<Countdown
|
|
165
|
+
layout="stacked"
|
|
166
|
+
showSeconds={false}
|
|
167
|
+
days={2}
|
|
168
|
+
hours={9}
|
|
169
|
+
minutes={48}
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
172
|
+
<View style={styles.section}>
|
|
173
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
174
|
+
Inline
|
|
175
|
+
</Typography>
|
|
176
|
+
<Countdown
|
|
177
|
+
layout="inline"
|
|
178
|
+
label="Label:"
|
|
179
|
+
days={2}
|
|
180
|
+
hours={9}
|
|
181
|
+
minutes={48}
|
|
182
|
+
seconds={38}
|
|
183
|
+
/>
|
|
184
|
+
<Countdown
|
|
185
|
+
layout="inline"
|
|
186
|
+
label="Label:"
|
|
187
|
+
showDays={false}
|
|
188
|
+
hours={9}
|
|
189
|
+
minutes={48}
|
|
190
|
+
seconds={38}
|
|
191
|
+
/>
|
|
192
|
+
<Countdown
|
|
193
|
+
layout="inline"
|
|
194
|
+
label="Label:"
|
|
195
|
+
showSeconds={false}
|
|
196
|
+
days={2}
|
|
197
|
+
hours={9}
|
|
198
|
+
minutes={48}
|
|
199
|
+
/>
|
|
200
|
+
</View>
|
|
201
|
+
</View>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const styles = StyleSheet.create({
|
|
206
|
+
container: {
|
|
207
|
+
padding: 16
|
|
208
|
+
},
|
|
209
|
+
column: {
|
|
210
|
+
padding: 16,
|
|
211
|
+
flexDirection: "column",
|
|
212
|
+
gap: 16
|
|
213
|
+
},
|
|
214
|
+
section: {
|
|
215
|
+
flexDirection: "column",
|
|
216
|
+
gap: 8
|
|
217
|
+
}
|
|
218
|
+
})
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
|
|
7
|
+
type CountdownLayout = "stacked" | "inline"
|
|
8
|
+
|
|
9
|
+
type CountdownUnit = {
|
|
10
|
+
key: "days" | "hours" | "minutes" | "seconds"
|
|
11
|
+
value: number
|
|
12
|
+
label: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type CountdownOwnProps = {
|
|
16
|
+
layout?: CountdownLayout
|
|
17
|
+
days?: number
|
|
18
|
+
hours?: number
|
|
19
|
+
minutes?: number
|
|
20
|
+
seconds?: number
|
|
21
|
+
showDays?: boolean
|
|
22
|
+
showSeconds?: boolean
|
|
23
|
+
label?: React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CountdownProps = CountdownOwnProps &
|
|
27
|
+
Omit<ViewProps, keyof CountdownOwnProps>
|
|
28
|
+
|
|
29
|
+
const UNIT_LABELS = {
|
|
30
|
+
days: "days",
|
|
31
|
+
hours: "hrs",
|
|
32
|
+
minutes: "min",
|
|
33
|
+
seconds: "secs"
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
37
|
+
|
|
38
|
+
const pad = (value: number): string =>
|
|
39
|
+
String(Math.max(0, Math.floor(value))).padStart(2, "0")
|
|
40
|
+
|
|
41
|
+
const StackedRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
42
|
+
flexDirection: "row",
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
alignSelf: "flex-start",
|
|
45
|
+
gap: rootGap
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
const StackedItem = styled(View)<{
|
|
49
|
+
itemGap: number
|
|
50
|
+
itemVerticalPadding: number
|
|
51
|
+
itemHorizontalPadding: number
|
|
52
|
+
itemBgColor: string
|
|
53
|
+
itemBorderRadius: number
|
|
54
|
+
}>(
|
|
55
|
+
({
|
|
56
|
+
itemGap,
|
|
57
|
+
itemVerticalPadding,
|
|
58
|
+
itemHorizontalPadding,
|
|
59
|
+
itemBgColor,
|
|
60
|
+
itemBorderRadius
|
|
61
|
+
}) => ({
|
|
62
|
+
flexDirection: "column",
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
gap: itemGap,
|
|
66
|
+
paddingVertical: itemVerticalPadding,
|
|
67
|
+
paddingHorizontal: itemHorizontalPadding,
|
|
68
|
+
backgroundColor: itemBgColor,
|
|
69
|
+
borderRadius: itemBorderRadius
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const StackedCountBox = styled(View)<{
|
|
74
|
+
countPaddingTop: number
|
|
75
|
+
countBgColor: string
|
|
76
|
+
countBorderRadius: number
|
|
77
|
+
}>(({ countPaddingTop, countBgColor, countBorderRadius }) => ({
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
justifyContent: "center",
|
|
80
|
+
// TODO: Add token — fixed count box dimensions are not yet tokenised.
|
|
81
|
+
width: 48,
|
|
82
|
+
height: 36,
|
|
83
|
+
paddingTop: countPaddingTop,
|
|
84
|
+
backgroundColor: countBgColor,
|
|
85
|
+
borderRadius: countBorderRadius
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
const InlineRoot = styled(View)<{
|
|
89
|
+
rootGap: number
|
|
90
|
+
rootVerticalPadding: number
|
|
91
|
+
rootHorizontalPadding: number
|
|
92
|
+
rootBgColor: string
|
|
93
|
+
rootBorderRadius: number
|
|
94
|
+
}>(
|
|
95
|
+
({
|
|
96
|
+
rootGap,
|
|
97
|
+
rootVerticalPadding,
|
|
98
|
+
rootHorizontalPadding,
|
|
99
|
+
rootBgColor,
|
|
100
|
+
rootBorderRadius
|
|
101
|
+
}) => ({
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "space-between",
|
|
105
|
+
gap: rootGap,
|
|
106
|
+
width: "100%",
|
|
107
|
+
paddingVertical: rootVerticalPadding,
|
|
108
|
+
paddingHorizontal: rootHorizontalPadding,
|
|
109
|
+
backgroundColor: rootBgColor,
|
|
110
|
+
borderRadius: rootBorderRadius,
|
|
111
|
+
overflow: "hidden"
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const InlineGroup = styled(View)<{ groupGap: number }>(({ groupGap }) => ({
|
|
116
|
+
flexDirection: "row",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
justifyContent: "flex-end",
|
|
119
|
+
gap: groupGap
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
const InlineCount = styled(View)<{ countGap: number }>(({ countGap }) => ({
|
|
123
|
+
flexDirection: "row",
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
gap: countGap
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Displays the remaining time until an event, deadline, or expiry. Purely
|
|
130
|
+
* presentational — the consuming app computes the remaining time and passes
|
|
131
|
+
* the discrete `days`/`hours`/`minutes`/`seconds` values.
|
|
132
|
+
*
|
|
133
|
+
* Two layouts:
|
|
134
|
+
* - `stacked` — each unit sits in its own styled container (brand fill) with
|
|
135
|
+
* the value above the label, separated by colons. Best for heroes, checkout
|
|
136
|
+
* screens, and promotional cards.
|
|
137
|
+
* - `inline` — a single condensed banner with a customizable prefix `label`
|
|
138
|
+
* followed by the time string on one line. Best for notification banners,
|
|
139
|
+
* sticky footers, and dense UI.
|
|
140
|
+
*
|
|
141
|
+
* Units can be toggled to scale across timeframes: hide `days` when under
|
|
142
|
+
* 24 hours remain (`showDays={false}`) and hide `seconds` to reduce visual
|
|
143
|
+
* noise (`showSeconds={false}`). Toggling a unit also removes its separator.
|
|
144
|
+
* Hours and minutes are always shown.
|
|
145
|
+
*
|
|
146
|
+
* Note: API parity with the web `Countdown`. Unlike the web version, the
|
|
147
|
+
* native root is a `View` and px token strings are parsed to numbers.
|
|
148
|
+
*
|
|
149
|
+
* @param {"stacked" | "inline"} [layout="stacked"] - Visual layout.
|
|
150
|
+
* @param {number} [days=0] - Days remaining (zero-padded to 2 digits).
|
|
151
|
+
* @param {number} [hours=0] - Hours remaining (zero-padded to 2 digits).
|
|
152
|
+
* @param {number} [minutes=0] - Minutes remaining (zero-padded to 2 digits).
|
|
153
|
+
* @param {number} [seconds=0] - Seconds remaining (zero-padded to 2 digits).
|
|
154
|
+
* @param {boolean} [showDays=true] - Whether to render the days unit and its separator.
|
|
155
|
+
* @param {boolean} [showSeconds=true] - Whether to render the seconds unit and its separator.
|
|
156
|
+
* @param {React.ReactNode} [label] - Customizable prefix label, rendered in the `inline` layout only.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```tsx
|
|
160
|
+
* import { Countdown } from "@butternutbox/pawprint-native"
|
|
161
|
+
*
|
|
162
|
+
* <Countdown layout="stacked" days={2} hours={9} minutes={48} seconds={38} />
|
|
163
|
+
* <Countdown layout="inline" label="Sale ends:" showDays={false} hours={3} minutes={49} seconds={38} />
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export const Countdown = React.forwardRef<View, CountdownProps>(
|
|
167
|
+
(
|
|
168
|
+
{
|
|
169
|
+
layout = "stacked",
|
|
170
|
+
days = 0,
|
|
171
|
+
hours = 0,
|
|
172
|
+
minutes = 0,
|
|
173
|
+
seconds = 0,
|
|
174
|
+
showDays = true,
|
|
175
|
+
showSeconds = true,
|
|
176
|
+
label,
|
|
177
|
+
...rest
|
|
178
|
+
},
|
|
179
|
+
ref
|
|
180
|
+
) => {
|
|
181
|
+
const theme = useTheme()
|
|
182
|
+
const { countdown } = theme.tokens.components
|
|
183
|
+
const { countdownItem } = countdown
|
|
184
|
+
|
|
185
|
+
const units: CountdownUnit[] = [
|
|
186
|
+
showDays && {
|
|
187
|
+
key: "days" as const,
|
|
188
|
+
value: days,
|
|
189
|
+
label: UNIT_LABELS.days
|
|
190
|
+
},
|
|
191
|
+
{ key: "hours" as const, value: hours, label: UNIT_LABELS.hours },
|
|
192
|
+
{ key: "minutes" as const, value: minutes, label: UNIT_LABELS.minutes },
|
|
193
|
+
showSeconds && {
|
|
194
|
+
key: "seconds" as const,
|
|
195
|
+
value: seconds,
|
|
196
|
+
label: UNIT_LABELS.seconds
|
|
197
|
+
}
|
|
198
|
+
].filter(Boolean) as CountdownUnit[]
|
|
199
|
+
|
|
200
|
+
const colon = (key: string) => (
|
|
201
|
+
<Typography
|
|
202
|
+
key={key}
|
|
203
|
+
token={countdown.typography.colon}
|
|
204
|
+
color={countdown.colour.text.colon}
|
|
205
|
+
>
|
|
206
|
+
:
|
|
207
|
+
</Typography>
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if (layout === "inline") {
|
|
211
|
+
return (
|
|
212
|
+
<InlineRoot
|
|
213
|
+
ref={ref}
|
|
214
|
+
rootGap={parseTokenValue(countdown.spacing.gap)}
|
|
215
|
+
rootVerticalPadding={parseTokenValue(
|
|
216
|
+
countdown.spacing.verticalPadding
|
|
217
|
+
)}
|
|
218
|
+
rootHorizontalPadding={parseTokenValue(
|
|
219
|
+
countdown.spacing.horizontalPadding
|
|
220
|
+
)}
|
|
221
|
+
rootBgColor={countdown.colour.background.default}
|
|
222
|
+
rootBorderRadius={parseTokenValue(countdown.borderRadius.default)}
|
|
223
|
+
{...rest}
|
|
224
|
+
>
|
|
225
|
+
{label != null && (
|
|
226
|
+
<Typography variant="body" weight="bold" size="md" color="primary">
|
|
227
|
+
{label}
|
|
228
|
+
</Typography>
|
|
229
|
+
)}
|
|
230
|
+
<InlineGroup
|
|
231
|
+
groupGap={parseTokenValue(countdown.countdownGroup.spacing.gap)}
|
|
232
|
+
>
|
|
233
|
+
{units.map((unit, index) => (
|
|
234
|
+
<React.Fragment key={unit.key}>
|
|
235
|
+
{index > 0 && colon(`colon-${unit.key}`)}
|
|
236
|
+
<InlineCount
|
|
237
|
+
countGap={parseTokenValue(countdown.count.spacing.gap)}
|
|
238
|
+
>
|
|
239
|
+
<Typography
|
|
240
|
+
variant="body"
|
|
241
|
+
weight="bold"
|
|
242
|
+
size="md"
|
|
243
|
+
color="primary"
|
|
244
|
+
>
|
|
245
|
+
{pad(unit.value)}
|
|
246
|
+
</Typography>
|
|
247
|
+
<Typography
|
|
248
|
+
variant="body"
|
|
249
|
+
weight="medium"
|
|
250
|
+
size="xs"
|
|
251
|
+
color="secondary"
|
|
252
|
+
>
|
|
253
|
+
{unit.label}
|
|
254
|
+
</Typography>
|
|
255
|
+
</InlineCount>
|
|
256
|
+
</React.Fragment>
|
|
257
|
+
))}
|
|
258
|
+
</InlineGroup>
|
|
259
|
+
</InlineRoot>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<StackedRoot
|
|
265
|
+
ref={ref}
|
|
266
|
+
rootGap={parseTokenValue(countdown.spacing.gap)}
|
|
267
|
+
{...rest}
|
|
268
|
+
>
|
|
269
|
+
{units.map((unit, index) => (
|
|
270
|
+
<React.Fragment key={unit.key}>
|
|
271
|
+
{index > 0 && colon(`colon-${unit.key}`)}
|
|
272
|
+
<StackedItem
|
|
273
|
+
itemGap={parseTokenValue(countdownItem.spacing.gap)}
|
|
274
|
+
itemVerticalPadding={parseTokenValue(
|
|
275
|
+
countdownItem.spacing.verticalPadding
|
|
276
|
+
)}
|
|
277
|
+
itemHorizontalPadding={parseTokenValue(
|
|
278
|
+
countdownItem.spacing.horizontalPadding
|
|
279
|
+
)}
|
|
280
|
+
itemBgColor={countdownItem.colour.background.default}
|
|
281
|
+
itemBorderRadius={parseTokenValue(
|
|
282
|
+
countdownItem.borderRadius.default
|
|
283
|
+
)}
|
|
284
|
+
>
|
|
285
|
+
<StackedCountBox
|
|
286
|
+
countPaddingTop={parseTokenValue(
|
|
287
|
+
countdownItem.count.spacing.paddingTop
|
|
288
|
+
)}
|
|
289
|
+
countBgColor={countdownItem.count.colour.background.default}
|
|
290
|
+
countBorderRadius={parseTokenValue(
|
|
291
|
+
countdownItem.count.borderRadius.default
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
<Typography
|
|
295
|
+
token={countdownItem.typography.count}
|
|
296
|
+
color={countdownItem.count.colour.text.count}
|
|
297
|
+
>
|
|
298
|
+
{pad(unit.value)}
|
|
299
|
+
</Typography>
|
|
300
|
+
</StackedCountBox>
|
|
301
|
+
<Typography
|
|
302
|
+
token={countdownItem.typography.label}
|
|
303
|
+
color={countdownItem.colour.text.label}
|
|
304
|
+
>
|
|
305
|
+
{unit.label}
|
|
306
|
+
</Typography>
|
|
307
|
+
</StackedItem>
|
|
308
|
+
</React.Fragment>
|
|
309
|
+
))}
|
|
310
|
+
</StackedRoot>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
Countdown.displayName = "Countdown"
|