@butternutbox/pawprint-native 0.8.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -3,7 +3,8 @@ import { View, StyleSheet } from "react-native"
3
3
  import { ProductDisplayCard } from "./ProductDisplayCard"
4
4
  import type { ProductDisplayCardProps } from "./ProductDisplayCard"
5
5
 
6
- const placeholderImageUrl = "https://placeholder.co/100x100?text=Recipe"
6
+ const placeholderImageUrl =
7
+ "https://placehold.co/148x148/EEE/31343C?font=oswald&text=product"
7
8
 
8
9
  const styles = StyleSheet.create({
9
10
  column: { flexDirection: "column", gap: 24, padding: 16 }
@@ -13,11 +14,6 @@ export default {
13
14
  title: "Molecules/ProductDisplayCard",
14
15
  component: ProductDisplayCard,
15
16
  argTypes: {
16
- device: {
17
- control: { type: "select" },
18
- options: ["desktop", "mobile"],
19
- description: "Device variant affecting sizing and typography"
20
- },
21
17
  title: {
22
18
  control: { type: "text" },
23
19
  description: "Product title"
@@ -38,10 +34,6 @@ export default {
38
34
  control: { type: "boolean" },
39
35
  description: "Show/hide quantity picker"
40
36
  },
41
- showImageQuantityBadge: {
42
- control: { type: "boolean" },
43
- description: "Show/hide quantity badge overlay on image"
44
- },
45
37
  disableQuantityButtons: {
46
38
  control: { type: "boolean" },
47
39
  description: "Disable quantity buttons"
@@ -50,14 +42,6 @@ export default {
50
42
  control: { type: "boolean" },
51
43
  description: "Hide quantity buttons"
52
44
  },
53
- showIncrementButton: {
54
- control: { type: "boolean" },
55
- description: "Show/hide increment button"
56
- },
57
- showDecrementButton: {
58
- control: { type: "boolean" },
59
- description: "Show/hide decrement button"
60
- },
61
45
  incrementDisabled: {
62
46
  control: { type: "boolean" },
63
47
  description: "Disable increment button"
@@ -100,7 +84,6 @@ export const Playground = (args: ProductDisplayCardProps) => (
100
84
  </View>
101
85
  )
102
86
  Playground.args = {
103
- device: "mobile",
104
87
  title: "Recipe Name",
105
88
  subtext: "+£0.00/pouch",
106
89
  showSubtext: true,
@@ -127,20 +110,6 @@ export const Default = () => (
127
110
  </View>
128
111
  )
129
112
 
130
- export const Desktop = () => (
131
- <View style={styles.column}>
132
- <ProductDisplayCard
133
- title="Recipe Name"
134
- subtext="+£0.00/pouch"
135
- showSubtext
136
- quantity={1}
137
- showQuantityPicker
138
- showBanner={false}
139
- image={placeholderImageUrl}
140
- />
141
- </View>
142
- )
143
-
144
113
  export const Mobile = () => (
145
114
  <View style={styles.column}>
146
115
  <ProductDisplayCard
@@ -202,32 +171,6 @@ export const NoSubtext = () => (
202
171
  </View>
203
172
  )
204
173
 
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}
227
- />
228
- </View>
229
- )
230
-
231
174
  export const AllVariants = () => (
232
175
  <View style={styles.column}>
233
176
  <ProductDisplayCard
@@ -262,28 +205,3 @@ export const AllVariants = () => (
262
205
  />
263
206
  </View>
264
207
  )
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}
287
- />
288
- </View>
289
- )
@@ -95,13 +95,7 @@ describe("ProductDisplayCard (Native)", () => {
95
95
  expect(screen.queryByText("Test Banner")).toBeFalsy()
96
96
  })
97
97
 
98
- it("renders with desktop device variant by default", () => {
99
- renderWithTheme(<ProductDisplayCard title="Test Product" />)
100
-
101
- expect(screen.getByText("Test Product")).toBeTruthy()
102
- })
103
-
104
- it("renders with mobile device variant when specified", () => {
98
+ it("renders as a mobile card", () => {
105
99
  renderWithTheme(<ProductDisplayCard title="Test Product" />)
106
100
 
107
101
  expect(screen.getByText("Test Product")).toBeTruthy()
@@ -15,26 +15,18 @@ export type ProductDisplayCardProps = ViewProps & {
15
15
  showQuantityPicker?: boolean
16
16
  disableQuantityButtons?: boolean
17
17
  hideQuantityButtons?: boolean
18
- showIncrementButton?: boolean
19
- showDecrementButton?: boolean
20
18
  incrementDisabled?: boolean
21
19
  decrementDisabled?: boolean
22
20
  image?: string | React.ReactNode
23
21
  imageBackgroundColor?: string
24
22
  thumbnailWidth?: number
25
- thumbnailBackgroundColor?: string
26
- showImageQuantityBadge?: boolean
27
- borderless?: boolean
28
23
  banner?: React.ReactNode
29
24
  showBanner?: boolean
30
25
  bannerType?: "error" | "success" | "warning" | "info"
31
26
  showBannerIcon?: boolean
32
27
  }
33
28
 
34
- const StyledCardContainer = styled(View)<{ borderless?: boolean }>(({
35
- theme,
36
- borderless
37
- }) => {
29
+ const StyledCardContainer = styled(View)(({ theme }) => {
38
30
  const { spacing, borderRadius } = theme.tokens.semantics.dimensions
39
31
  const { colour } = theme.tokens.semantics
40
32
  const spacingMd = parseTokenValue(spacing.md)
@@ -42,20 +34,20 @@ const StyledCardContainer = styled(View)<{ borderless?: boolean }>(({
42
34
  return {
43
35
  flexDirection: "row",
44
36
  width: "100%",
45
- height: borderless ? undefined : "100%",
46
- maxHeight: spacingXl * 6 + spacingMd,
37
+ // Cap at the Figma mobile card width (can be narrower — responsive).
38
+ maxWidth: 358,
39
+ // The 148px image sets the baseline height; the card grows if the content
40
+ // is taller (e.g. a long or translated title wrapping to multiple lines)
41
+ // so nothing is clipped.
42
+ minHeight: 148,
47
43
  backgroundColor: colour.background.surface.default,
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
- }),
44
+ borderWidth: 1,
45
+ borderColor: colour.border.default,
46
+ shadowColor: "#522a10",
47
+ shadowOffset: { width: 0, height: spacingMd / 4 },
48
+ shadowOpacity: 0.05,
49
+ shadowRadius: spacingXl - 4,
50
+ elevation: 5,
59
51
  borderRadius: parseTokenValue(borderRadius.md),
60
52
  overflow: "hidden",
61
53
  zIndex: 1
@@ -65,57 +57,26 @@ const StyledCardContainer = styled(View)<{ borderless?: boolean }>(({
65
57
  const StyledImage = styled(View)<{
66
58
  imageBackgroundColor?: string
67
59
  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
- )
88
-
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
- })
60
+ }>(({ theme, imageBackgroundColor, thumbnailWidth = 148 }) => ({
61
+ flexShrink: 0,
62
+ backgroundColor:
63
+ imageBackgroundColor ||
64
+ theme.tokens.semantics.colour.background.container.secondary,
65
+ overflow: "hidden",
66
+ width: thumbnailWidth,
67
+ // Square (148) by default; stretches taller to fill the card when the
68
+ // content makes it taller than the image.
69
+ minHeight: thumbnailWidth,
70
+ position: "relative"
71
+ }))
108
72
 
109
- const StyledContentArea = styled(View)<{ borderless?: boolean }>(({
110
- theme,
111
- borderless
112
- }) => {
73
+ const StyledContentArea = styled(View)(({ theme }) => {
113
74
  const { spacing } = theme.tokens.semantics.dimensions
114
75
  const paddingValue = parseTokenValue(spacing.md)
115
76
  return {
116
77
  flex: 1,
117
78
  paddingHorizontal: paddingValue,
118
- paddingVertical: borderless ? 0 : paddingValue,
79
+ paddingVertical: paddingValue,
119
80
  gap: parseTokenValue(spacing.xs),
120
81
  justifyContent: "space-between"
121
82
  }
@@ -125,27 +86,40 @@ const StyledTextContainer = styled(View)(() => ({
125
86
  gap: 2
126
87
  }))
127
88
 
128
- const StyledBannerWrapper = styled(View)(() => ({
129
- width: "100%",
130
- marginTop: -48,
131
- paddingTop: 24,
132
- paddingBottom: 16
133
- }))
89
+ const StyledBannerWrapper = styled(View)(({ theme }) => {
90
+ const { spacing, borderRadius } = theme.tokens.semantics.dimensions
91
+ return {
92
+ width: "100%",
93
+ // Tuck the banner up behind the card by the corner radius so its square top
94
+ // corners hide under the card's rounded bottom; the rest sits below.
95
+ marginTop: -parseTokenValue(borderRadius.md),
96
+ paddingBottom: parseTokenValue(spacing.md)
97
+ }
98
+ })
134
99
 
135
100
  const StyledRootWrapper = styled(View)(() => {
136
101
  return {
137
102
  flexDirection: "column",
138
103
  width: "100%",
139
- height: "100%"
104
+ maxWidth: 358
140
105
  }
141
106
  })
142
107
 
143
108
  const StyledNotification = styled(Notification as any)(({ theme }) => {
144
- const { spacing } = theme.tokens.semantics.dimensions
109
+ const { spacing, borderRadius } = theme.tokens.semantics.dimensions
110
+ const spacingMd = parseTokenValue(spacing.md)
111
+ const radiusMd = parseTokenValue(borderRadius.md)
145
112
  return {
146
- top: parseTokenValue(spacing.md),
147
- paddingHorizontal: parseTokenValue(spacing.xl),
148
- paddingVertical: parseTokenValue(spacing.md)
113
+ paddingHorizontal: spacingMd,
114
+ // 27px top padding (Figma) leaves a small cream reveal above the text once
115
+ // the banner tucks behind the card.
116
+ paddingTop: 27,
117
+ paddingBottom: spacingMd,
118
+ // Square top corners, rounded bottom corners (banner tucks under the card).
119
+ borderTopLeftRadius: 0,
120
+ borderTopRightRadius: 0,
121
+ borderBottomLeftRadius: radiusMd,
122
+ borderBottomRightRadius: radiusMd
149
123
  }
150
124
  })
151
125
 
@@ -190,16 +164,11 @@ export const ProductDisplayCard = React.forwardRef<
190
164
  showQuantityPicker = false,
191
165
  disableQuantityButtons = false,
192
166
  hideQuantityButtons = false,
193
- showIncrementButton,
194
- showDecrementButton,
195
167
  incrementDisabled = false,
196
168
  decrementDisabled = false,
197
169
  image,
198
170
  imageBackgroundColor,
199
171
  thumbnailWidth,
200
- thumbnailBackgroundColor,
201
- showImageQuantityBadge = false,
202
- borderless = false,
203
172
  banner,
204
173
  showBanner = false,
205
174
  bannerType = "info",
@@ -215,32 +184,25 @@ export const ProductDisplayCard = React.forwardRef<
215
184
  }
216
185
 
217
186
  const cardContent = (
218
- <StyledCardContainer ref={ref} borderless={borderless} {...rest}>
187
+ <StyledCardContainer ref={ref} {...rest}>
219
188
  {image && (
220
189
  <StyledImage
221
190
  imageBackgroundColor={imageBackgroundColor}
222
191
  thumbnailWidth={thumbnailWidth}
223
- thumbnailBackgroundColor={thumbnailBackgroundColor}
224
192
  >
225
193
  {typeof image === "string" ? (
226
194
  <Image
227
195
  source={{ uri: image }}
228
- style={{ width: "100%", height: "100%" }}
196
+ resizeMode="cover"
197
+ style={{ flex: 1, width: "100%" }}
229
198
  />
230
199
  ) : (
231
200
  image
232
201
  )}
233
- {showImageQuantityBadge && quantity && (
234
- <StyledQuantityBadge>
235
- <Typography variant="body" size="sm" weight="bold">
236
- {quantity}
237
- </Typography>
238
- </StyledQuantityBadge>
239
- )}
240
202
  </StyledImage>
241
203
  )}
242
204
 
243
- <StyledContentArea borderless={borderless}>
205
+ <StyledContentArea>
244
206
  <StyledTextContainer>
245
207
  <Typography variant="heading" size="2xs" color="primary">
246
208
  {title}
@@ -269,16 +231,8 @@ export const ProductDisplayCard = React.forwardRef<
269
231
  min={1}
270
232
  accessible
271
233
  accessibilityLabel={`Quantity: ${quantity}`}
272
- showIncrementButton={
273
- showIncrementButton !== undefined
274
- ? showIncrementButton
275
- : !hideQuantityButtons
276
- }
277
- showDecrementButton={
278
- showDecrementButton !== undefined
279
- ? showDecrementButton
280
- : !hideQuantityButtons
281
- }
234
+ showIncrementButton={!hideQuantityButtons}
235
+ showDecrementButton={!hideQuantityButtons}
282
236
  incrementDisabled={incrementDisabled || disableQuantityButtons}
283
237
  decrementDisabled={decrementDisabled || disableQuantityButtons}
284
238
  />
@@ -27,8 +27,8 @@ const BadgeContainer = styled(View)(({ theme }) => {
27
27
  alignItems: "center",
28
28
  justifyContent: "center",
29
29
  height: parseTokenValue(sizing["2xl"]),
30
- paddingRight: parseTokenValue(spacing.sm),
31
- backgroundColor: colour.background.container.alt,
30
+ paddingRight: parseTokenValue(spacing.xs),
31
+ backgroundColor: colour.background.container.brand,
32
32
  borderTopRightRadius: parseTokenValue(borderRadius.xs),
33
33
  borderBottomRightRadius: parseTokenValue(borderRadius.xs)
34
34
  }
@@ -48,12 +48,15 @@ const StyledRoot = styled(Pressable)(({ theme }) => {
48
48
  }
49
49
  })
50
50
 
51
- const ImageContainer = styled(View)({
51
+ const ImageContainer = styled(View)(({ theme }) => ({
52
52
  position: "relative",
53
53
  width: "100%",
54
54
  aspectRatio: 1,
55
- overflow: "hidden"
56
- })
55
+ overflow: "hidden",
56
+ borderRadius: parseTokenValue(
57
+ theme.tokens.semantics.dimensions.borderRadius.md
58
+ )
59
+ }))
57
60
 
58
61
  const StyledImage = styled(Image)({
59
62
  width: "100%",
@@ -61,27 +64,13 @@ const StyledImage = styled(Image)({
61
64
  resizeMode: "cover"
62
65
  })
63
66
 
64
- const BadgeContainer = styled(View)(({ theme }) => {
65
- const spacing = theme.tokens.semantics.dimensions.spacing
66
- const { sizing, borderRadius } = theme.tokens.semantics.dimensions
67
- const { colour } = theme.tokens.semantics
68
-
69
- return {
70
- position: "absolute",
71
- top: parseTokenValue(spacing.md),
72
- left: 0,
73
- display: "flex",
74
- flexDirection: "row",
75
- gap: parseTokenValue(spacing["2xs"]),
76
- alignItems: "center",
77
- justifyContent: "center",
78
- height: parseTokenValue(sizing["2xl"]),
79
- paddingRight: parseTokenValue(spacing.sm),
80
- backgroundColor: colour.background.container.alt,
81
- borderTopRightRadius: parseTokenValue(borderRadius.sm),
82
- borderBottomRightRadius: parseTokenValue(borderRadius.sm)
83
- }
84
- })
67
+ // Positioning only — the visual badge (background, padding, radius) is the
68
+ // `Badge` component rendered inside, so it isn't double-wrapped.
69
+ const BadgeContainer = styled(View)(({ theme }) => ({
70
+ position: "absolute",
71
+ top: parseTokenValue(theme.tokens.semantics.dimensions.spacing.md),
72
+ left: 0
73
+ }))
85
74
 
86
75
  const ContentContainer = styled(View)(({ theme }) => {
87
76
  const spacing = theme.tokens.semantics.dimensions.spacing
@@ -110,23 +99,12 @@ const CategoryWrapper = styled(View)(({ theme }) => {
110
99
  }
111
100
  })
112
101
 
113
- const TitleContainer = styled(View)(({ theme }) => {
114
- const spacing = theme.tokens.semantics.dimensions.spacing
115
-
116
- return {
117
- display: "flex",
118
- flexDirection: "row",
119
- width: "100%",
120
- marginBottom: parseTokenValue(spacing.xs)
121
- }
122
- })
123
-
124
- const SizeWrapper = styled(View)(({ theme }) => {
125
- const spacing = theme.tokens.semantics.dimensions.spacing
126
-
127
- return {
128
- marginBottom: parseTokenValue(spacing.md)
129
- }
102
+ const TitleContainer = styled(View)({
103
+ display: "flex",
104
+ flexDirection: "row",
105
+ width: "100%",
106
+ // 6px below the title to match Figma (4px title bottom padding + 2px gap).
107
+ marginBottom: 6
130
108
  })
131
109
 
132
110
  const PricingContainer = styled(View)(({ theme }) => {
@@ -194,11 +172,9 @@ const _ProductListingCard = React.forwardRef<View, ProductListingCardProps>(
194
172
  </TitleContainer>
195
173
  )}
196
174
  {showSize && size && (
197
- <SizeWrapper>
198
- <Typography variant="body" size="md" color="secondary">
199
- {size}
200
- </Typography>
201
- </SizeWrapper>
175
+ <Typography variant="body" size="md" color="secondary">
176
+ {size}
177
+ </Typography>
202
178
  )}
203
179
  </DetailsContainer>
204
180