@butternutbox/pawprint-native 0.5.1 → 0.7.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.1",
3
+ "version": "0.7.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}
@@ -0,0 +1,98 @@
1
+ import React, { useState } from "react"
2
+ import { View } from "react-native"
3
+ import { NativeSelectPicker } from "./NativeSelectPicker"
4
+
5
+ export default {
6
+ title: "Molecules/NativeSelectPicker",
7
+ component: NativeSelectPicker,
8
+ argTypes: {
9
+ disabled: {
10
+ control: "boolean"
11
+ },
12
+ hideIcon: {
13
+ control: "boolean"
14
+ }
15
+ },
16
+ decorators: [
17
+ (Story: React.ComponentType) => (
18
+ <View style={{ padding: 20, flex: 1, justifyContent: "center" }}>
19
+ <Story />
20
+ </View>
21
+ )
22
+ ]
23
+ }
24
+
25
+ const BANK_OPTIONS = [
26
+ { label: "ABN Amro", value: "abn_amro" },
27
+ { label: "ASN Bank", value: "asn_bank" },
28
+ { label: "bunq B.V.", value: "bunq" },
29
+ { label: "ING Bank", value: "ing" },
30
+ { label: "Knab", value: "knab" },
31
+ { label: "Rabobank", value: "rabobank" }
32
+ ]
33
+
34
+ export const Basic = () => {
35
+ const [value, setValue] = useState<string | number | null>(null)
36
+ return (
37
+ <NativeSelectPicker
38
+ items={BANK_OPTIONS}
39
+ placeholder={{ label: "Select a bank" }}
40
+ value={value}
41
+ onValueChange={setValue}
42
+ />
43
+ )
44
+ }
45
+
46
+ export const WithSelectedValue = () => {
47
+ const [value, setValue] = useState<string | number | null>("ing")
48
+ return (
49
+ <NativeSelectPicker
50
+ items={BANK_OPTIONS}
51
+ placeholder={{ label: "Select a bank" }}
52
+ value={value}
53
+ onValueChange={setValue}
54
+ />
55
+ )
56
+ }
57
+
58
+ export const Disabled = () => {
59
+ const [value, setValue] = useState<string | number | null>(null)
60
+ return (
61
+ <NativeSelectPicker
62
+ items={BANK_OPTIONS}
63
+ placeholder={{ label: "Select a bank" }}
64
+ value={value}
65
+ onValueChange={setValue}
66
+ disabled
67
+ />
68
+ )
69
+ }
70
+
71
+ export const HideIcon = () => {
72
+ const [value, setValue] = useState<string | number | null>(null)
73
+ return (
74
+ <NativeSelectPicker
75
+ items={BANK_OPTIONS}
76
+ placeholder={{ label: "Select a bank" }}
77
+ value={value}
78
+ onValueChange={setValue}
79
+ hideIcon
80
+ />
81
+ )
82
+ }
83
+
84
+ export const NumericValues = () => {
85
+ const [value, setValue] = useState<string | number | null>(null)
86
+ return (
87
+ <NativeSelectPicker
88
+ items={[
89
+ { label: "Option 1", value: 1 },
90
+ { label: "Option 2", value: 2 },
91
+ { label: "Option 3", value: 3 }
92
+ ]}
93
+ placeholder={{ label: "Select an option" }}
94
+ value={value}
95
+ onValueChange={setValue}
96
+ />
97
+ )
98
+ }
@@ -0,0 +1,127 @@
1
+ import styled from "@emotion/native"
2
+ import { View, Text } from "react-native"
3
+ import Animated from "react-native-reanimated"
4
+ import { DEFAULT_THEME_OPTIONS } from "@butternutbox/pawprint-tokens"
5
+
6
+ type AndroidOptionTextProps = {
7
+ isSelected: boolean
8
+ }
9
+
10
+ const themeTokens = DEFAULT_THEME_OPTIONS.tokens
11
+ const { components, semantics } = themeTokens
12
+ const inputTokens = components.inputs
13
+ const { dimensions } = semantics
14
+
15
+ const parseSize = (value: string): number => {
16
+ return parseInt(value.replace("px", ""), 10)
17
+ }
18
+
19
+ const Wrapper = styled(View)({
20
+ borderWidth: parseSize(inputTokens.borderWidth.field.selected),
21
+ borderColor: inputTokens.colour.field.border.selected,
22
+ borderRadius: parseSize(inputTokens.borderRadius.field.default),
23
+ backgroundColor: inputTokens.colour.field.background.default,
24
+ paddingHorizontal: parseSize(inputTokens.spacing.field.horizontalPadding),
25
+ paddingVertical: parseSize(inputTokens.spacing.field.verticalPadding),
26
+ height: parseSize(inputTokens.size.field.height),
27
+ flexDirection: "row",
28
+ alignItems: "center",
29
+ justifyContent: "space-between"
30
+ })
31
+
32
+ const SelectIcon = styled(Animated.View)({
33
+ position: "absolute",
34
+ right: 0,
35
+ marginRight: 0
36
+ })
37
+
38
+ const InputText = styled(Text)({
39
+ color: inputTokens.colour.field.text.default,
40
+ fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
41
+ fontWeight: inputTokens.field.placeholder.default.fontWeight,
42
+ flex: 1
43
+ })
44
+
45
+ const PlaceholderText = styled(Text)({
46
+ color: inputTokens.colour.field.text.placeholder,
47
+ fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
48
+ fontWeight: inputTokens.field.placeholder.default.fontWeight,
49
+ flex: 1
50
+ })
51
+
52
+ const ModalOverlay = styled(View)({
53
+ flex: 1,
54
+ backgroundColor: "rgba(0, 0, 0, 0.32)",
55
+ justifyContent: "flex-end"
56
+ })
57
+
58
+ const AndroidDialog = styled(View)({
59
+ backgroundColor: inputTokens.colour.field.background.default,
60
+ borderTopLeftRadius: parseSize(dimensions.borderRadius.lg),
61
+ borderTopRightRadius: parseSize(dimensions.borderRadius.lg),
62
+ maxHeight: "80%",
63
+ minHeight: "40%",
64
+ paddingTop: parseSize(dimensions.spacing.md),
65
+ paddingBottom: parseSize(dimensions.spacing.xl),
66
+ paddingHorizontal: parseSize(dimensions.spacing.md)
67
+ })
68
+
69
+ const DialogTitle = styled(Text)({
70
+ fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
71
+ fontWeight: inputTokens.text.label.fontWeight,
72
+ color: inputTokens.colour.field.text.default,
73
+ marginBottom: parseSize(dimensions.spacing.md),
74
+ marginLeft: parseSize(dimensions.spacing.xs)
75
+ })
76
+
77
+ const AndroidOptionRow = styled(View)({
78
+ flexDirection: "row",
79
+ alignItems: "center",
80
+ paddingVertical: parseSize(dimensions.spacing.md),
81
+ paddingHorizontal: parseSize(dimensions.spacing.sm),
82
+ borderBottomWidth: parseSize(dimensions.borderWidth.sm),
83
+ borderBottomColor: themeTokens.primitives.colour.brand.brown[2]
84
+ })
85
+
86
+ const RadioOuter = styled(View)({
87
+ width: parseSize(dimensions.spacing.lg),
88
+ height: parseSize(dimensions.spacing.lg),
89
+ borderRadius: parseSize(dimensions.spacing.lg) / 2,
90
+ borderWidth: parseSize(dimensions.borderWidth.md),
91
+ borderColor: inputTokens.colour.field.border.selected,
92
+ alignItems: "center",
93
+ justifyContent: "center",
94
+ marginRight: parseSize(dimensions.spacing.sm)
95
+ })
96
+
97
+ const RadioInner = styled(View)({
98
+ width: parseSize(dimensions.spacing.md) / 2,
99
+ height: parseSize(dimensions.spacing.md) / 2,
100
+ borderRadius: parseSize(dimensions.spacing.md) / 4,
101
+ backgroundColor: inputTokens.colour.field.border.selected
102
+ })
103
+
104
+ const AndroidOptionText = styled(Text)<AndroidOptionTextProps>(
105
+ ({ isSelected }) => ({
106
+ fontSize: parseSize(inputTokens.field.placeholder.default.fontSize),
107
+ fontWeight: isSelected
108
+ ? inputTokens.text.label.fontWeight
109
+ : inputTokens.field.placeholder.default.fontWeight,
110
+ color: inputTokens.colour.field.text.default,
111
+ flex: 1
112
+ })
113
+ )
114
+
115
+ export {
116
+ Wrapper,
117
+ SelectIcon,
118
+ InputText,
119
+ PlaceholderText,
120
+ ModalOverlay,
121
+ AndroidDialog,
122
+ DialogTitle,
123
+ AndroidOptionRow,
124
+ RadioOuter,
125
+ RadioInner,
126
+ AndroidOptionText
127
+ }
@@ -0,0 +1,250 @@
1
+ import React, { useCallback, useMemo, useState } from "react"
2
+ import {
3
+ Platform,
4
+ ActionSheetIOS,
5
+ Modal,
6
+ TouchableOpacity,
7
+ FlatList,
8
+ TouchableWithoutFeedback,
9
+ View,
10
+ ViewProps
11
+ } from "react-native"
12
+ import { useSharedValue, useAnimatedStyle } from "react-native-reanimated"
13
+ import { KeyboardArrowDown } from "@butternutbox/pawprint-icons/core"
14
+ import { DEFAULT_THEME_OPTIONS } from "@butternutbox/pawprint-tokens"
15
+
16
+ import * as S from "./NativeSelectPicker.styled"
17
+
18
+ const themeTokens = DEFAULT_THEME_OPTIONS.tokens
19
+ const { components, semantics } = themeTokens
20
+ const inputTokens = components.inputs
21
+ const { dimensions } = semantics
22
+
23
+ const parseSize = (value: string): number => {
24
+ return parseInt(value.replace("px", ""), 10)
25
+ }
26
+
27
+ export type NativeSelectPickerItem = {
28
+ label: string
29
+ value: string | number
30
+ key?: string | number
31
+ }
32
+
33
+ export type NativeSelectPickerProps = ViewProps & {
34
+ items: NativeSelectPickerItem[]
35
+ value?: string | number | null
36
+ placeholder?: {
37
+ label?: string
38
+ value?: string | number
39
+ }
40
+ onValueChange: (value: string | number) => void
41
+ disabled?: boolean
42
+ hideIcon?: boolean
43
+ }
44
+
45
+ type IconAnimation = {
46
+ iconAnimationStyles: {
47
+ transform: Array<{ rotate: string }>
48
+ }
49
+ }
50
+
51
+ const useIconAnimation = (): IconAnimation => {
52
+ const iconRotation = useSharedValue(0)
53
+
54
+ const iconAnimationStyles = useAnimatedStyle(() => {
55
+ "worklet"
56
+ return {
57
+ transform: [{ rotate: `${iconRotation.value}deg` }]
58
+ }
59
+ })
60
+
61
+ return {
62
+ iconAnimationStyles
63
+ }
64
+ }
65
+
66
+ /**
67
+ * NativeSelectPicker is a native UI dropdown component that uses:
68
+ * - ActionSheetIOS on iOS (native system picker)
69
+ * - Modal with FlatList on Android
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * <NativeSelectPicker
74
+ * items={[
75
+ * { label: "Option 1", value: "opt1" },
76
+ * { label: "Option 2", value: "opt2" }
77
+ * ]}
78
+ * placeholder={{ label: "Select an option" }}
79
+ * value={selectedValue}
80
+ * onValueChange={setSelectedValue}
81
+ * />
82
+ * ```
83
+ */
84
+ const NativeSelectPicker = React.forwardRef<View, NativeSelectPickerProps>(
85
+ (
86
+ {
87
+ items,
88
+ value,
89
+ placeholder,
90
+ onValueChange,
91
+ disabled = false,
92
+ hideIcon = false,
93
+ ...rest
94
+ },
95
+ ref
96
+ ) => {
97
+ const [isOpen, setIsOpen] = useState(false)
98
+ const { iconAnimationStyles } = useIconAnimation()
99
+
100
+ const hasPlaceholder =
101
+ placeholder &&
102
+ Object.keys(placeholder).length > 0 &&
103
+ placeholder.label !== undefined
104
+
105
+ const allItems = useMemo(() => {
106
+ if (hasPlaceholder) {
107
+ return [
108
+ {
109
+ label: placeholder.label!,
110
+ value: placeholder.value ?? "",
111
+ key: "__placeholder__"
112
+ },
113
+ ...items
114
+ ]
115
+ }
116
+ return items
117
+ }, [items, hasPlaceholder, placeholder])
118
+
119
+ const selectedItem = allItems.find((item) => item.value === value)
120
+ const displayLabel =
121
+ selectedItem?.label ?? (hasPlaceholder ? placeholder.label! : "")
122
+
123
+ const handleOpenIOS = useCallback(() => {
124
+ if (disabled) return
125
+
126
+ const options = [...allItems.map((item) => item.label), "Cancel"]
127
+ const cancelButtonIndex = options.length - 1
128
+
129
+ ActionSheetIOS.showActionSheetWithOptions(
130
+ {
131
+ options,
132
+ cancelButtonIndex
133
+ },
134
+ (buttonIndex) => {
135
+ if (buttonIndex !== cancelButtonIndex) {
136
+ onValueChange(allItems[buttonIndex].value)
137
+ }
138
+ }
139
+ )
140
+ }, [disabled, allItems, onValueChange])
141
+
142
+ const handleOpenAndroid = useCallback(() => {
143
+ if (!disabled) {
144
+ setIsOpen(true)
145
+ }
146
+ }, [disabled])
147
+
148
+ const handleClose = useCallback(() => {
149
+ setIsOpen(false)
150
+ }, [])
151
+
152
+ const handleSelect = useCallback(
153
+ (itemValue: string | number) => {
154
+ onValueChange(itemValue)
155
+ setIsOpen(false)
156
+ },
157
+ [onValueChange]
158
+ )
159
+
160
+ return (
161
+ <S.Wrapper ref={ref} {...rest}>
162
+ <TouchableOpacity
163
+ activeOpacity={0.8}
164
+ onPress={Platform.OS === "ios" ? handleOpenIOS : handleOpenAndroid}
165
+ disabled={disabled}
166
+ hitSlop={{
167
+ top: parseSize(dimensions.spacing.md),
168
+ right: parseSize(dimensions.spacing.md),
169
+ bottom: parseSize(dimensions.spacing.md),
170
+ left: parseSize(dimensions.spacing.md)
171
+ }}
172
+ accessibilityRole="button"
173
+ style={{ flexDirection: "row", alignItems: "center" }}
174
+ >
175
+ <S.InputText numberOfLines={1}>
176
+ {displayLabel || (
177
+ <S.PlaceholderText>
178
+ {placeholder?.label || "Select"}
179
+ </S.PlaceholderText>
180
+ )}
181
+ </S.InputText>
182
+ {!hideIcon && (
183
+ <S.SelectIcon style={[iconAnimationStyles]}>
184
+ <KeyboardArrowDown
185
+ width={parseSize(dimensions.spacing.xl)}
186
+ height={parseSize(dimensions.spacing.xl)}
187
+ color={inputTokens.colour.field.text.default}
188
+ />
189
+ </S.SelectIcon>
190
+ )}
191
+ </TouchableOpacity>
192
+
193
+ {Platform.OS === "android" && (
194
+ <Modal
195
+ visible={isOpen}
196
+ transparent
197
+ animationType="fade"
198
+ onRequestClose={handleClose}
199
+ >
200
+ <TouchableWithoutFeedback
201
+ accessibilityRole="button"
202
+ onPress={handleClose}
203
+ >
204
+ <S.ModalOverlay>
205
+ <TouchableWithoutFeedback>
206
+ <S.AndroidDialog>
207
+ <S.DialogTitle>{"Select an option"}</S.DialogTitle>
208
+ <FlatList
209
+ data={allItems}
210
+ keyExtractor={(item, index) =>
211
+ item.key?.toString() ?? `${item.value}-${index}`
212
+ }
213
+ renderItem={({ item }) => {
214
+ const isSelected = item.value === value
215
+
216
+ return (
217
+ <TouchableOpacity
218
+ onPress={() => handleSelect(item.value)}
219
+ activeOpacity={0.7}
220
+ accessibilityRole="button"
221
+ >
222
+ <S.AndroidOptionRow>
223
+ <S.RadioOuter>
224
+ {isSelected && <S.RadioInner />}
225
+ </S.RadioOuter>
226
+ <S.AndroidOptionText isSelected={isSelected}>
227
+ {item.label}
228
+ </S.AndroidOptionText>
229
+ </S.AndroidOptionRow>
230
+ </TouchableOpacity>
231
+ )
232
+ }}
233
+ style={{
234
+ maxHeight: parseSize(dimensions.spacing["8xl"])
235
+ }}
236
+ />
237
+ </S.AndroidDialog>
238
+ </TouchableWithoutFeedback>
239
+ </S.ModalOverlay>
240
+ </TouchableWithoutFeedback>
241
+ </Modal>
242
+ )}
243
+ </S.Wrapper>
244
+ )
245
+ }
246
+ )
247
+
248
+ NativeSelectPicker.displayName = "NativeSelectPicker"
249
+
250
+ export { NativeSelectPicker }
@@ -0,0 +1,5 @@
1
+ export { NativeSelectPicker } from "./NativeSelectPicker"
2
+ export type {
3
+ NativeSelectPickerProps,
4
+ NativeSelectPickerItem
5
+ } from "./NativeSelectPicker"
@@ -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}>