@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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +9 -9
  2. package/CHANGELOG.md +8 -0
  3. package/dist/index.cjs +965 -151
  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 +960 -151
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -1
  10. package/src/components/atoms/Hint/Hint.tsx +1 -2
  11. package/src/components/atoms/Input/InputField.tsx +7 -1
  12. package/src/components/molecules/Animated/Animated.tsx +12 -3
  13. package/src/components/molecules/Countdown/Countdown.stories.tsx +218 -0
  14. package/src/components/molecules/Countdown/Countdown.tsx +315 -0
  15. package/src/components/molecules/Countdown/index.ts +2 -0
  16. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +248 -0
  17. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
  18. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +243 -0
  19. package/src/components/molecules/ProductDisplayCard/index.ts +5 -0
  20. package/src/components/molecules/ProductListingCard/Badge.tsx +65 -0
  21. package/src/components/molecules/ProductListingCard/Grid.tsx +59 -0
  22. package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +209 -0
  23. package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +235 -0
  24. package/src/components/molecules/ProductListingCard/index.ts +2 -0
  25. package/src/components/molecules/TabNavigation/TabNavigation.stories.tsx +183 -0
  26. package/src/components/molecules/TabNavigation/TabNavigation.tsx +354 -0
  27. package/src/components/molecules/TabNavigation/index.ts +7 -0
  28. package/src/components/molecules/index.ts +4 -0
  29. package/src/utils/index.ts +1 -0
  30. package/src/utils/token.ts +1 -0
@@ -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
+ }
@@ -0,0 +1,235 @@
1
+ import React from "react"
2
+ import {
3
+ View,
4
+ Image,
5
+ ImageSourcePropType,
6
+ Pressable,
7
+ PressableProps
8
+ } from "react-native"
9
+ import styled from "@emotion/native"
10
+ import { Typography } from "../../atoms/Typography"
11
+ import { Badge } from "./Badge"
12
+ import { Grid } from "./Grid"
13
+
14
+ type ProductListingCardOwnProps = {
15
+ image?: ImageSourcePropType
16
+ imageAlt?: string
17
+ badge?: React.ReactNode
18
+ category?: string
19
+ title?: string
20
+ size?: string
21
+ price?: string
22
+ wasPrice?: string
23
+ showBadge?: boolean
24
+ showCategory?: boolean
25
+ showTitle?: boolean
26
+ showSize?: boolean
27
+ showPrice?: boolean
28
+ onPress?: () => void
29
+ }
30
+
31
+ export type ProductListingCardProps = ProductListingCardOwnProps &
32
+ Omit<PressableProps, keyof ProductListingCardOwnProps>
33
+
34
+ const parseTokenValue = (value: string | number): number => {
35
+ if (typeof value === "number") return value
36
+ return parseFloat(value)
37
+ }
38
+
39
+ const StyledRoot = styled(Pressable)(({ theme }) => {
40
+ const spacing = theme.tokens.semantics.dimensions.spacing
41
+
42
+ return {
43
+ display: "flex",
44
+ flexDirection: "column",
45
+ gap: parseTokenValue(spacing.md),
46
+ alignItems: "flex-start",
47
+ width: "100%"
48
+ }
49
+ })
50
+
51
+ const ImageContainer = styled(View)({
52
+ position: "relative",
53
+ width: "100%",
54
+ aspectRatio: 1,
55
+ overflow: "hidden"
56
+ })
57
+
58
+ const StyledImage = styled(Image)({
59
+ width: "100%",
60
+ height: "100%",
61
+ resizeMode: "cover"
62
+ })
63
+
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
+ })
85
+
86
+ const ContentContainer = styled(View)(({ theme }) => {
87
+ const spacing = theme.tokens.semantics.dimensions.spacing
88
+
89
+ return {
90
+ display: "flex",
91
+ flexDirection: "column",
92
+ gap: parseTokenValue(spacing.md),
93
+ alignItems: "flex-start",
94
+ width: "100%"
95
+ }
96
+ })
97
+
98
+ const DetailsContainer = styled(View)({
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ alignItems: "flex-start",
102
+ width: "100%"
103
+ })
104
+
105
+ const CategoryWrapper = styled(View)(({ theme }) => {
106
+ const spacing = theme.tokens.semantics.dimensions.spacing
107
+
108
+ return {
109
+ marginBottom: parseTokenValue(spacing["3xs"])
110
+ }
111
+ })
112
+
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
+ }
130
+ })
131
+
132
+ const PricingContainer = styled(View)(({ theme }) => {
133
+ const spacing = theme.tokens.semantics.dimensions.spacing
134
+
135
+ return {
136
+ display: "flex",
137
+ flexDirection: "row",
138
+ gap: Math.round(parseFloat(spacing["2xs"] as string)),
139
+ alignItems: "center",
140
+ width: "100%"
141
+ }
142
+ })
143
+
144
+ const _ProductListingCard = React.forwardRef<View, ProductListingCardProps>(
145
+ (
146
+ {
147
+ image,
148
+ imageAlt,
149
+ badge,
150
+ category,
151
+ title,
152
+ size,
153
+ price,
154
+ wasPrice,
155
+ showBadge = true,
156
+ showCategory = true,
157
+ showTitle = true,
158
+ showSize = true,
159
+ showPrice = true,
160
+ onPress,
161
+ ...rest
162
+ },
163
+ ref
164
+ ) => {
165
+ return (
166
+ <StyledRoot ref={ref} onPress={onPress} {...rest}>
167
+ <ImageContainer>
168
+ {image && (
169
+ <View style={{ flex: 1, width: "100%", height: "100%" }}>
170
+ <StyledImage
171
+ source={image}
172
+ accessibilityLabel={imageAlt}
173
+ accessible
174
+ />
175
+ </View>
176
+ )}
177
+ {showBadge && badge && <BadgeContainer>{badge}</BadgeContainer>}
178
+ </ImageContainer>
179
+
180
+ <ContentContainer>
181
+ <DetailsContainer>
182
+ {showCategory && category && (
183
+ <CategoryWrapper>
184
+ <Typography variant="body" size="sm" color="secondary">
185
+ {category}
186
+ </Typography>
187
+ </CategoryWrapper>
188
+ )}
189
+ {showTitle && (
190
+ <TitleContainer>
191
+ <Typography variant="heading" size="xs">
192
+ {title}
193
+ </Typography>
194
+ </TitleContainer>
195
+ )}
196
+ {showSize && size && (
197
+ <SizeWrapper>
198
+ <Typography variant="body" size="md" color="secondary">
199
+ {size}
200
+ </Typography>
201
+ </SizeWrapper>
202
+ )}
203
+ </DetailsContainer>
204
+
205
+ {showPrice && price && (
206
+ <PricingContainer>
207
+ <Typography variant="body" size="md" color="secondary">
208
+ {price}
209
+ </Typography>
210
+ {wasPrice && (
211
+ <Typography
212
+ variant="body"
213
+ size="md"
214
+ color="secondary"
215
+ textDecoration="line-through"
216
+ >
217
+ {wasPrice}
218
+ </Typography>
219
+ )}
220
+ </PricingContainer>
221
+ )}
222
+ </ContentContainer>
223
+ </StyledRoot>
224
+ )
225
+ }
226
+ )
227
+
228
+ _ProductListingCard.displayName = "ProductListingCard"
229
+
230
+ const ProductListingCardWithBadgeAndGrid = Object.assign(_ProductListingCard, {
231
+ Badge,
232
+ Grid
233
+ }) as typeof _ProductListingCard & { Badge: typeof Badge; Grid: typeof Grid }
234
+
235
+ export { ProductListingCardWithBadgeAndGrid as ProductListingCard }
@@ -0,0 +1,2 @@
1
+ export { ProductListingCard } from "./ProductListingCard"
2
+ export type { ProductListingCardProps } from "./ProductListingCard"