@butternutbox/pawprint-native 0.3.1 → 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.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +16 -16
  2. package/CHANGELOG.md +15 -0
  3. package/dist/index.cjs +1447 -627
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +246 -5
  6. package/dist/index.d.ts +246 -5
  7. package/dist/index.js +1442 -627
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -1
  10. package/src/components/atoms/Button/Button.tsx +4 -1
  11. package/src/components/atoms/Hint/Hint.tsx +1 -2
  12. package/src/components/atoms/Input/InputField.tsx +7 -1
  13. package/src/components/molecules/Animated/Animated.tsx +12 -3
  14. package/src/components/molecules/Countdown/Countdown.stories.tsx +218 -0
  15. package/src/components/molecules/Countdown/Countdown.tsx +315 -0
  16. package/src/components/molecules/Countdown/index.ts +2 -0
  17. package/src/components/molecules/Drawer/DrawerBody.tsx +12 -24
  18. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +248 -0
  19. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
  20. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +243 -0
  21. package/src/components/molecules/ProductDisplayCard/index.ts +5 -0
  22. package/src/components/molecules/ProductListingCard/Badge.tsx +65 -0
  23. package/src/components/molecules/ProductListingCard/Grid.tsx +59 -0
  24. package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +209 -0
  25. package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +235 -0
  26. package/src/components/molecules/ProductListingCard/index.ts +2 -0
  27. package/src/components/molecules/TabNavigation/TabNavigation.stories.tsx +183 -0
  28. package/src/components/molecules/TabNavigation/TabNavigation.tsx +354 -0
  29. package/src/components/molecules/TabNavigation/index.ts +7 -0
  30. package/src/components/molecules/index.ts +4 -0
  31. package/src/utils/index.ts +1 -0
  32. package/src/utils/token.ts +1 -0
@@ -0,0 +1,243 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { Typography } from "../../atoms/Typography"
5
+ import { NumberField } from "../../molecules/NumberField"
6
+ import { Notification } from "../Notification"
7
+ import { parseTokenValue } from "../../../utils"
8
+
9
+ export type ProductDisplayCardDevice = "desktop" | "mobile"
10
+
11
+ export type ProductDisplayCardProps = ViewProps & {
12
+ device?: ProductDisplayCardDevice
13
+ title: string
14
+ subtext?: string
15
+ showSubtext?: boolean
16
+ quantity?: number
17
+ onQuantityChange?: (quantity: number) => void
18
+ showQuantityPicker?: boolean
19
+ disableQuantityButtons?: boolean
20
+ hideQuantityButtons?: boolean
21
+ showIncrementButton?: boolean
22
+ showDecrementButton?: boolean
23
+ incrementDisabled?: boolean
24
+ decrementDisabled?: boolean
25
+ image?: React.ReactNode
26
+ banner?: React.ReactNode
27
+ showBanner?: boolean
28
+ bannerType?: "error" | "success" | "warning" | "info"
29
+ showBannerIcon?: boolean
30
+ }
31
+
32
+ const StyledCardContainer = styled(View)<{
33
+ cardDevice: ProductDisplayCardDevice
34
+ }>(({ theme }) => {
35
+ const { spacing, borderRadius } = theme.tokens.semantics.dimensions
36
+ const { colour } = theme.tokens.semantics
37
+ const spacingMd = parseTokenValue(spacing.md)
38
+ const spacingXl = parseTokenValue(spacing.xl)
39
+ return {
40
+ flexDirection: "row",
41
+ width: "100%",
42
+ height: "100%",
43
+ maxHeight: spacingXl * 6 + spacingMd,
44
+ backgroundColor: colour.background.surface.default,
45
+ borderWidth: 1,
46
+ borderColor: colour.border.default,
47
+ borderRadius: parseTokenValue(borderRadius.md),
48
+ overflow: "hidden",
49
+ shadowColor: "#522a10",
50
+ shadowOffset: { width: 0, height: spacingMd / 4 },
51
+ shadowOpacity: 0.05,
52
+ shadowRadius: spacingXl - 4,
53
+ elevation: 5,
54
+ zIndex: 1
55
+ }
56
+ })
57
+
58
+ const StyledImage = styled(View)(() => ({
59
+ flexShrink: 0,
60
+ backgroundColor: "#f5f5f5",
61
+ overflow: "hidden",
62
+ aspectRatio: "1 / 1",
63
+ height: "100%"
64
+ }))
65
+
66
+ const StyledContentArea = styled(View)<{
67
+ contentDevice: ProductDisplayCardDevice
68
+ }>(({ theme }) => {
69
+ const { spacing } = theme.tokens.semantics.dimensions
70
+ return {
71
+ flex: 1,
72
+ paddingHorizontal: parseTokenValue(spacing.md),
73
+ paddingVertical: parseTokenValue(spacing.md),
74
+ gap: parseTokenValue(spacing.xs),
75
+ justifyContent: "space-between"
76
+ }
77
+ })
78
+
79
+ const StyledTextContainer = styled(View)(() => ({
80
+ gap: 2
81
+ }))
82
+
83
+ const StyledBannerWrapper = styled(View)(() => ({
84
+ width: "100%",
85
+ marginTop: -48,
86
+ paddingTop: 24,
87
+ paddingBottom: 16
88
+ }))
89
+
90
+ const StyledRootWrapper = styled(View)(() => {
91
+ return {
92
+ flexDirection: "column",
93
+ width: "100%",
94
+ height: "100%"
95
+ }
96
+ })
97
+
98
+ const StyledNotification = styled(Notification as any)(({ theme }) => {
99
+ const { spacing } = theme.tokens.semantics.dimensions
100
+ return {
101
+ top: parseTokenValue(spacing.md),
102
+ paddingHorizontal: parseTokenValue(spacing.xl),
103
+ paddingVertical: parseTokenValue(spacing.md)
104
+ }
105
+ })
106
+
107
+ /**
108
+ * Product Display Card component for showing product information with optional quantity picker.
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * import { ProductDisplayCard } from "@butternutbox/pawprint-native"
113
+ *
114
+ * <ProductDisplayCard
115
+ * device="mobile"
116
+ * title="Premium Recipe"
117
+ * subtext="+£0.00/pouch"
118
+ * quantity={1}
119
+ * showQuantityPicker
120
+ * />
121
+ * ```
122
+ *
123
+ * @param device - *(optional)* Device variant: "desktop" or "mobile" (default)
124
+ * @param title - Product title
125
+ * @param subtext - *(optional)* Subtitle text (e.g. price)
126
+ * @param showSubtext - *(optional)* Show/hide subtext
127
+ * @param quantity - *(optional)* Current quantity value
128
+ * @param onQuantityChange - *(optional)* Callback when quantity changes
129
+ * @param showQuantityPicker - *(optional)* Show/hide quantity picker
130
+ * @param image - *(optional)* Image element or content
131
+ * @param banner - *(optional)* Banner content to show below card
132
+ * @param showBanner - *(optional)* Show/hide banner
133
+ * @param bannerType - *(optional)* Banner notification type: "error", "success", "warning", or "info" (default)
134
+ * @param showBannerIcon - *(optional)* Show/hide banner notification icon
135
+ */
136
+ export const ProductDisplayCard = React.forwardRef<
137
+ View,
138
+ ProductDisplayCardProps
139
+ >(
140
+ (
141
+ {
142
+ device = "mobile",
143
+ title,
144
+ subtext,
145
+ showSubtext = true,
146
+ quantity = 1,
147
+ onQuantityChange,
148
+ showQuantityPicker = false,
149
+ disableQuantityButtons = false,
150
+ hideQuantityButtons = false,
151
+ showIncrementButton,
152
+ showDecrementButton,
153
+ incrementDisabled = false,
154
+ decrementDisabled = false,
155
+ image,
156
+ banner,
157
+ showBanner = false,
158
+ bannerType = "info",
159
+ showBannerIcon = false,
160
+ ...rest
161
+ },
162
+ ref
163
+ ) => {
164
+ const handleQuantityChange = (newQuantity: number) => {
165
+ if (newQuantity >= 1) {
166
+ onQuantityChange?.(newQuantity)
167
+ }
168
+ }
169
+
170
+ const cardContent = (
171
+ <StyledCardContainer ref={ref} cardDevice={device} {...rest}>
172
+ {image && <StyledImage>{image}</StyledImage>}
173
+
174
+ <StyledContentArea contentDevice={device}>
175
+ <StyledTextContainer>
176
+ <Typography variant="heading" size="xs" color="primary">
177
+ {title}
178
+ </Typography>
179
+
180
+ {showSubtext && subtext && (
181
+ <Typography variant="body" size="sm" color="secondary">
182
+ {subtext}
183
+ </Typography>
184
+ )}
185
+ </StyledTextContainer>
186
+
187
+ {showQuantityPicker && (
188
+ <View style={{ marginLeft: "auto" }}>
189
+ <NumberField
190
+ size="sm"
191
+ value={String(quantity)}
192
+ onChangeText={(text) => {
193
+ const value = parseInt(text, 10)
194
+ if (!isNaN(value)) {
195
+ handleQuantityChange(value)
196
+ }
197
+ }}
198
+ onIncrement={() => handleQuantityChange(quantity + 1)}
199
+ onDecrement={() => handleQuantityChange(quantity - 1)}
200
+ min={1}
201
+ accessible
202
+ accessibilityLabel={`Quantity: ${quantity}`}
203
+ showIncrementButton={
204
+ showIncrementButton !== undefined
205
+ ? showIncrementButton
206
+ : !hideQuantityButtons
207
+ }
208
+ showDecrementButton={
209
+ showDecrementButton !== undefined
210
+ ? showDecrementButton
211
+ : !hideQuantityButtons
212
+ }
213
+ incrementDisabled={incrementDisabled || disableQuantityButtons}
214
+ decrementDisabled={decrementDisabled || disableQuantityButtons}
215
+ />
216
+ </View>
217
+ )}
218
+ </StyledContentArea>
219
+ </StyledCardContainer>
220
+ )
221
+
222
+ if (showBanner && banner) {
223
+ return (
224
+ <StyledRootWrapper>
225
+ {cardContent}
226
+ <StyledBannerWrapper>
227
+ <StyledNotification
228
+ variant="inline"
229
+ type={bannerType}
230
+ showIcon={showBannerIcon}
231
+ >
232
+ {banner}
233
+ </StyledNotification>
234
+ </StyledBannerWrapper>
235
+ </StyledRootWrapper>
236
+ )
237
+ }
238
+
239
+ return cardContent
240
+ }
241
+ )
242
+
243
+ ProductDisplayCard.displayName = "ProductDisplayCard"
@@ -0,0 +1,5 @@
1
+ export { ProductDisplayCard } from "./ProductDisplayCard"
2
+ export type {
3
+ ProductDisplayCardProps,
4
+ ProductDisplayCardDevice
5
+ } from "./ProductDisplayCard"
@@ -0,0 +1,65 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { Typography } from "../../atoms/Typography"
5
+
6
+ const parseTokenValue = (value: string | number): number => {
7
+ if (typeof value === "number") return value
8
+ return parseFloat(value)
9
+ }
10
+
11
+ type BadgeOwnProps = {
12
+ icon?: React.ReactNode
13
+ text: React.ReactNode
14
+ }
15
+
16
+ export type BadgeProps = BadgeOwnProps & Omit<ViewProps, keyof BadgeOwnProps>
17
+
18
+ const BadgeContainer = styled(View)(({ theme }) => {
19
+ const spacing = theme.tokens.semantics.dimensions.spacing
20
+ const { sizing, borderRadius } = theme.tokens.semantics.dimensions
21
+ const { colour } = theme.tokens.semantics
22
+
23
+ return {
24
+ display: "flex",
25
+ flexDirection: "row",
26
+ gap: parseTokenValue(spacing["2xs"]),
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ height: parseTokenValue(sizing["2xl"]),
30
+ paddingRight: parseTokenValue(spacing.sm),
31
+ backgroundColor: colour.background.container.alt,
32
+ borderTopRightRadius: parseTokenValue(borderRadius.xs),
33
+ borderBottomRightRadius: parseTokenValue(borderRadius.xs)
34
+ }
35
+ })
36
+
37
+ const BadgeIcon = styled(View)(({ theme }) => {
38
+ const spacing = theme.tokens.semantics.dimensions.spacing
39
+ const sizing = theme.tokens.semantics.dimensions.sizing
40
+
41
+ return {
42
+ display: "flex",
43
+ alignItems: "center",
44
+ justifyContent: "center",
45
+ width: parseTokenValue(sizing.md),
46
+ height: parseTokenValue(sizing.md),
47
+ marginLeft: parseTokenValue(spacing.xs),
48
+ flexShrink: 0
49
+ }
50
+ })
51
+
52
+ export const Badge = React.forwardRef<View, BadgeProps>(
53
+ ({ icon, text, ...rest }, ref) => {
54
+ return (
55
+ <BadgeContainer ref={ref} {...rest}>
56
+ {icon && <BadgeIcon>{icon}</BadgeIcon>}
57
+ <Typography variant="body" size="sm" weight="bold">
58
+ {text}
59
+ </Typography>
60
+ </BadgeContainer>
61
+ )
62
+ }
63
+ )
64
+
65
+ Badge.displayName = "ProductListingCard.Badge"
@@ -0,0 +1,59 @@
1
+ import React from "react"
2
+ import { View, ViewProps, useWindowDimensions } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { DEFAULT_THEME_OPTIONS } from "@butternutbox/pawprint-tokens"
5
+
6
+ type ProductListingCardGridOwnProps = {
7
+ children: React.ReactNode
8
+ xs?: number
9
+ md?: number
10
+ lg?: number
11
+ }
12
+
13
+ export type ProductListingCardGridProps = ProductListingCardGridOwnProps &
14
+ Omit<ViewProps, keyof ProductListingCardGridOwnProps>
15
+
16
+ const StyledGrid = styled(View)(({ theme }) => {
17
+ const spacing = theme.tokens.semantics.dimensions.spacing
18
+
19
+ return {
20
+ display: "grid",
21
+ "grid-column-gap": spacing.sm,
22
+ "grid-row-gap": spacing.xl
23
+ } as any
24
+ })
25
+
26
+ export const Grid = React.forwardRef<View, ProductListingCardGridProps>(
27
+ ({ children, xs = 1, md = 2, lg = 3, ...rest }, ref) => {
28
+ const { width } = useWindowDimensions()
29
+
30
+ const breakpoints =
31
+ DEFAULT_THEME_OPTIONS.tokens.semantics.dimensions.breakpoints.spacing
32
+ const SM_BREAKPOINT = parseFloat(String(breakpoints.sm))
33
+ const MD_BREAKPOINT = parseFloat(String(breakpoints.md))
34
+
35
+ const getItemsPerRow = () => {
36
+ if (width >= MD_BREAKPOINT) return lg
37
+ if (width >= SM_BREAKPOINT) return md
38
+ return xs
39
+ }
40
+
41
+ const itemsPerRow = getItemsPerRow()
42
+
43
+ return (
44
+ <StyledGrid
45
+ ref={ref}
46
+ style={
47
+ {
48
+ gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`
49
+ } as any
50
+ }
51
+ {...rest}
52
+ >
53
+ {children}
54
+ </StyledGrid>
55
+ )
56
+ }
57
+ )
58
+
59
+ Grid.displayName = "ProductListingCard.Grid"
@@ -0,0 +1,209 @@
1
+ import React from "react"
2
+ import { View } from "react-native"
3
+ import { ProductListingCard } from "./ProductListingCard"
4
+ import { Badge } from "./Badge"
5
+ import { ThumbsUpFilledPrimary } from "@butternutbox/pawprint-icons/marketing"
6
+
7
+ const defaultBadge = (
8
+ <Badge icon={<ThumbsUpFilledPrimary />} text="New & improved" />
9
+ )
10
+
11
+ const placeholderImage = {
12
+ uri: "https://placehold.co/232x232"
13
+ }
14
+
15
+ export default {
16
+ title: "Molecules/ProductListingCard",
17
+ component: ProductListingCard,
18
+ argTypes: {
19
+ image: {
20
+ description: "Product image source"
21
+ },
22
+ imageAlt: {
23
+ control: "text",
24
+ description: "Alt text for the product image"
25
+ },
26
+ badge: {
27
+ description: "Badge content to display in the top-left corner"
28
+ },
29
+ category: {
30
+ description: "Product category Typography element(s)"
31
+ },
32
+ title: {
33
+ description: "Product name/title Typography element(s)"
34
+ },
35
+ size: {
36
+ description: "Product size or variant Typography element(s)"
37
+ },
38
+ price: {
39
+ description: "Price content Typography element(s)"
40
+ },
41
+ showBadge: {
42
+ control: "boolean",
43
+ description: "Toggle badge visibility"
44
+ },
45
+ showCategory: {
46
+ control: "boolean",
47
+ description: "Toggle category visibility"
48
+ },
49
+ showTitle: {
50
+ control: "boolean",
51
+ description: "Toggle product title visibility"
52
+ },
53
+ showSize: {
54
+ control: "boolean",
55
+ description: "Toggle product size visibility"
56
+ },
57
+ showPrice: {
58
+ control: "boolean",
59
+ description: "Toggle price visibility"
60
+ }
61
+ }
62
+ }
63
+
64
+ export const Default = {
65
+ render: () => (
66
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
67
+ <ProductListingCard
68
+ image={placeholderImage}
69
+ imageAlt="Fish Oil Product"
70
+ badge={defaultBadge}
71
+ category="Daily supplements"
72
+ title="Fish Oil"
73
+ size="200ml"
74
+ price="From £7.49"
75
+ showBadge={true}
76
+ showCategory={true}
77
+ showTitle={true}
78
+ showSize={true}
79
+ showPrice={true}
80
+ />
81
+ </View>
82
+ )
83
+ }
84
+
85
+ export const WithoutBadge = {
86
+ render: () => (
87
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
88
+ <ProductListingCard
89
+ image={placeholderImage}
90
+ imageAlt="Fish Oil Product"
91
+ category="Daily supplements"
92
+ title="Fish Oil"
93
+ size="200ml"
94
+ price="From £7.49"
95
+ showBadge={false}
96
+ showCategory={true}
97
+ showTitle={true}
98
+ showSize={true}
99
+ showPrice={true}
100
+ />
101
+ </View>
102
+ )
103
+ }
104
+
105
+ export const MinimalInfo = {
106
+ render: () => (
107
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
108
+ <ProductListingCard
109
+ image={{ uri: "https://placeholder.co/232x232" }}
110
+ imageAlt="Product"
111
+ title="Product Name"
112
+ price="£9.99"
113
+ showBadge={false}
114
+ showCategory={false}
115
+ showSize={false}
116
+ />
117
+ </View>
118
+ )
119
+ }
120
+
121
+ export const FullyCustomized = {
122
+ render: () => (
123
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
124
+ <ProductListingCard
125
+ image={{ uri: "https://placehold.co/232x232" }}
126
+ imageAlt="Premium Item"
127
+ badge={<Badge icon="⭐" text="Premium" />}
128
+ category="Premium Collection"
129
+ title="Deluxe Supplement Pack"
130
+ size="500ml x 3 bottles"
131
+ price="From £24.99"
132
+ showBadge={true}
133
+ showCategory={true}
134
+ showTitle={true}
135
+ showSize={true}
136
+ showPrice={true}
137
+ />
138
+ </View>
139
+ )
140
+ }
141
+
142
+ export const Grid = {
143
+ render: () => (
144
+ <View style={{ width: "100%", padding: 16 }}>
145
+ <ProductListingCard.Grid xs={1} md={2} lg={3}>
146
+ {Array.from({ length: 6 }).map((_, i) => (
147
+ <ProductListingCard
148
+ key={i}
149
+ image={placeholderImage}
150
+ imageAlt="Fish Oil Product"
151
+ badge={defaultBadge}
152
+ category="Daily supplements"
153
+ title="Fish Oil"
154
+ size="200ml"
155
+ price="From £7.49"
156
+ showBadge={true}
157
+ showCategory={true}
158
+ showTitle={true}
159
+ showSize={true}
160
+ showPrice={true}
161
+ />
162
+ ))}
163
+ </ProductListingCard.Grid>
164
+ </View>
165
+ )
166
+ }
167
+
168
+ export const Playground = {
169
+ render: () => (
170
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
171
+ <ProductListingCard
172
+ image={placeholderImage}
173
+ imageAlt="Fish Oil Product"
174
+ badge={defaultBadge}
175
+ category="Daily supplements"
176
+ title="Fish Oil"
177
+ size="200ml"
178
+ price="From £7.49"
179
+ showBadge={true}
180
+ showCategory={true}
181
+ showTitle={true}
182
+ showSize={true}
183
+ showPrice={true}
184
+ />
185
+ </View>
186
+ )
187
+ }
188
+
189
+ export const WithWasPrice = {
190
+ render: () => (
191
+ <View style={{ width: 232, alignSelf: "flex-start" }}>
192
+ <ProductListingCard
193
+ image={placeholderImage}
194
+ imageAlt="Fish Oil Product"
195
+ badge={defaultBadge}
196
+ category="Daily supplements"
197
+ title="Fish Oil"
198
+ size="200ml"
199
+ price="From £7.49"
200
+ wasPrice="£12.50"
201
+ showBadge={true}
202
+ showCategory={true}
203
+ showTitle={true}
204
+ showSize={true}
205
+ showPrice={true}
206
+ />
207
+ </View>
208
+ )
209
+ }