@butternutbox/pawprint-native 0.10.10 → 0.11.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.10.10",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -1,16 +1,38 @@
1
1
  import React from "react"
2
2
 
3
3
  const AnimatedView = React.forwardRef<HTMLDivElement, Record<string, unknown>>(
4
- ({ children, style, ..._rest }, ref) => {
4
+ (
5
+ {
6
+ children,
7
+ style,
8
+ accessibilityRole,
9
+ accessibilityLabel,
10
+ accessibilityElementsHidden,
11
+ importantForAccessibility,
12
+ ..._rest
13
+ },
14
+ ref
15
+ ) => {
5
16
  const flatStyle = Array.isArray(style)
6
17
  ? Object.assign({}, ...style.filter(Boolean))
7
18
  : style
19
+
20
+ const domProps: Record<string, unknown> = {}
21
+ if (accessibilityRole) domProps.role = accessibilityRole
22
+ if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
23
+ if (
24
+ accessibilityElementsHidden ||
25
+ importantForAccessibility === "no-hide-descendants"
26
+ )
27
+ domProps["aria-hidden"] = true
28
+
8
29
  return (
9
30
  <div
10
31
  ref={ref}
11
32
  style={
12
33
  typeof flatStyle === "object" ? (flatStyle as object) : undefined
13
34
  }
35
+ {...domProps}
14
36
  >
15
37
  {children as React.ReactNode}
16
38
  </div>
@@ -35,6 +57,7 @@ export const runOnJS =
35
57
  fn(...args)
36
58
 
37
59
  export const withTiming = <T,>(value: T) => value
60
+ export const withSpring = <T,>(value: T) => value
38
61
 
39
62
  // Animation builder stub — supports chaining .duration() / .easing() / .delay()
40
63
  const createBuilder = () => {
@@ -63,6 +86,17 @@ export const FadeInRight = createBuilder()
63
86
  export const FadeOutRight = createBuilder()
64
87
  export const ZoomIn = createBuilder()
65
88
  export const ZoomOut = createBuilder()
89
+
90
+ // Keyframe stub — supports chaining .duration() / .delay()
91
+ export class Keyframe {
92
+ constructor(_definitions: Record<string, unknown>) {}
93
+ duration() {
94
+ return this
95
+ }
96
+ delay() {
97
+ return this
98
+ }
99
+ }
66
100
  export class ComplexAnimationBuilder {
67
101
  static duration() {
68
102
  return new ComplexAnimationBuilder()
@@ -9,6 +9,8 @@ export const View = React.forwardRef<HTMLDivElement, any>(
9
9
  accessibilityLabel,
10
10
  accessibilityState,
11
11
  accessibilityValue,
12
+ accessibilityElementsHidden,
13
+ importantForAccessibility,
12
14
  accessible: _accessible,
13
15
  children,
14
16
  style,
@@ -24,6 +26,11 @@ export const View = React.forwardRef<HTMLDivElement, any>(
24
26
  accessibilityRole === "adjustable" ? "slider" : accessibilityRole
25
27
  }
26
28
  if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
29
+ if (
30
+ accessibilityElementsHidden ||
31
+ importantForAccessibility === "no-hide-descendants"
32
+ )
33
+ domProps["aria-hidden"] = true
27
34
  if (accessibilityState?.disabled) domProps["aria-disabled"] = true
28
35
  if (accessibilityState?.selected) domProps["aria-selected"] = true
29
36
  if (accessibilityState?.checked !== undefined)
@@ -90,6 +97,8 @@ export const Pressable = React.forwardRef<HTMLButtonElement, any>(
90
97
  if (accessibilityLabel) domProps["aria-label"] = accessibilityLabel
91
98
  if (accessibilityState?.disabled) domProps["aria-disabled"] = true
92
99
  if (accessibilityState?.selected) domProps["aria-selected"] = true
100
+ if (accessibilityState?.checked !== undefined)
101
+ domProps["aria-checked"] = accessibilityState.checked
93
102
  if (accessibilityState?.expanded !== undefined)
94
103
  domProps["aria-expanded"] = accessibilityState.expanded
95
104
  if (accessibilityState?.busy) domProps["aria-busy"] = true
@@ -2,7 +2,6 @@ import React from "react"
2
2
  import { View, Pressable, PressableProps } from "react-native"
3
3
  import styled from "@emotion/native"
4
4
  import { useTheme } from "@emotion/react"
5
- import * as CheckboxPrimitive from "@rn-primitives/checkbox"
6
5
  import { Check } from "@butternutbox/pawprint-icons/core"
7
6
  import { Icon } from "../../atoms/Icon"
8
7
  import { Typography } from "../../atoms/Typography"
@@ -158,126 +157,124 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
158
157
  const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
159
158
  const isChecked = isControlled ? controlledChecked : internalChecked
160
159
 
161
- const handleCheckedChange = (checked: boolean) => {
160
+ const handlePress = () => {
161
+ if (disabled) return
162
+ const next = !isChecked
162
163
  if (!isControlled) {
163
- setInternalChecked(checked)
164
+ setInternalChecked(next)
164
165
  }
165
- onCheckedChange?.(checked)
166
+ onCheckedChange?.(next)
166
167
  }
167
168
 
168
169
  const isTile = variant === "tile"
169
170
  const controlSize = parseTokenValue(checkbox.size.control.default)
170
171
 
172
+ // A single Pressable owns the tap. The primitive's native build does not
173
+ // forward `asChild`, so wrapping it would nest a second Pressable that
174
+ // swallows the press on device — mirror Radio and drive state directly.
171
175
  return (
172
- <CheckboxPrimitive.Root
173
- checked={isChecked}
174
- onCheckedChange={handleCheckedChange}
176
+ <StyledRootPressable
177
+ ref={ref}
178
+ onPress={handlePress}
175
179
  disabled={disabled}
176
- asChild
180
+ accessibilityRole="checkbox"
181
+ accessibilityState={{ checked: isChecked, disabled }}
182
+ rootFlexAlign={isTile ? "center" : "flex-start"}
183
+ rootGap={parseTokenValue(
184
+ isTile ? checkbox.checkboxTile.spacing.gap : checkbox.spacing.gap
185
+ )}
186
+ rootOpacity={
187
+ disabled ? parseFloat(theme.tokens.primitives.opacity["40"]) : 1
188
+ }
189
+ rootPaddingVertical={
190
+ isTile
191
+ ? parseTokenValue(checkbox.checkboxTile.spacing.verticalPadding)
192
+ : undefined
193
+ }
194
+ rootPaddingHorizontal={
195
+ isTile
196
+ ? parseTokenValue(checkbox.checkboxTile.spacing.horizontalPadding)
197
+ : undefined
198
+ }
199
+ rootMaxWidth={
200
+ isTile
201
+ ? parseTokenValue(checkbox.checkboxTile.size.maxWidth)
202
+ : undefined
203
+ }
204
+ rootBgColor={
205
+ isTile
206
+ ? isChecked
207
+ ? checkbox.checkboxTile.colour.background.selected
208
+ : checkbox.checkboxTile.colour.background.default
209
+ : undefined
210
+ }
211
+ rootBorderWidth={
212
+ isTile
213
+ ? parseTokenValue(checkbox.checkboxTile.borderWidth.default)
214
+ : undefined
215
+ }
216
+ rootBorderColor={
217
+ isTile
218
+ ? isChecked
219
+ ? checkbox.checkboxTile.colour.border.selected
220
+ : checkbox.checkboxTile.colour.border.default
221
+ : undefined
222
+ }
223
+ rootBorderRadius={
224
+ isTile
225
+ ? parseTokenValue(checkbox.checkboxTile.borderRadius.default)
226
+ : undefined
227
+ }
228
+ style={typeof style === "function" ? undefined : style}
229
+ {...rest}
177
230
  >
178
- <StyledRootPressable
179
- ref={ref}
180
- rootFlexAlign={isTile ? "center" : "flex-start"}
181
- rootGap={parseTokenValue(
182
- isTile ? checkbox.checkboxTile.spacing.gap : checkbox.spacing.gap
183
- )}
184
- rootOpacity={
185
- disabled ? parseFloat(theme.tokens.primitives.opacity["40"]) : 1
186
- }
187
- rootPaddingVertical={
188
- isTile
189
- ? parseTokenValue(checkbox.checkboxTile.spacing.verticalPadding)
190
- : undefined
191
- }
192
- rootPaddingHorizontal={
193
- isTile
194
- ? parseTokenValue(checkbox.checkboxTile.spacing.horizontalPadding)
195
- : undefined
196
- }
197
- rootMaxWidth={
198
- isTile
199
- ? parseTokenValue(checkbox.checkboxTile.size.maxWidth)
200
- : undefined
201
- }
202
- rootBgColor={
203
- isTile
204
- ? isChecked
205
- ? checkbox.checkboxTile.colour.background.selected
206
- : checkbox.checkboxTile.colour.background.default
207
- : undefined
231
+ <StyledControl
232
+ controlSize={controlSize}
233
+ controlBorderWidth={parseTokenValue(checkbox.control.border.default)}
234
+ controlBorderColor={
235
+ isChecked
236
+ ? checkbox.colour.background.selected
237
+ : checkbox.colour.border.default
208
238
  }
209
- rootBorderWidth={
210
- isTile
211
- ? parseTokenValue(checkbox.checkboxTile.borderWidth.default)
212
- : undefined
239
+ controlBorderRadius={parseTokenValue(dimensions.borderRadius.xs)}
240
+ controlBgColor={
241
+ isChecked ? checkbox.colour.background.selected : "transparent"
213
242
  }
214
- rootBorderColor={
215
- isTile
216
- ? isChecked
217
- ? checkbox.checkboxTile.colour.border.selected
218
- : checkbox.checkboxTile.colour.border.default
219
- : undefined
220
- }
221
- rootBorderRadius={
222
- isTile
223
- ? parseTokenValue(checkbox.checkboxTile.borderRadius.default)
224
- : undefined
225
- }
226
- style={typeof style === "function" ? undefined : style}
227
- {...rest}
228
243
  >
229
- <StyledControl
230
- controlSize={controlSize}
231
- controlBorderWidth={parseTokenValue(
232
- checkbox.control.border.default
233
- )}
234
- controlBorderColor={
235
- isChecked
236
- ? checkbox.colour.background.selected
237
- : checkbox.colour.border.default
238
- }
239
- controlBorderRadius={parseTokenValue(dimensions.borderRadius.xs)}
240
- controlBgColor={
241
- isChecked ? checkbox.colour.background.selected : "transparent"
242
- }
243
- >
244
- <CheckboxPrimitive.Indicator>
245
- <Icon icon={Check} size="xs" colour="alt" />
246
- </CheckboxPrimitive.Indicator>
247
- </StyledControl>
244
+ {isChecked && <Icon icon={Check} size="xs" colour="alt" />}
245
+ </StyledControl>
248
246
 
249
- <StyledContent
250
- contentGap={parseTokenValue(checkbox.spacing.content.gap)}
251
- >
252
- {label && (
253
- <Typography
254
- token={checkbox.typography.label}
255
- color={checkbox.colour.text.title}
256
- >
257
- {label}
258
- </Typography>
259
- )}
260
- {subText && (
261
- <Typography
262
- token={checkbox.typography.subText}
263
- color={checkbox.colour.text.subtext}
264
- >
265
- {subText}
266
- </Typography>
267
- )}
268
- </StyledContent>
269
-
270
- {isTile && illustration && (
271
- <StyledIllustration
272
- illustrationSize={parseTokenValue(
273
- checkbox.checkboxTile.size.illustration.default
274
- )}
247
+ <StyledContent
248
+ contentGap={parseTokenValue(checkbox.spacing.content.gap)}
249
+ >
250
+ {label && (
251
+ <Typography
252
+ token={checkbox.typography.label}
253
+ color={checkbox.colour.text.title}
275
254
  >
276
- {illustration}
277
- </StyledIllustration>
255
+ {label}
256
+ </Typography>
278
257
  )}
279
- </StyledRootPressable>
280
- </CheckboxPrimitive.Root>
258
+ {subText && (
259
+ <Typography
260
+ token={checkbox.typography.subText}
261
+ color={checkbox.colour.text.subtext}
262
+ >
263
+ {subText}
264
+ </Typography>
265
+ )}
266
+ </StyledContent>
267
+
268
+ {isTile && illustration && (
269
+ <StyledIllustration
270
+ illustrationSize={parseTokenValue(
271
+ checkbox.checkboxTile.size.illustration.default
272
+ )}
273
+ >
274
+ {illustration}
275
+ </StyledIllustration>
276
+ )}
277
+ </StyledRootPressable>
281
278
  )
282
279
  }
283
280
  )
@@ -41,7 +41,7 @@ const StyledRoot = styled(View)<{
41
41
  fullWidth?: boolean
42
42
  }>(({ rootGap, fullWidth }) => ({
43
43
  gap: rootGap,
44
- ...(fullWidth && { width: "100%" })
44
+ ...(fullWidth ? { width: "100%" } : { alignItems: "center" })
45
45
  }))
46
46
 
47
47
  const StyledLabelGroup = styled(View)<{
@@ -134,6 +134,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
134
134
  <Typography
135
135
  token={typoTokens.label}
136
136
  color={tokens.colour.text.label}
137
+ align="center"
137
138
  >
138
139
  {label}
139
140
  </Typography>
@@ -142,6 +143,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
142
143
  <Typography
143
144
  token={typoTokens.smallLabel}
144
145
  color={tokens.colour.text.description}
146
+ align="center"
145
147
  >
146
148
  {smallLabel}
147
149
  </Typography>
@@ -172,6 +174,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
172
174
  : tokens.typography.small.smallLabel
173
175
  }
174
176
  color={tokens.colour.text.description}
177
+ align="center"
175
178
  >
176
179
  {description}
177
180
  </Typography>
@@ -184,6 +187,7 @@ export const NumberField = React.forwardRef<View, NumberFieldProps>(
184
187
  : tokens.typography.small.smallLabel
185
188
  }
186
189
  color={theme.tokens.components.inputs.colour.description.error}
190
+ align="center"
187
191
  >
188
192
  {error}
189
193
  </Typography>
@@ -0,0 +1,220 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { StackedNotifications } from "./StackedNotifications"
4
+ import type { StackedNotificationItem } from "./StackedNotifications"
5
+ import { Button } from "../../atoms/Button"
6
+ import { Tag } from "../../atoms/Tag"
7
+ import { Typography } from "../../atoms/Typography"
8
+
9
+ export default {
10
+ title: "Molecules/StackedNotifications",
11
+ component: StackedNotifications,
12
+ parameters: {
13
+ layout: "fullscreen"
14
+ },
15
+ argTypes: {
16
+ items: {
17
+ control: "object",
18
+ description:
19
+ "Active notifications, oldest first, newest last. Fully controlled."
20
+ },
21
+ bottomOffset: {
22
+ control: "number",
23
+ description: "Distance from the bottom edge in pixels"
24
+ }
25
+ }
26
+ }
27
+
28
+ // A bounded "screen" so the bottom-pinned stack is visible in the canvas.
29
+ // The component itself is position: absolute; bottom, so it needs a sized,
30
+ // relatively-positioned container to float within.
31
+ const Backdrop = ({
32
+ label = "Screen content — the notification floats over this, pinned to the bottom.",
33
+ children
34
+ }: {
35
+ label?: string
36
+ children?: React.ReactNode
37
+ }) => (
38
+ <View style={styles.backdrop}>
39
+ <Typography variant="body" size="md">
40
+ {label}
41
+ </Typography>
42
+ {children}
43
+ </View>
44
+ )
45
+
46
+ // --- Playground ---
47
+
48
+ const PLAYGROUND_ITEMS: StackedNotificationItem[] = [
49
+ {
50
+ id: "chicken",
51
+ type: "info",
52
+ message: "Chicken is a common protein — we'll avoid it in future boxes."
53
+ },
54
+ {
55
+ id: "beef",
56
+ type: "warning",
57
+ message: "Beef can cause reactions in some dogs. We've flagged it."
58
+ }
59
+ ]
60
+
61
+ export const Playground = {
62
+ args: {
63
+ items: PLAYGROUND_ITEMS
64
+ },
65
+ render: (args: React.ComponentProps<typeof StackedNotifications>) => (
66
+ <Backdrop>
67
+ <StackedNotifications {...args} />
68
+ </Backdrop>
69
+ )
70
+ }
71
+
72
+ // Stacked (peek)
73
+
74
+ const STACKED_ITEMS: StackedNotificationItem[] = [
75
+ {
76
+ id: "chicken",
77
+ type: "info",
78
+ message: "Chicken is a common protein — we'll avoid it in future boxes."
79
+ },
80
+ {
81
+ id: "beef",
82
+ type: "warning",
83
+ message: "Beef can cause reactions in some dogs. We've flagged it."
84
+ },
85
+ {
86
+ id: "dairy",
87
+ type: "info",
88
+ message: "Dairy selected — noted for your dog's plan."
89
+ },
90
+ {
91
+ id: "wheat",
92
+ type: "warning",
93
+ message: "Wheat can trigger reactions. We've flagged it for the vet team."
94
+ }
95
+ ]
96
+
97
+ export const Stacked = {
98
+ name: "Stacked (peek)",
99
+ render: () => (
100
+ <Backdrop label="Four notifications are queued, but only the front card plus two peeking behind it are shown.">
101
+ <StackedNotifications items={STACKED_ITEMS} />
102
+ </Backdrop>
103
+ )
104
+ }
105
+
106
+ // --- Variants ---
107
+
108
+ export const Variants = {
109
+ name: "Info & Warning",
110
+ render: () => (
111
+ <Backdrop>
112
+ <StackedNotifications
113
+ items={[
114
+ {
115
+ id: "info",
116
+ type: "info",
117
+ message: "Info notification — a positive, helpful message."
118
+ }
119
+ ]}
120
+ />
121
+ <View style={styles.spacer} />
122
+ <StackedNotifications
123
+ bottomOffset={120}
124
+ items={[
125
+ {
126
+ id: "warning",
127
+ type: "warning",
128
+ message: "Warning notification — a negative, cautionary message."
129
+ }
130
+ ]}
131
+ />
132
+ </Backdrop>
133
+ )
134
+ }
135
+
136
+ // --- Interactive: mimics the allergens wizard screen ---
137
+
138
+ const ALLERGENS = [
139
+ { id: "chicken", label: "Chicken", type: "info" as const },
140
+ { id: "beef", label: "Beef", type: "warning" as const },
141
+ { id: "dairy", label: "Dairy", type: "info" as const },
142
+ { id: "wheat", label: "Wheat", type: "warning" as const }
143
+ ]
144
+
145
+ const MESSAGES: Record<string, string> = {
146
+ chicken: "Chicken selected — we'll leave it out of future boxes.",
147
+ beef: "Beef can trigger reactions. We've flagged it for the vet team.",
148
+ dairy: "Dairy selected — noted for your dog's plan.",
149
+ wheat: "Wheat can trigger reactions. We've flagged it for the vet team."
150
+ }
151
+
152
+ export const Interactive = {
153
+ name: "Interactive (allergens)",
154
+ render: () => {
155
+ // Ordered list of selected ids — insertion order drives the LIFO stack.
156
+ const [selected, setSelected] = React.useState<string[]>([])
157
+
158
+ const toggle = (id: string) =>
159
+ setSelected((prev) =>
160
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
161
+ )
162
+
163
+ const items: StackedNotificationItem[] = selected.map((id) => {
164
+ const allergen = ALLERGENS.find((a) => a.id === id)
165
+ return {
166
+ id,
167
+ type: allergen?.type ?? "info",
168
+ message: MESSAGES[id]
169
+ }
170
+ })
171
+
172
+ return (
173
+ <Backdrop label="Tap an allergen to select it (adds a notification). Tap again to deselect (removes it and re-raises on reselect). Use the ✕ to dismiss and reveal the next.">
174
+ <View style={styles.chips}>
175
+ {ALLERGENS.map((allergen) => (
176
+ <Button
177
+ key={allergen.id}
178
+ size="sm"
179
+ variant={selected.includes(allergen.id) ? "filled" : "outlined"}
180
+ onPress={() => toggle(allergen.id)}
181
+ >
182
+ {allergen.label}
183
+ </Button>
184
+ ))}
185
+ </View>
186
+ <View style={styles.chips}>
187
+ {selected.map((id) => (
188
+ <Tag key={id}>{id}</Tag>
189
+ ))}
190
+ </View>
191
+ <StackedNotifications
192
+ items={items}
193
+ onDismiss={(id) => console.warn("dismissed", id)}
194
+ />
195
+ </Backdrop>
196
+ )
197
+ }
198
+ }
199
+
200
+ const styles = StyleSheet.create({
201
+ backdrop: {
202
+ position: "relative",
203
+ height: 360,
204
+ padding: 16,
205
+ gap: 16,
206
+ backgroundColor: "#f4f2ee",
207
+ borderWidth: 1,
208
+ borderColor: "#dcd7cf",
209
+ borderRadius: 8,
210
+ overflow: "hidden"
211
+ },
212
+ chips: {
213
+ flexDirection: "row",
214
+ flexWrap: "wrap",
215
+ gap: 8
216
+ },
217
+ spacer: {
218
+ height: 24
219
+ }
220
+ })