@butternutbox/pawprint-native 0.5.0 → 0.6.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.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -22,12 +22,14 @@ const StyledDockRoot = styled(View)<{
22
22
  dockBorderTopWidth: number
23
23
  dockBorderTopColor: string
24
24
  dockPaddingVertical: number
25
+ dockPaddingHorizontal: number
25
26
  }>(
26
27
  ({
27
28
  dockBgColor,
28
29
  dockBorderTopWidth,
29
30
  dockBorderTopColor,
30
- dockPaddingVertical
31
+ dockPaddingVertical,
32
+ dockPaddingHorizontal
31
33
  }) => ({
32
34
  alignItems: "center",
33
35
  justifyContent: "center",
@@ -35,7 +37,8 @@ const StyledDockRoot = styled(View)<{
35
37
  backgroundColor: dockBgColor,
36
38
  borderTopWidth: dockBorderTopWidth,
37
39
  borderTopColor: dockBorderTopColor,
38
- paddingVertical: dockPaddingVertical
40
+ paddingVertical: dockPaddingVertical,
41
+ paddingHorizontal: dockPaddingHorizontal
39
42
  })
40
43
  )
41
44
 
@@ -51,7 +54,7 @@ const StyledStackedInner = styled(View)<{ innerGap: number }>(
51
54
  const StyledButtonGroup = styled(View)<{
52
55
  groupDirection: "column" | "row"
53
56
  groupAlign: "stretch" | "center"
54
- groupJustify: "center" | "space-around"
57
+ groupJustify: "center" | "space-between"
55
58
  groupGap: number
56
59
  }>(({ groupDirection, groupAlign, groupJustify, groupGap }) => ({
57
60
  flexDirection: groupDirection,
@@ -105,6 +108,9 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
105
108
  dockPaddingVertical={parseTokenValue(
106
109
  buttonDock.spacing[variant].mobile.topPadding
107
110
  )}
111
+ dockPaddingHorizontal={parseTokenValue(
112
+ buttonDock.spacing[variant].mobile.horizontalPadding
113
+ )}
108
114
  {...rest}
109
115
  >
110
116
  {isStacked ? (
@@ -134,7 +140,7 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
134
140
  <StyledButtonGroup
135
141
  groupDirection="row"
136
142
  groupAlign="center"
137
- groupJustify="space-around"
143
+ groupJustify="space-between"
138
144
  groupGap={groupGap}
139
145
  >
140
146
  {children}
@@ -1,8 +1,9 @@
1
1
  import React from "react"
2
- import { ScrollView, ScrollViewProps } from "react-native"
2
+ import { ScrollView, ScrollViewProps, useWindowDimensions } from "react-native"
3
3
  import styled from "@emotion/native"
4
4
  import { useTheme } from "@emotion/react"
5
5
  import { useDrawerHeaderContext } from "./DrawerHeaderContext"
6
+ import { useDrawerFooterContext } from "./DrawerFooterContext"
6
7
 
7
8
  export type DrawerBodyProps = ScrollViewProps
8
9
 
@@ -12,12 +13,24 @@ const StyledScrollView = styled(ScrollView)<{
12
13
  bodyPaddingHorizontal: number
13
14
  bodyPaddingRight: number
14
15
  bodyGap: number
15
- }>(({ bodyPaddingHorizontal, bodyPaddingRight, bodyGap }) => ({
16
- flex: 1,
17
- paddingLeft: bodyPaddingHorizontal,
18
- paddingRight: bodyPaddingRight,
19
- gap: bodyGap
20
- }))
16
+ bodyMaxHeight: number
17
+ bodyPaddingBottom: number
18
+ }>(
19
+ ({
20
+ bodyPaddingHorizontal,
21
+ bodyPaddingRight,
22
+ bodyGap,
23
+ bodyMaxHeight,
24
+ bodyPaddingBottom
25
+ }) => ({
26
+ flex: 1,
27
+ paddingLeft: bodyPaddingHorizontal,
28
+ paddingRight: bodyPaddingRight,
29
+ gap: bodyGap,
30
+ maxHeight: bodyMaxHeight,
31
+ paddingBottom: bodyPaddingBottom
32
+ })
33
+ )
21
34
 
22
35
  /**
23
36
  * Scrollable content area of the drawer. Grows to fill available space
@@ -30,10 +43,13 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
30
43
  const { buttons } = theme.tokens.components
31
44
  const { content } = spacing
32
45
  const headerContext = useDrawerHeaderContext()
46
+ const footerContext = useDrawerFooterContext()
47
+ const { height: windowHeight } = useWindowDimensions()
33
48
 
34
49
  const gap = parseTokenValue(content.slot.gap)
35
50
  const horizontalPadding = parseTokenValue(content.slot.horizontalPadding)
36
51
  const topPadding = parseTokenValue(content.slot.verticalPadding)
52
+ const bodyMaxHeight = windowHeight - 300
37
53
 
38
54
  // When there's no header the close button floats in the top-right corner.
39
55
  // Reserve space on the right so body content doesn't slide under it.
@@ -43,12 +59,18 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
43
59
  parseTokenValue(buttons.size.sm.height)
44
60
  : horizontalPadding
45
61
 
62
+ const bodyPaddingBottom = !footerContext
63
+ ? parseTokenValue(theme.tokens.semantics.dimensions.spacing["2xl"])
64
+ : 0
65
+
46
66
  return (
47
67
  <StyledScrollView
48
68
  ref={ref}
49
69
  bodyPaddingHorizontal={horizontalPadding}
50
70
  bodyPaddingRight={paddingRight}
51
71
  bodyGap={gap}
72
+ bodyMaxHeight={bodyMaxHeight}
73
+ bodyPaddingBottom={bodyPaddingBottom}
52
74
  contentContainerStyle={[
53
75
  {
54
76
  gap,
@@ -29,7 +29,7 @@ type InlineVariantProps = BaseProps & {
29
29
  title?: string
30
30
  onClose?: () => void
31
31
  size?: never
32
- link?: never
32
+ link?: { label: string; href?: string; onPress?: () => void }
33
33
  }
34
34
 
35
35
  type ToastVariantProps = BaseProps & {
@@ -45,7 +45,7 @@ type SystemVariantProps = BaseProps & {
45
45
  size?: NotificationSize
46
46
  title?: never
47
47
  onClose?: never
48
- link?: never
48
+ link?: { label: string; href?: string; onPress?: () => void }
49
49
  }
50
50
 
51
51
  type NotificationOwnProps =
@@ -207,7 +207,7 @@ const StyledCloseButton = styled(Pressable)({
207
207
  * variants — inline (within content flow), toast (floating overlay), and
208
208
  * system (full-width banner) — each with four severity types.
209
209
  *
210
- * Note: Unlike the web version, toast link uses `onPress` callback instead
210
+ * Note: Unlike the web version, links use `onPress` callback instead
211
211
  * of `href` for navigation. The `href` prop is also available and opens
212
212
  * the URL via `Linking`.
213
213
  *
@@ -216,7 +216,7 @@ const StyledCloseButton = styled(Pressable)({
216
216
  * @param {boolean} [showIcon=true] - Whether to show the status icon.
217
217
  * @param {string} [title] - Optional headline (inline variant only).
218
218
  * @param {() => void} [onClose] - Close callback (inline and toast variants).
219
- * @param {{ label: string; href?: string; onPress?: () => void }} [link] - Optional action link (toast variant only).
219
+ * @param {{ label: string; href?: string; onPress?: () => void }} [link] - Optional action link (all variants).
220
220
  * @param {"sm" | "lg"} [size="sm"] - Size variant (system variant only).
221
221
  * @param {React.ReactNode} children - The notification body text.
222
222
  *
@@ -286,12 +286,24 @@ export const Notification = React.forwardRef<View, NotificationProps>(
286
286
  aria-label={type}
287
287
  />
288
288
  )}
289
- <Typography
290
- token={systemNotifications.notifications.typography.default}
291
- color={systemNotifications.notification.colour.text.default}
292
- >
293
- {children}
294
- </Typography>
289
+ <View style={{ flexDirection: "column", gap: 8, flex: 1 }}>
290
+ <Typography
291
+ token={systemNotifications.notifications.typography.default}
292
+ color={systemNotifications.notification.colour.text.default}
293
+ >
294
+ {children}
295
+ </Typography>
296
+ {link && (
297
+ <Link
298
+ href={link.href}
299
+ onPress={link.onPress}
300
+ weight="semiBold"
301
+ size="md"
302
+ >
303
+ {link.label}
304
+ </Link>
305
+ )}
306
+ </View>
295
307
  </StyledSystemRoot>
296
308
  )
297
309
  }
@@ -408,6 +420,20 @@ export const Notification = React.forwardRef<View, NotificationProps>(
408
420
  </Typography>
409
421
  </StyledInlineCopy>
410
422
  </StyledCopyRow>
423
+ {link && (
424
+ <StyledLinkWrapper
425
+ linkPaddingLeft={parseTokenValue(content.copy.spacing.gap)}
426
+ >
427
+ <Link
428
+ href={link.href}
429
+ onPress={link.onPress}
430
+ weight="semiBold"
431
+ size="md"
432
+ >
433
+ {link.label}
434
+ </Link>
435
+ </StyledLinkWrapper>
436
+ )}
411
437
  </StyledContents>
412
438
  {onClose && (
413
439
  <StyledCloseButton
@@ -3,6 +3,7 @@ import { View, StyleSheet } from "react-native"
3
3
  import { RadioGroup } from "./RadioGroup"
4
4
  import type { RadioGroupProps } from "./RadioGroup"
5
5
  import { Typography } from "../../atoms/Typography"
6
+ import { StarRate } from "@butternutbox/pawprint-icons/core"
6
7
 
7
8
  export default {
8
9
  title: "Molecules/Radio",
@@ -95,6 +96,51 @@ export const TileVariant = () => (
95
96
  </View>
96
97
  )
97
98
 
99
+ export const WithTag = () => (
100
+ <View style={styles.column}>
101
+ <View style={styles.section}>
102
+ <Typography size="sm" weight="semiBold" color="tertiary">
103
+ Tile with tag
104
+ </Typography>
105
+ <RadioGroup name="tag-tile" defaultValue="1">
106
+ <RadioGroup.Radio
107
+ variant="tile"
108
+ value="1"
109
+ label="1 pouch over 2 days"
110
+ subText="400g pouches"
111
+ tag={{ children: "Save up to £12.20 per box" }}
112
+ />
113
+ <RadioGroup.Radio
114
+ variant="tile"
115
+ value="2"
116
+ label="1 pouch per day"
117
+ subText="200g pouches"
118
+ />
119
+ </RadioGroup>
120
+ </View>
121
+
122
+ <View style={styles.section}>
123
+ <Typography size="sm" weight="semiBold" color="tertiary">
124
+ Tag variants & icon
125
+ </Typography>
126
+ <RadioGroup name="tag-variants" defaultValue="1">
127
+ <RadioGroup.Radio
128
+ value="1"
129
+ label="Most popular"
130
+ subText="Recommended for most dogs"
131
+ tag={{ children: "Best value", variant: "promo", icon: StarRate }}
132
+ />
133
+ <RadioGroup.Radio
134
+ value="2"
135
+ label="Flexible plan"
136
+ subText="Change anytime"
137
+ tag={{ children: "New", variant: "success" }}
138
+ />
139
+ </RadioGroup>
140
+ </View>
141
+ </View>
142
+ )
143
+
98
144
  export const States = () => (
99
145
  <View style={styles.column}>
100
146
  <View style={styles.section}>
@@ -40,6 +40,24 @@ describe("Radio", () => {
40
40
  })
41
41
  })
42
42
 
43
+ describe("when rendering a tag", () => {
44
+ it("renders the tag when provided", () => {
45
+ renderWithTheme(
46
+ <Radio
47
+ value="chicken"
48
+ label="Chicken"
49
+ tag={{ children: "Save up to £12.20 per box" }}
50
+ />
51
+ )
52
+ expect(screen.getByText("Save up to £12.20 per box")).toBeInTheDocument()
53
+ })
54
+
55
+ it("does not render a tag when omitted", () => {
56
+ renderWithTheme(<Radio value="chicken" label="Chicken" />)
57
+ expect(screen.queryByText("Best value")).not.toBeInTheDocument()
58
+ })
59
+ })
60
+
43
61
  describe("when component is interactive", () => {
44
62
  it("calls onSelect when pressed", async () => {
45
63
  const user = userEvent.setup()
@@ -4,6 +4,7 @@ import styled from "@emotion/native"
4
4
  import { useTheme } from "@emotion/react"
5
5
  import { Typography } from "../../atoms/Typography"
6
6
  import { Illustration, type IllustrationProps } from "../../atoms/Illustration"
7
+ import { Tag, type TagProps } from "../../atoms/Tag"
7
8
 
8
9
  export type RadioOwnProps = {
9
10
  value: string
@@ -11,6 +12,7 @@ export type RadioOwnProps = {
11
12
  label?: React.ReactNode
12
13
  subText?: React.ReactNode
13
14
  asset?: IllustrationProps
15
+ tag?: TagProps
14
16
  disabled?: boolean
15
17
  selected?: boolean
16
18
  onSelect?: (value: string) => void
@@ -102,6 +104,13 @@ const StyledAssetWrapper = styled(View)({
102
104
  alignSelf: "center"
103
105
  })
104
106
 
107
+ const StyledTagWrapper = styled(View)<{ tagTopPadding: number }>(
108
+ ({ tagTopPadding }) => ({
109
+ alignItems: "flex-start",
110
+ paddingTop: tagTopPadding
111
+ })
112
+ )
113
+
105
114
  /**
106
115
  * Radio button component for single selection within a group.
107
116
  *
@@ -110,6 +119,7 @@ const StyledAssetWrapper = styled(View)({
110
119
  * @param {React.ReactNode} [label] - Main label text or content.
111
120
  * @param {React.ReactNode} [subText] - Optional descriptive subtext.
112
121
  * @param {IllustrationProps} [asset] - Optional illustration asset for tile variant.
122
+ * @param {TagProps} [tag] - Optional tag rendered below the label/subtext. Accepts the full Tag API (variant, icon, size). Hidden when omitted.
113
123
  * @param {boolean} [disabled=false] - Whether the radio is disabled.
114
124
  * @param {boolean} [selected=false] - Whether the radio is currently selected.
115
125
  * @param {(value: string) => void} [onSelect] - Callback when radio is selected.
@@ -124,6 +134,7 @@ export const Radio = React.forwardRef<View, RadioProps>(
124
134
  label,
125
135
  subText,
126
136
  asset,
137
+ tag,
127
138
  disabled = false,
128
139
  value,
129
140
  selected = false,
@@ -215,7 +226,7 @@ export const Radio = React.forwardRef<View, RadioProps>(
215
226
  )}
216
227
  </StyledRadioControl>
217
228
 
218
- {(label || subText) && (
229
+ {(label || subText || tag) && (
219
230
  <StyledTextContent
220
231
  textContentGap={parseTokenValue(spacing.content.gap)}
221
232
  >
@@ -232,6 +243,13 @@ export const Radio = React.forwardRef<View, RadioProps>(
232
243
  {subText}
233
244
  </Typography>
234
245
  )}
246
+ {tag && (
247
+ <StyledTagWrapper
248
+ tagTopPadding={parseTokenValue(spacing.content.tag.topPadding)}
249
+ >
250
+ <Tag {...tag} />
251
+ </StyledTagWrapper>
252
+ )}
235
253
  </StyledTextContent>
236
254
  )}
237
255