@butternutbox/pawprint-native 0.4.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -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 UNIT_LABELS = {
30
- days: "days",
31
- hours: "hrs",
32
- minutes: "min",
33
- seconds: "secs"
34
- } as const
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: UNIT_LABELS.days
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: UNIT_LABELS.seconds
238
+ label: resolveLabel(unitLabels.seconds, seconds)
197
239
  }
198
240
  ].filter(Boolean) as CountdownUnit[]
199
241
 
@@ -1,2 +1,6 @@
1
1
  export { Countdown } from "./Countdown"
2
- export type { CountdownProps } from "./Countdown"
2
+ export type {
3
+ CountdownProps,
4
+ CountdownLabels,
5
+ CountdownUnitLabel
6
+ } from "./Countdown"
@@ -116,7 +116,6 @@ Playground.args = {
116
116
  export const Default = () => (
117
117
  <View style={styles.column}>
118
118
  <ProductDisplayCard
119
- device="mobile"
120
119
  title="Recipe Name"
121
120
  subtext="+£0.00/pouch"
122
121
  showSubtext
@@ -131,7 +130,6 @@ export const Default = () => (
131
130
  export const Desktop = () => (
132
131
  <View style={styles.column}>
133
132
  <ProductDisplayCard
134
- device="desktop"
135
133
  title="Recipe Name"
136
134
  subtext="+£0.00/pouch"
137
135
  showSubtext
@@ -146,7 +144,6 @@ export const Desktop = () => (
146
144
  export const Mobile = () => (
147
145
  <View style={styles.column}>
148
146
  <ProductDisplayCard
149
- device="mobile"
150
147
  title="Recipe Name"
151
148
  subtext="+£0.50/pouch"
152
149
  showSubtext
@@ -161,7 +158,6 @@ export const Mobile = () => (
161
158
  export const WithBanner = () => (
162
159
  <View style={styles.column}>
163
160
  <ProductDisplayCard
164
- device="mobile"
165
161
  title="Recipe Name"
166
162
  subtext="+£0.00/pouch"
167
163
  showSubtext
@@ -179,7 +175,6 @@ export const WithBanner = () => (
179
175
  export const WithBannerIcon = () => (
180
176
  <View style={styles.column}>
181
177
  <ProductDisplayCard
182
- device="mobile"
183
178
  title="Recipe Name"
184
179
  subtext="+£0.00/pouch"
185
180
  showSubtext
@@ -197,7 +192,6 @@ export const WithBannerIcon = () => (
197
192
  export const NoSubtext = () => (
198
193
  <View style={styles.column}>
199
194
  <ProductDisplayCard
200
- device="mobile"
201
195
  title="Recipe Name"
202
196
  showSubtext={false}
203
197
  quantity={1}
@@ -211,7 +205,6 @@ export const NoSubtext = () => (
211
205
  export const WithImageQuantityBadge = () => (
212
206
  <View style={styles.column}>
213
207
  <ProductDisplayCard
214
- device="mobile"
215
208
  title="Gobble Gobble Turkey"
216
209
  subtext="+£0.50/pouch"
217
210
  showSubtext
@@ -223,7 +216,6 @@ export const WithImageQuantityBadge = () => (
223
216
  />
224
217
 
225
218
  <ProductDisplayCard
226
- device="mobile"
227
219
  title="Salmon Delight"
228
220
  subtext="+£0.25/pouch"
229
221
  showSubtext
@@ -239,7 +231,6 @@ export const WithImageQuantityBadge = () => (
239
231
  export const AllVariants = () => (
240
232
  <View style={styles.column}>
241
233
  <ProductDisplayCard
242
- device="desktop"
243
234
  title="Recipe Name"
244
235
  subtext="+£0.00/pouch"
245
236
  showSubtext
@@ -250,7 +241,6 @@ export const AllVariants = () => (
250
241
  />
251
242
 
252
243
  <ProductDisplayCard
253
- device="mobile"
254
244
  title="Recipe Name"
255
245
  subtext="+£0.50/pouch"
256
246
  showSubtext
@@ -261,7 +251,6 @@ export const AllVariants = () => (
261
251
  />
262
252
 
263
253
  <ProductDisplayCard
264
- device="mobile"
265
254
  title="Recipe Name"
266
255
  subtext="+£0.00/pouch"
267
256
  showSubtext
@@ -273,3 +262,28 @@ export const AllVariants = () => (
273
262
  />
274
263
  </View>
275
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}
287
+ />
288
+ </View>
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" device="mobile" />)
105
+ renderWithTheme(<ProductDisplayCard title="Test Product" />)
106
106
 
107
107
  expect(screen.getByText("Test Product")).toBeTruthy()
108
108
  })
@@ -6,10 +6,7 @@ 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
11
  subtext?: string | React.ReactNode
15
12
  showSubtext?: boolean
@@ -24,16 +21,20 @@ export type ProductDisplayCardProps = ViewProps & {
24
21
  decrementDisabled?: boolean
25
22
  image?: string | React.ReactNode
26
23
  imageBackgroundColor?: string
24
+ thumbnailWidth?: number
25
+ thumbnailBackgroundColor?: string
27
26
  showImageQuantityBadge?: boolean
27
+ borderless?: boolean
28
28
  banner?: React.ReactNode
29
29
  showBanner?: boolean
30
30
  bannerType?: "error" | "success" | "warning" | "info"
31
31
  showBannerIcon?: boolean
32
32
  }
33
33
 
34
- const StyledCardContainer = styled(View)<{
35
- cardDevice: ProductDisplayCardDevice
36
- }>(({ theme }) => {
34
+ const StyledCardContainer = styled(View)<{ borderless?: boolean }>(({
35
+ theme,
36
+ borderless
37
+ }) => {
37
38
  const { spacing, borderRadius } = theme.tokens.semantics.dimensions
38
39
  const { colour } = theme.tokens.semantics
39
40
  const spacingMd = parseTokenValue(spacing.md)
@@ -41,31 +42,46 @@ const StyledCardContainer = styled(View)<{
41
42
  return {
42
43
  flexDirection: "row",
43
44
  width: "100%",
44
- height: "100%",
45
+ height: borderless ? undefined : "100%",
45
46
  maxHeight: spacingXl * 6 + spacingMd,
46
47
  backgroundColor: colour.background.surface.default,
47
- borderWidth: 1,
48
- borderColor: colour.border.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
+ }),
49
59
  borderRadius: parseTokenValue(borderRadius.md),
50
60
  overflow: "hidden",
51
- shadowColor: "#522a10",
52
- shadowOffset: { width: 0, height: spacingMd / 4 },
53
- shadowOpacity: 0.05,
54
- shadowRadius: spacingXl - 4,
55
- elevation: 5,
56
61
  zIndex: 1
57
62
  }
58
63
  })
59
64
 
60
- const StyledImage = styled(View)<{ imageBackgroundColor?: string }>(
61
- ({ theme, imageBackgroundColor }) => ({
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
+ }) => ({
62
76
  flexShrink: 0,
63
77
  backgroundColor:
78
+ thumbnailBackgroundColor ||
64
79
  imageBackgroundColor ||
65
80
  theme.tokens.semantics.colour.background.container.secondary,
66
81
  overflow: "hidden",
67
82
  aspectRatio: "1 / 1",
68
- height: "100%",
83
+ width: thumbnailWidth,
84
+ height: thumbnailWidth,
69
85
  position: "relative"
70
86
  })
71
87
  )
@@ -90,14 +106,16 @@ const StyledQuantityBadge = styled(View)(({ theme }) => {
90
106
  }
91
107
  })
92
108
 
93
- const StyledContentArea = styled(View)<{
94
- contentDevice: ProductDisplayCardDevice
95
- }>(({ theme }) => {
109
+ const StyledContentArea = styled(View)<{ borderless?: boolean }>(({
110
+ theme,
111
+ borderless
112
+ }) => {
96
113
  const { spacing } = theme.tokens.semantics.dimensions
114
+ const paddingValue = parseTokenValue(spacing.md)
97
115
  return {
98
116
  flex: 1,
99
- paddingHorizontal: parseTokenValue(spacing.md),
100
- paddingVertical: parseTokenValue(spacing.md),
117
+ paddingHorizontal: paddingValue,
118
+ paddingVertical: borderless ? 0 : paddingValue,
101
119
  gap: parseTokenValue(spacing.xs),
102
120
  justifyContent: "space-between"
103
121
  }
@@ -139,7 +157,6 @@ const StyledNotification = styled(Notification as any)(({ theme }) => {
139
157
  * import { ProductDisplayCard } from "@butternutbox/pawprint-native"
140
158
  *
141
159
  * <ProductDisplayCard
142
- * device="mobile"
143
160
  * title="Premium Recipe"
144
161
  * subtext="+£0.00/pouch"
145
162
  * quantity={1}
@@ -147,16 +164,13 @@ const StyledNotification = styled(Notification as any)(({ theme }) => {
147
164
  * />
148
165
  * ```
149
166
  *
150
- * @param device - *(optional)* Device variant: "desktop" or "mobile" (default)
151
167
  * @param title - Product title
152
- * @param subtext - *(optional)* Subtitle text or JSX element (e.g. price)
168
+ * @param subtext - *(optional)* Subtitle text (e.g. price)
153
169
  * @param showSubtext - *(optional)* Show/hide subtext
154
170
  * @param quantity - *(optional)* Current quantity value
155
171
  * @param onQuantityChange - *(optional)* Callback when quantity changes
156
172
  * @param showQuantityPicker - *(optional)* Show/hide quantity picker
157
- * @param image - *(optional)* Image URL string or React element/JSX content
158
- * @param imageBackgroundColor - *(optional)* Background color for image container (default: theme background.container.secondary)
159
- * @param showImageQuantityBadge - *(optional)* Show/hide quantity badge overlay on image
173
+ * @param image - *(optional)* Image element or content
160
174
  * @param banner - *(optional)* Banner content to show below card
161
175
  * @param showBanner - *(optional)* Show/hide banner
162
176
  * @param bannerType - *(optional)* Banner notification type: "error", "success", "warning", or "info" (default)
@@ -168,7 +182,6 @@ export const ProductDisplayCard = React.forwardRef<
168
182
  >(
169
183
  (
170
184
  {
171
- device = "mobile",
172
185
  title,
173
186
  subtext,
174
187
  showSubtext = true,
@@ -183,7 +196,10 @@ export const ProductDisplayCard = React.forwardRef<
183
196
  decrementDisabled = false,
184
197
  image,
185
198
  imageBackgroundColor,
199
+ thumbnailWidth,
200
+ thumbnailBackgroundColor,
186
201
  showImageQuantityBadge = false,
202
+ borderless = false,
187
203
  banner,
188
204
  showBanner = false,
189
205
  bannerType = "info",
@@ -199,9 +215,13 @@ export const ProductDisplayCard = React.forwardRef<
199
215
  }
200
216
 
201
217
  const cardContent = (
202
- <StyledCardContainer ref={ref} cardDevice={device} {...rest}>
218
+ <StyledCardContainer ref={ref} borderless={borderless} {...rest}>
203
219
  {image && (
204
- <StyledImage imageBackgroundColor={imageBackgroundColor}>
220
+ <StyledImage
221
+ imageBackgroundColor={imageBackgroundColor}
222
+ thumbnailWidth={thumbnailWidth}
223
+ thumbnailBackgroundColor={thumbnailBackgroundColor}
224
+ >
205
225
  {typeof image === "string" ? (
206
226
  <Image
207
227
  source={{ uri: image }}
@@ -220,9 +240,9 @@ export const ProductDisplayCard = React.forwardRef<
220
240
  </StyledImage>
221
241
  )}
222
242
 
223
- <StyledContentArea contentDevice={device}>
243
+ <StyledContentArea borderless={borderless}>
224
244
  <StyledTextContainer>
225
- <Typography variant="heading" size="xs" color="primary">
245
+ <Typography variant="heading" size="2xs" color="primary">
226
246
  {title}
227
247
  </Typography>
228
248
 
@@ -1,5 +1,2 @@
1
1
  export { ProductDisplayCard } from "./ProductDisplayCard"
2
- export type {
3
- ProductDisplayCardProps,
4
- ProductDisplayCardDevice
5
- } from "./ProductDisplayCard"
2
+ export type { ProductDisplayCardProps } from "./ProductDisplayCard"