@butternutbox/pawprint-native 0.4.0 → 0.5.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 +6 -6
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +121 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -10
- package/dist/index.d.ts +40 -10
- package/dist/index.js +121 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/molecules/Countdown/Countdown.stories.tsx +35 -0
- package/src/components/molecules/Countdown/Countdown.tsx +52 -10
- package/src/components/molecules/Countdown/index.ts +5 -1
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +14 -24
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +67 -26
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +1 -1
- package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +105 -36
- package/src/components/molecules/ProductDisplayCard/index.ts +1 -4
package/package.json
CHANGED
|
@@ -40,6 +40,11 @@ export default {
|
|
|
40
40
|
label: {
|
|
41
41
|
control: { type: "text" },
|
|
42
42
|
description: "Customizable prefix label (inline layout only)"
|
|
43
|
+
},
|
|
44
|
+
labels: {
|
|
45
|
+
control: { type: "object" },
|
|
46
|
+
description:
|
|
47
|
+
"Per-unit labels (days/hours/minutes/seconds). Pass translated strings; omitted units fall back to the English defaults."
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
}
|
|
@@ -139,6 +144,36 @@ export const WithoutSeconds = {
|
|
|
139
144
|
)
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
export const TranslatedLabels = {
|
|
148
|
+
name: "Translated Labels",
|
|
149
|
+
render: () => (
|
|
150
|
+
<View style={styles.column}>
|
|
151
|
+
<Countdown
|
|
152
|
+
layout="stacked"
|
|
153
|
+
labels={{
|
|
154
|
+
days: { singular: "jour", plural: "jours" },
|
|
155
|
+
hours: { singular: "heure", plural: "heures" },
|
|
156
|
+
minutes: { singular: "min", plural: "min" },
|
|
157
|
+
seconds: { singular: "sec", plural: "sec" }
|
|
158
|
+
}}
|
|
159
|
+
days={1}
|
|
160
|
+
hours={2}
|
|
161
|
+
minutes={48}
|
|
162
|
+
seconds={38}
|
|
163
|
+
/>
|
|
164
|
+
<Countdown
|
|
165
|
+
layout="inline"
|
|
166
|
+
label="La vente se termine dans :"
|
|
167
|
+
labels={{ days: "j", hours: "h", minutes: "min", seconds: "s" }}
|
|
168
|
+
days={2}
|
|
169
|
+
hours={9}
|
|
170
|
+
minutes={48}
|
|
171
|
+
seconds={38}
|
|
172
|
+
/>
|
|
173
|
+
</View>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
142
177
|
export const AllVariants = {
|
|
143
178
|
name: "All Variants",
|
|
144
179
|
render: () => (
|
|
@@ -12,6 +12,20 @@ type CountdownUnit = {
|
|
|
12
12
|
label: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A unit label. Pass a plain string to use it regardless of count, or a
|
|
17
|
+
* `{ singular, plural }` pair to pluralise based on the unit's value
|
|
18
|
+
* (`singular` when the value is exactly 1, `plural` otherwise).
|
|
19
|
+
*/
|
|
20
|
+
export type CountdownUnitLabel = string | { singular: string; plural: string }
|
|
21
|
+
|
|
22
|
+
export type CountdownLabels = {
|
|
23
|
+
days?: CountdownUnitLabel
|
|
24
|
+
hours?: CountdownUnitLabel
|
|
25
|
+
minutes?: CountdownUnitLabel
|
|
26
|
+
seconds?: CountdownUnitLabel
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
type CountdownOwnProps = {
|
|
16
30
|
layout?: CountdownLayout
|
|
17
31
|
days?: number
|
|
@@ -21,17 +35,25 @@ type CountdownOwnProps = {
|
|
|
21
35
|
showDays?: boolean
|
|
22
36
|
showSeconds?: boolean
|
|
23
37
|
label?: React.ReactNode
|
|
38
|
+
labels?: CountdownLabels
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export type CountdownProps = CountdownOwnProps &
|
|
27
42
|
Omit<ViewProps, keyof CountdownOwnProps>
|
|
28
43
|
|
|
29
|
-
const
|
|
30
|
-
days: "days",
|
|
31
|
-
hours: "hrs",
|
|
32
|
-
minutes: "min",
|
|
33
|
-
seconds: "secs"
|
|
34
|
-
}
|
|
44
|
+
const DEFAULT_UNIT_LABELS: Required<CountdownLabels> = {
|
|
45
|
+
days: { singular: "day", plural: "days" },
|
|
46
|
+
hours: { singular: "hr", plural: "hrs" },
|
|
47
|
+
minutes: { singular: "min", plural: "min" },
|
|
48
|
+
seconds: { singular: "sec", plural: "secs" }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolveLabel = (label: CountdownUnitLabel, value: number): string =>
|
|
52
|
+
typeof label === "string"
|
|
53
|
+
? label
|
|
54
|
+
: value === 1
|
|
55
|
+
? label.singular
|
|
56
|
+
: label.plural
|
|
35
57
|
|
|
36
58
|
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
37
59
|
|
|
@@ -154,6 +176,7 @@ const InlineCount = styled(View)<{ countGap: number }>(({ countGap }) => ({
|
|
|
154
176
|
* @param {boolean} [showDays=true] - Whether to render the days unit and its separator.
|
|
155
177
|
* @param {boolean} [showSeconds=true] - Whether to render the seconds unit and its separator.
|
|
156
178
|
* @param {React.ReactNode} [label] - Customizable prefix label, rendered in the `inline` layout only.
|
|
179
|
+
* @param {CountdownLabels} [labels] - Overrides the per-unit labels (`days`/`hours`/`minutes`/`seconds`). Each label is either a plain string or a `{ singular, plural }` pair that resolves against the unit's value. Pass translated strings here; any omitted unit falls back to its English default.
|
|
157
180
|
*
|
|
158
181
|
* @example
|
|
159
182
|
* ```tsx
|
|
@@ -161,6 +184,14 @@ const InlineCount = styled(View)<{ countGap: number }>(({ countGap }) => ({
|
|
|
161
184
|
*
|
|
162
185
|
* <Countdown layout="stacked" days={2} hours={9} minutes={48} seconds={38} />
|
|
163
186
|
* <Countdown layout="inline" label="Sale ends:" showDays={false} hours={3} minutes={49} seconds={38} />
|
|
187
|
+
* <Countdown
|
|
188
|
+
* labels={{
|
|
189
|
+
* days: { singular: "jour", plural: "jours" },
|
|
190
|
+
* hours: { singular: "heure", plural: "heures" }
|
|
191
|
+
* }}
|
|
192
|
+
* days={1}
|
|
193
|
+
* hours={2}
|
|
194
|
+
* />
|
|
164
195
|
* ```
|
|
165
196
|
*/
|
|
166
197
|
export const Countdown = React.forwardRef<View, CountdownProps>(
|
|
@@ -174,6 +205,7 @@ export const Countdown = React.forwardRef<View, CountdownProps>(
|
|
|
174
205
|
showDays = true,
|
|
175
206
|
showSeconds = true,
|
|
176
207
|
label,
|
|
208
|
+
labels,
|
|
177
209
|
...rest
|
|
178
210
|
},
|
|
179
211
|
ref
|
|
@@ -182,18 +214,28 @@ export const Countdown = React.forwardRef<View, CountdownProps>(
|
|
|
182
214
|
const { countdown } = theme.tokens.components
|
|
183
215
|
const { countdownItem } = countdown
|
|
184
216
|
|
|
217
|
+
const unitLabels = { ...DEFAULT_UNIT_LABELS, ...labels }
|
|
218
|
+
|
|
185
219
|
const units: CountdownUnit[] = [
|
|
186
220
|
showDays && {
|
|
187
221
|
key: "days" as const,
|
|
188
222
|
value: days,
|
|
189
|
-
label:
|
|
223
|
+
label: resolveLabel(unitLabels.days, days)
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: "hours" as const,
|
|
227
|
+
value: hours,
|
|
228
|
+
label: resolveLabel(unitLabels.hours, hours)
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
key: "minutes" as const,
|
|
232
|
+
value: minutes,
|
|
233
|
+
label: resolveLabel(unitLabels.minutes, minutes)
|
|
190
234
|
},
|
|
191
|
-
{ key: "hours" as const, value: hours, label: UNIT_LABELS.hours },
|
|
192
|
-
{ key: "minutes" as const, value: minutes, label: UNIT_LABELS.minutes },
|
|
193
235
|
showSeconds && {
|
|
194
236
|
key: "seconds" as const,
|
|
195
237
|
value: seconds,
|
|
196
|
-
label:
|
|
238
|
+
label: resolveLabel(unitLabels.seconds, seconds)
|
|
197
239
|
}
|
|
198
240
|
].filter(Boolean) as CountdownUnit[]
|
|
199
241
|
|
|
@@ -74,29 +74,20 @@ const StyledTextInput = styled(TextInput)<{
|
|
|
74
74
|
inputFontFamily: string
|
|
75
75
|
inputFontWeight?: FontWeight
|
|
76
76
|
inputFontSize: number
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
width: "100%",
|
|
92
|
-
height: "100%",
|
|
93
|
-
color: inputColor,
|
|
94
|
-
fontFamily: inputFontFamily,
|
|
95
|
-
...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
|
|
96
|
-
fontSize: inputFontSize,
|
|
97
|
-
lineHeight: inputLineHeight
|
|
98
|
-
})
|
|
99
|
-
)
|
|
77
|
+
}>(({ inputColor, inputFontFamily, inputFontWeight, inputFontSize }) => ({
|
|
78
|
+
textAlign: "center" as const,
|
|
79
|
+
outlineStyle: "none" as unknown as undefined,
|
|
80
|
+
padding: 0,
|
|
81
|
+
minWidth: 0,
|
|
82
|
+
minHeight: 0,
|
|
83
|
+
width: "100%",
|
|
84
|
+
height: "100%",
|
|
85
|
+
color: inputColor,
|
|
86
|
+
fontFamily: inputFontFamily,
|
|
87
|
+
...(inputFontWeight ? { fontWeight: inputFontWeight } : {}),
|
|
88
|
+
fontSize: inputFontSize,
|
|
89
|
+
lineHeight: 0
|
|
90
|
+
}))
|
|
100
91
|
|
|
101
92
|
const StyledRow = styled(View)<{
|
|
102
93
|
rowGap: number
|
|
@@ -256,7 +247,6 @@ export const NumberFieldInput = React.forwardRef<
|
|
|
256
247
|
: undefined
|
|
257
248
|
}
|
|
258
249
|
inputFontSize={parseTokenValue(typographyToken.fontSize)}
|
|
259
|
-
inputLineHeight={parseTokenValue(typographyToken.lineHeight)}
|
|
260
250
|
keyboardType="numeric"
|
|
261
251
|
editable={!disabled}
|
|
262
252
|
onFocus={handleFocus}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { View, StyleSheet
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
3
|
import { ProductDisplayCard } from "./ProductDisplayCard"
|
|
4
4
|
import type { ProductDisplayCardProps } from "./ProductDisplayCard"
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
<Image
|
|
8
|
-
source={{ uri: "https://placeholder.co/148x148?text=Recipe" }}
|
|
9
|
-
style={{ width: "100%", height: "100%" }}
|
|
10
|
-
/>
|
|
11
|
-
)
|
|
6
|
+
const placeholderImageUrl = "https://placeholder.co/100x100?text=Recipe"
|
|
12
7
|
|
|
13
8
|
const styles = StyleSheet.create({
|
|
14
9
|
column: { flexDirection: "column", gap: 24, padding: 16 }
|
|
@@ -43,6 +38,10 @@ export default {
|
|
|
43
38
|
control: { type: "boolean" },
|
|
44
39
|
description: "Show/hide quantity picker"
|
|
45
40
|
},
|
|
41
|
+
showImageQuantityBadge: {
|
|
42
|
+
control: { type: "boolean" },
|
|
43
|
+
description: "Show/hide quantity badge overlay on image"
|
|
44
|
+
},
|
|
46
45
|
disableQuantityButtons: {
|
|
47
46
|
control: { type: "boolean" },
|
|
48
47
|
description: "Disable quantity buttons"
|
|
@@ -111,20 +110,19 @@ Playground.args = {
|
|
|
111
110
|
bannerType: "info",
|
|
112
111
|
showBannerIcon: false,
|
|
113
112
|
banner: " High in protein • Natural ingredients • Vet approved",
|
|
114
|
-
image:
|
|
113
|
+
image: placeholderImageUrl
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
export const Default = () => (
|
|
118
117
|
<View style={styles.column}>
|
|
119
118
|
<ProductDisplayCard
|
|
120
|
-
device="mobile"
|
|
121
119
|
title="Recipe Name"
|
|
122
120
|
subtext="+£0.00/pouch"
|
|
123
121
|
showSubtext
|
|
124
122
|
quantity={1}
|
|
125
123
|
showQuantityPicker={false}
|
|
126
124
|
showBanner={false}
|
|
127
|
-
image={
|
|
125
|
+
image={placeholderImageUrl}
|
|
128
126
|
/>
|
|
129
127
|
</View>
|
|
130
128
|
)
|
|
@@ -132,14 +130,13 @@ export const Default = () => (
|
|
|
132
130
|
export const Desktop = () => (
|
|
133
131
|
<View style={styles.column}>
|
|
134
132
|
<ProductDisplayCard
|
|
135
|
-
device="desktop"
|
|
136
133
|
title="Recipe Name"
|
|
137
134
|
subtext="+£0.00/pouch"
|
|
138
135
|
showSubtext
|
|
139
136
|
quantity={1}
|
|
140
137
|
showQuantityPicker
|
|
141
138
|
showBanner={false}
|
|
142
|
-
image={
|
|
139
|
+
image={placeholderImageUrl}
|
|
143
140
|
/>
|
|
144
141
|
</View>
|
|
145
142
|
)
|
|
@@ -147,14 +144,13 @@ export const Desktop = () => (
|
|
|
147
144
|
export const Mobile = () => (
|
|
148
145
|
<View style={styles.column}>
|
|
149
146
|
<ProductDisplayCard
|
|
150
|
-
device="mobile"
|
|
151
147
|
title="Recipe Name"
|
|
152
148
|
subtext="+£0.50/pouch"
|
|
153
149
|
showSubtext
|
|
154
150
|
quantity={1}
|
|
155
151
|
showQuantityPicker
|
|
156
152
|
showBanner={false}
|
|
157
|
-
image={
|
|
153
|
+
image={placeholderImageUrl}
|
|
158
154
|
/>
|
|
159
155
|
</View>
|
|
160
156
|
)
|
|
@@ -162,7 +158,6 @@ export const Mobile = () => (
|
|
|
162
158
|
export const WithBanner = () => (
|
|
163
159
|
<View style={styles.column}>
|
|
164
160
|
<ProductDisplayCard
|
|
165
|
-
device="mobile"
|
|
166
161
|
title="Recipe Name"
|
|
167
162
|
subtext="+£0.00/pouch"
|
|
168
163
|
showSubtext
|
|
@@ -172,7 +167,7 @@ export const WithBanner = () => (
|
|
|
172
167
|
bannerType="success"
|
|
173
168
|
showBannerIcon={false}
|
|
174
169
|
banner="High in protein • Natural ingredients • Vet approved"
|
|
175
|
-
image={
|
|
170
|
+
image={placeholderImageUrl}
|
|
176
171
|
/>
|
|
177
172
|
</View>
|
|
178
173
|
)
|
|
@@ -180,7 +175,6 @@ export const WithBanner = () => (
|
|
|
180
175
|
export const WithBannerIcon = () => (
|
|
181
176
|
<View style={styles.column}>
|
|
182
177
|
<ProductDisplayCard
|
|
183
|
-
device="mobile"
|
|
184
178
|
title="Recipe Name"
|
|
185
179
|
subtext="+£0.00/pouch"
|
|
186
180
|
showSubtext
|
|
@@ -190,7 +184,7 @@ export const WithBannerIcon = () => (
|
|
|
190
184
|
bannerType="success"
|
|
191
185
|
showBannerIcon
|
|
192
186
|
banner="High in protein • Natural ingredients • Vet approved"
|
|
193
|
-
image={
|
|
187
|
+
image={placeholderImageUrl}
|
|
194
188
|
/>
|
|
195
189
|
</View>
|
|
196
190
|
)
|
|
@@ -198,13 +192,38 @@ export const WithBannerIcon = () => (
|
|
|
198
192
|
export const NoSubtext = () => (
|
|
199
193
|
<View style={styles.column}>
|
|
200
194
|
<ProductDisplayCard
|
|
201
|
-
device="mobile"
|
|
202
195
|
title="Recipe Name"
|
|
203
196
|
showSubtext={false}
|
|
204
197
|
quantity={1}
|
|
205
198
|
showQuantityPicker
|
|
206
199
|
showBanner={false}
|
|
207
|
-
image={
|
|
200
|
+
image={placeholderImageUrl}
|
|
201
|
+
/>
|
|
202
|
+
</View>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
export const WithImageQuantityBadge = () => (
|
|
206
|
+
<View style={styles.column}>
|
|
207
|
+
<ProductDisplayCard
|
|
208
|
+
title="Gobble Gobble Turkey"
|
|
209
|
+
subtext="+£0.50/pouch"
|
|
210
|
+
showSubtext
|
|
211
|
+
quantity={4}
|
|
212
|
+
showQuantityPicker={false}
|
|
213
|
+
showImageQuantityBadge
|
|
214
|
+
showBanner={false}
|
|
215
|
+
image={placeholderImageUrl}
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
<ProductDisplayCard
|
|
219
|
+
title="Salmon Delight"
|
|
220
|
+
subtext="+£0.25/pouch"
|
|
221
|
+
showSubtext
|
|
222
|
+
quantity={2}
|
|
223
|
+
showQuantityPicker
|
|
224
|
+
showImageQuantityBadge
|
|
225
|
+
showBanner={false}
|
|
226
|
+
image={placeholderImageUrl}
|
|
208
227
|
/>
|
|
209
228
|
</View>
|
|
210
229
|
)
|
|
@@ -212,29 +231,26 @@ export const NoSubtext = () => (
|
|
|
212
231
|
export const AllVariants = () => (
|
|
213
232
|
<View style={styles.column}>
|
|
214
233
|
<ProductDisplayCard
|
|
215
|
-
device="desktop"
|
|
216
234
|
title="Recipe Name"
|
|
217
235
|
subtext="+£0.00/pouch"
|
|
218
236
|
showSubtext
|
|
219
237
|
quantity={1}
|
|
220
238
|
showQuantityPicker
|
|
221
239
|
showBanner={false}
|
|
222
|
-
image={
|
|
240
|
+
image={placeholderImageUrl}
|
|
223
241
|
/>
|
|
224
242
|
|
|
225
243
|
<ProductDisplayCard
|
|
226
|
-
device="mobile"
|
|
227
244
|
title="Recipe Name"
|
|
228
245
|
subtext="+£0.50/pouch"
|
|
229
246
|
showSubtext
|
|
230
247
|
quantity={2}
|
|
231
248
|
showQuantityPicker
|
|
232
249
|
showBanner={false}
|
|
233
|
-
image={
|
|
250
|
+
image={placeholderImageUrl}
|
|
234
251
|
/>
|
|
235
252
|
|
|
236
253
|
<ProductDisplayCard
|
|
237
|
-
device="mobile"
|
|
238
254
|
title="Recipe Name"
|
|
239
255
|
subtext="+£0.00/pouch"
|
|
240
256
|
showSubtext
|
|
@@ -242,7 +258,32 @@ export const AllVariants = () => (
|
|
|
242
258
|
showQuantityPicker
|
|
243
259
|
showBanner
|
|
244
260
|
banner=" High in protein • Natural ingredients"
|
|
245
|
-
image={
|
|
261
|
+
image={placeholderImageUrl}
|
|
262
|
+
/>
|
|
263
|
+
</View>
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
export const Borderless = () => (
|
|
267
|
+
<View style={styles.column}>
|
|
268
|
+
<ProductDisplayCard
|
|
269
|
+
title="Recipe Name"
|
|
270
|
+
subtext="+£0.00/pouch"
|
|
271
|
+
showSubtext
|
|
272
|
+
quantity={1}
|
|
273
|
+
showQuantityPicker
|
|
274
|
+
borderless
|
|
275
|
+
image={placeholderImageUrl}
|
|
276
|
+
/>
|
|
277
|
+
|
|
278
|
+
<ProductDisplayCard
|
|
279
|
+
title="Premium Recipe"
|
|
280
|
+
subtext="+£0.50/pouch"
|
|
281
|
+
showSubtext
|
|
282
|
+
quantity={2}
|
|
283
|
+
showQuantityPicker
|
|
284
|
+
showImageQuantityBadge
|
|
285
|
+
borderless
|
|
286
|
+
image={placeholderImageUrl}
|
|
246
287
|
/>
|
|
247
288
|
</View>
|
|
248
289
|
)
|
|
@@ -102,7 +102,7 @@ describe("ProductDisplayCard (Native)", () => {
|
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
it("renders with mobile device variant when specified", () => {
|
|
105
|
-
renderWithTheme(<ProductDisplayCard title="Test Product"
|
|
105
|
+
renderWithTheme(<ProductDisplayCard title="Test Product" />)
|
|
106
106
|
|
|
107
107
|
expect(screen.getByText("Test Product")).toBeTruthy()
|
|
108
108
|
})
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { View, ViewProps } from "react-native"
|
|
2
|
+
import { View, ViewProps, Image } from "react-native"
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { Typography } from "../../atoms/Typography"
|
|
5
5
|
import { NumberField } from "../../molecules/NumberField"
|
|
6
6
|
import { Notification } from "../Notification"
|
|
7
7
|
import { parseTokenValue } from "../../../utils"
|
|
8
8
|
|
|
9
|
-
export type ProductDisplayCardDevice = "desktop" | "mobile"
|
|
10
|
-
|
|
11
9
|
export type ProductDisplayCardProps = ViewProps & {
|
|
12
|
-
device?: ProductDisplayCardDevice
|
|
13
10
|
title: string
|
|
14
|
-
subtext?: string
|
|
11
|
+
subtext?: string | React.ReactNode
|
|
15
12
|
showSubtext?: boolean
|
|
16
13
|
quantity?: number
|
|
17
14
|
onQuantityChange?: (quantity: number) => void
|
|
@@ -22,16 +19,22 @@ export type ProductDisplayCardProps = ViewProps & {
|
|
|
22
19
|
showDecrementButton?: boolean
|
|
23
20
|
incrementDisabled?: boolean
|
|
24
21
|
decrementDisabled?: boolean
|
|
25
|
-
image?: React.ReactNode
|
|
22
|
+
image?: string | React.ReactNode
|
|
23
|
+
imageBackgroundColor?: string
|
|
24
|
+
thumbnailWidth?: number
|
|
25
|
+
thumbnailBackgroundColor?: string
|
|
26
|
+
showImageQuantityBadge?: boolean
|
|
27
|
+
borderless?: boolean
|
|
26
28
|
banner?: React.ReactNode
|
|
27
29
|
showBanner?: boolean
|
|
28
30
|
bannerType?: "error" | "success" | "warning" | "info"
|
|
29
31
|
showBannerIcon?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
const StyledCardContainer = styled(View)<{
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const StyledCardContainer = styled(View)<{ borderless?: boolean }>(({
|
|
35
|
+
theme,
|
|
36
|
+
borderless
|
|
37
|
+
}) => {
|
|
35
38
|
const { spacing, borderRadius } = theme.tokens.semantics.dimensions
|
|
36
39
|
const { colour } = theme.tokens.semantics
|
|
37
40
|
const spacingMd = parseTokenValue(spacing.md)
|
|
@@ -39,38 +42,80 @@ const StyledCardContainer = styled(View)<{
|
|
|
39
42
|
return {
|
|
40
43
|
flexDirection: "row",
|
|
41
44
|
width: "100%",
|
|
42
|
-
height: "100%",
|
|
45
|
+
height: borderless ? undefined : "100%",
|
|
43
46
|
maxHeight: spacingXl * 6 + spacingMd,
|
|
44
47
|
backgroundColor: colour.background.surface.default,
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
...(borderless
|
|
49
|
+
? {}
|
|
50
|
+
: {
|
|
51
|
+
borderWidth: 1,
|
|
52
|
+
borderColor: colour.border.default,
|
|
53
|
+
shadowColor: "#522a10",
|
|
54
|
+
shadowOffset: { width: 0, height: spacingMd / 4 },
|
|
55
|
+
shadowOpacity: 0.05,
|
|
56
|
+
shadowRadius: spacingXl - 4,
|
|
57
|
+
elevation: 5
|
|
58
|
+
}),
|
|
47
59
|
borderRadius: parseTokenValue(borderRadius.md),
|
|
48
60
|
overflow: "hidden",
|
|
49
|
-
shadowColor: "#522a10",
|
|
50
|
-
shadowOffset: { width: 0, height: spacingMd / 4 },
|
|
51
|
-
shadowOpacity: 0.05,
|
|
52
|
-
shadowRadius: spacingXl - 4,
|
|
53
|
-
elevation: 5,
|
|
54
61
|
zIndex: 1
|
|
55
62
|
}
|
|
56
63
|
})
|
|
57
64
|
|
|
58
|
-
const StyledImage = styled(View)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const StyledImage = styled(View)<{
|
|
66
|
+
imageBackgroundColor?: string
|
|
67
|
+
thumbnailWidth?: number
|
|
68
|
+
thumbnailBackgroundColor?: string
|
|
69
|
+
}>(
|
|
70
|
+
({
|
|
71
|
+
theme,
|
|
72
|
+
imageBackgroundColor,
|
|
73
|
+
thumbnailWidth = 100,
|
|
74
|
+
thumbnailBackgroundColor
|
|
75
|
+
}) => ({
|
|
76
|
+
flexShrink: 0,
|
|
77
|
+
backgroundColor:
|
|
78
|
+
thumbnailBackgroundColor ||
|
|
79
|
+
imageBackgroundColor ||
|
|
80
|
+
theme.tokens.semantics.colour.background.container.secondary,
|
|
81
|
+
overflow: "hidden",
|
|
82
|
+
aspectRatio: "1 / 1",
|
|
83
|
+
width: thumbnailWidth,
|
|
84
|
+
height: thumbnailWidth,
|
|
85
|
+
position: "relative"
|
|
86
|
+
})
|
|
87
|
+
)
|
|
65
88
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
const StyledQuantityBadge = styled(View)(({ theme }) => {
|
|
90
|
+
const spacing = theme.tokens.semantics.dimensions.spacing
|
|
91
|
+
const borderRadius = theme.tokens.semantics.dimensions.borderRadius
|
|
92
|
+
const { colour } = theme.tokens.semantics
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
position: "absolute",
|
|
96
|
+
top: parseTokenValue(spacing["2xs"]),
|
|
97
|
+
right: parseTokenValue(spacing["2xs"]),
|
|
98
|
+
backgroundColor: colour.background.container.brand,
|
|
99
|
+
borderRadius: parseTokenValue(borderRadius.xs),
|
|
100
|
+
paddingHorizontal: parseTokenValue(spacing.sm),
|
|
101
|
+
height: parseTokenValue(spacing["2xl"]),
|
|
102
|
+
minWidth: parseTokenValue(spacing["2xl"]),
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
zIndex: 2
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const StyledContentArea = styled(View)<{ borderless?: boolean }>(({
|
|
110
|
+
theme,
|
|
111
|
+
borderless
|
|
112
|
+
}) => {
|
|
69
113
|
const { spacing } = theme.tokens.semantics.dimensions
|
|
114
|
+
const paddingValue = parseTokenValue(spacing.md)
|
|
70
115
|
return {
|
|
71
116
|
flex: 1,
|
|
72
|
-
paddingHorizontal:
|
|
73
|
-
paddingVertical:
|
|
117
|
+
paddingHorizontal: paddingValue,
|
|
118
|
+
paddingVertical: borderless ? 0 : paddingValue,
|
|
74
119
|
gap: parseTokenValue(spacing.xs),
|
|
75
120
|
justifyContent: "space-between"
|
|
76
121
|
}
|
|
@@ -112,7 +157,6 @@ const StyledNotification = styled(Notification as any)(({ theme }) => {
|
|
|
112
157
|
* import { ProductDisplayCard } from "@butternutbox/pawprint-native"
|
|
113
158
|
*
|
|
114
159
|
* <ProductDisplayCard
|
|
115
|
-
* device="mobile"
|
|
116
160
|
* title="Premium Recipe"
|
|
117
161
|
* subtext="+£0.00/pouch"
|
|
118
162
|
* quantity={1}
|
|
@@ -120,7 +164,6 @@ const StyledNotification = styled(Notification as any)(({ theme }) => {
|
|
|
120
164
|
* />
|
|
121
165
|
* ```
|
|
122
166
|
*
|
|
123
|
-
* @param device - *(optional)* Device variant: "desktop" or "mobile" (default)
|
|
124
167
|
* @param title - Product title
|
|
125
168
|
* @param subtext - *(optional)* Subtitle text (e.g. price)
|
|
126
169
|
* @param showSubtext - *(optional)* Show/hide subtext
|
|
@@ -139,7 +182,6 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
139
182
|
>(
|
|
140
183
|
(
|
|
141
184
|
{
|
|
142
|
-
device = "mobile",
|
|
143
185
|
title,
|
|
144
186
|
subtext,
|
|
145
187
|
showSubtext = true,
|
|
@@ -153,6 +195,11 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
153
195
|
incrementDisabled = false,
|
|
154
196
|
decrementDisabled = false,
|
|
155
197
|
image,
|
|
198
|
+
imageBackgroundColor,
|
|
199
|
+
thumbnailWidth,
|
|
200
|
+
thumbnailBackgroundColor,
|
|
201
|
+
showImageQuantityBadge = false,
|
|
202
|
+
borderless = false,
|
|
156
203
|
banner,
|
|
157
204
|
showBanner = false,
|
|
158
205
|
bannerType = "info",
|
|
@@ -168,12 +215,34 @@ export const ProductDisplayCard = React.forwardRef<
|
|
|
168
215
|
}
|
|
169
216
|
|
|
170
217
|
const cardContent = (
|
|
171
|
-
<StyledCardContainer ref={ref}
|
|
172
|
-
{image &&
|
|
218
|
+
<StyledCardContainer ref={ref} borderless={borderless} {...rest}>
|
|
219
|
+
{image && (
|
|
220
|
+
<StyledImage
|
|
221
|
+
imageBackgroundColor={imageBackgroundColor}
|
|
222
|
+
thumbnailWidth={thumbnailWidth}
|
|
223
|
+
thumbnailBackgroundColor={thumbnailBackgroundColor}
|
|
224
|
+
>
|
|
225
|
+
{typeof image === "string" ? (
|
|
226
|
+
<Image
|
|
227
|
+
source={{ uri: image }}
|
|
228
|
+
style={{ width: "100%", height: "100%" }}
|
|
229
|
+
/>
|
|
230
|
+
) : (
|
|
231
|
+
image
|
|
232
|
+
)}
|
|
233
|
+
{showImageQuantityBadge && quantity && (
|
|
234
|
+
<StyledQuantityBadge>
|
|
235
|
+
<Typography variant="body" size="sm" weight="bold">
|
|
236
|
+
{quantity}
|
|
237
|
+
</Typography>
|
|
238
|
+
</StyledQuantityBadge>
|
|
239
|
+
)}
|
|
240
|
+
</StyledImage>
|
|
241
|
+
)}
|
|
173
242
|
|
|
174
|
-
<StyledContentArea
|
|
243
|
+
<StyledContentArea borderless={borderless}>
|
|
175
244
|
<StyledTextContainer>
|
|
176
|
-
<Typography variant="heading" size="
|
|
245
|
+
<Typography variant="heading" size="2xs" color="primary">
|
|
177
246
|
{title}
|
|
178
247
|
</Typography>
|
|
179
248
|
|