@butternutbox/pawprint-native 0.8.0 → 0.10.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.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -324,6 +324,13 @@ export const Platform = {
324
324
  select: (obj: any) => obj.ios ?? obj.default
325
325
  }
326
326
 
327
+ export const Keyboard = {
328
+ addListener: (_event: string, _handler: () => void) => ({
329
+ remove: () => {}
330
+ }),
331
+ dismiss: () => {}
332
+ }
333
+
327
334
  // ─── Type exports (re-exported as empty for TS compat) ───────────────────
328
335
  export type ViewProps = any
329
336
  export type TextProps = any
@@ -1,10 +1,24 @@
1
1
  import React from "react"
2
2
 
3
+ const SelectCtx = React.createContext<{
4
+ open: boolean
5
+ setOpen: (open: boolean) => void
6
+ }>({ open: false, setOpen: () => {} })
7
+
3
8
  export const Root = React.forwardRef<any, any>(
4
9
  (
5
- { value, defaultValue, onValueChange, disabled, children, ...rest },
10
+ {
11
+ value,
12
+ defaultValue,
13
+ onValueChange,
14
+ disabled,
15
+ children,
16
+ onOpenChange,
17
+ ...rest
18
+ },
6
19
  ref
7
20
  ) => {
21
+ const [open, setOpenState] = React.useState(false)
8
22
  const [internalValue, setInternalValue] = React.useState(
9
23
  value ?? defaultValue ?? null
10
24
  )
@@ -17,12 +31,19 @@ export const Root = React.forwardRef<any, any>(
17
31
  }
18
32
  }
19
33
 
34
+ const setOpen = (nextOpen: boolean) => {
35
+ setOpenState(nextOpen)
36
+ onOpenChange?.(nextOpen)
37
+ }
38
+
20
39
  return (
21
- <div ref={ref} data-disabled={disabled} {...rest}>
22
- {typeof children === "function"
23
- ? children({ value: currentValue, onChange: handleChange })
24
- : children}
25
- </div>
40
+ <SelectCtx.Provider value={{ open, setOpen }}>
41
+ <div ref={ref} data-disabled={disabled} {...rest}>
42
+ {typeof children === "function"
43
+ ? children({ value: currentValue, onChange: handleChange })
44
+ : children}
45
+ </div>
46
+ </SelectCtx.Provider>
26
47
  )
27
48
  }
28
49
  )
@@ -30,11 +51,18 @@ Root.displayName = "SelectPrimitive.Root"
30
51
 
31
52
  export const Trigger = React.forwardRef<any, any>(
32
53
  ({ asChild, children, ...rest }, ref) => {
54
+ const { open, setOpen } = React.useContext(SelectCtx)
55
+ const handlePress = () => setOpen(!open)
56
+
33
57
  if (asChild && React.isValidElement(children)) {
34
- return React.cloneElement(children, { ref, ...rest } as any)
58
+ return React.cloneElement(children, {
59
+ ref,
60
+ onPress: handlePress,
61
+ ...rest
62
+ } as any)
35
63
  }
36
64
  return (
37
- <button ref={ref} role="button" {...rest}>
65
+ <button ref={ref} role="button" onClick={handlePress} {...rest}>
38
66
  {children}
39
67
  </button>
40
68
  )
@@ -59,22 +87,20 @@ Value.displayName = "SelectPrimitive.Value"
59
87
  export const Portal = ({ children }: any) => <>{children}</>
60
88
  Portal.displayName = "SelectPrimitive.Portal"
61
89
 
62
- export const Viewport = React.forwardRef<any, any>(
63
- ({ children, ...rest }, ref) => (
64
- <div ref={ref} {...rest}>
65
- {children}
66
- </div>
67
- )
68
- )
69
- Viewport.displayName = "SelectPrimitive.Viewport"
70
-
71
90
  export const Overlay = React.forwardRef<any, any>(
72
- ({ asChild, children, ...rest }, ref) => {
73
- if (asChild && React.isValidElement(children)) {
74
- return React.cloneElement(children, { ref, ...rest } as any)
91
+ ({ onPress, closeOnPress = true, children, ...rest }, ref) => {
92
+ const { setOpen } = React.useContext(SelectCtx)
93
+ const handlePress = () => {
94
+ if (closeOnPress) setOpen(false)
95
+ onPress?.()
75
96
  }
76
97
  return (
77
- <div ref={ref} {...rest}>
98
+ <div
99
+ ref={ref}
100
+ data-testid="select-overlay"
101
+ onClick={handlePress}
102
+ {...rest}
103
+ >
78
104
  {children}
79
105
  </div>
80
106
  )
@@ -82,8 +108,19 @@ export const Overlay = React.forwardRef<any, any>(
82
108
  )
83
109
  Overlay.displayName = "SelectPrimitive.Overlay"
84
110
 
111
+ export const Viewport = React.forwardRef<any, any>(
112
+ ({ children, ...rest }, ref) => (
113
+ <div ref={ref} {...rest}>
114
+ {children}
115
+ </div>
116
+ )
117
+ )
118
+ Viewport.displayName = "SelectPrimitive.Viewport"
119
+
85
120
  export const Content = React.forwardRef<any, any>(
86
121
  ({ asChild, children, ...rest }, ref) => {
122
+ const { open } = React.useContext(SelectCtx)
123
+ if (!open) return null
87
124
  if (asChild && React.isValidElement(children)) {
88
125
  return React.cloneElement(children, { ref, ...rest } as any)
89
126
  }
@@ -13,6 +13,7 @@ import {
13
13
  Error as ErrorIcon,
14
14
  CheckCircle as SuccessIcon
15
15
  } from "@butternutbox/pawprint-icons/core"
16
+ import { resolveFont } from "../../../fonts"
16
17
  import { Icon } from "../Icon"
17
18
 
18
19
  export type InputState = "default" | "error" | "success"
@@ -61,18 +62,24 @@ const StyledInputWrapper = styled(Animated.View)<{
61
62
 
62
63
  const StyledInput = styled(TextInput)(({ theme }) => {
63
64
  const { colour, field } = theme.tokens.components.inputs
64
- const placeholderTypography = field.placeholder.default
65
+ const typography = field.placeholder.default
66
+ const { fontFamily } = resolveFont(
67
+ typography.fontFamily,
68
+ typography.fontWeight
69
+ )
70
+ const fontSize = parseTokenValue(typography.fontSize)
71
+ const letterSpacing = typography.letterSpacing?.endsWith("%")
72
+ ? (parseFloat(typography.letterSpacing) / 100) * fontSize
73
+ : parseFloat(typography.letterSpacing ?? "0")
65
74
  return {
66
75
  flex: 1,
67
76
  minWidth: 0,
68
77
  minHeight: 0,
69
78
  padding: 0,
70
79
  color: colour.field.text.default,
71
- fontSize: parseTokenValue(placeholderTypography.fontSize),
72
- fontFamily: placeholderTypography.fontFamily,
73
- fontWeight: placeholderTypography.fontWeight,
74
- lineHeight: parseTokenValue(placeholderTypography.lineHeight),
75
- letterSpacing: parseTokenValue(placeholderTypography.letterSpacing),
80
+ fontFamily,
81
+ fontSize,
82
+ letterSpacing,
76
83
  // Suppress the browser default focus outline on React Native Web —
77
84
  // the focus indicator is rendered on the wrapper instead.
78
85
  ...({
@@ -275,3 +282,27 @@ export const InputField = React.forwardRef<TextInput, InputFieldProps>(
275
282
  )
276
283
 
277
284
  InputField.displayName = "Input.Field"
285
+
286
+ /**
287
+ * Bare text input with design-system typography and colors applied.
288
+ * No container, border, or padding — use inside components that provide
289
+ * their own visual wrapper (e.g. SelectField trigger).
290
+ */
291
+ export const InputText = React.forwardRef<TextInput, TextInputProps>(
292
+ ({ placeholderTextColor, ...rest }, ref) => {
293
+ const theme = useTheme()
294
+ const { colour } = theme.tokens.components.inputs
295
+
296
+ return (
297
+ <StyledInput
298
+ ref={ref}
299
+ placeholderTextColor={
300
+ placeholderTextColor ?? colour.field.text.placeholder
301
+ }
302
+ {...rest}
303
+ />
304
+ )
305
+ }
306
+ )
307
+
308
+ InputText.displayName = "Input.Text"
@@ -2,7 +2,7 @@ export { Input } from "./Input"
2
2
  export type { InputProps } from "./Input"
3
3
  export { InputLabel } from "./InputLabel"
4
4
  export type { InputLabelProps } from "./InputLabel"
5
- export { InputField } from "./InputField"
5
+ export { InputField, InputText } from "./InputField"
6
6
  export type { InputFieldProps } from "./InputField"
7
7
  export { InputDescription } from "./InputDescription"
8
8
  export type { InputDescriptionProps } from "./InputDescription"
@@ -3,7 +3,8 @@ import { View, StyleSheet } from "react-native"
3
3
  import { ProductDisplayCard } from "./ProductDisplayCard"
4
4
  import type { ProductDisplayCardProps } from "./ProductDisplayCard"
5
5
 
6
- const placeholderImageUrl = "https://placeholder.co/100x100?text=Recipe"
6
+ const placeholderImageUrl =
7
+ "https://placehold.co/148x148/EEE/31343C?font=oswald&text=product"
7
8
 
8
9
  const styles = StyleSheet.create({
9
10
  column: { flexDirection: "column", gap: 24, padding: 16 }
@@ -13,11 +14,6 @@ export default {
13
14
  title: "Molecules/ProductDisplayCard",
14
15
  component: ProductDisplayCard,
15
16
  argTypes: {
16
- device: {
17
- control: { type: "select" },
18
- options: ["desktop", "mobile"],
19
- description: "Device variant affecting sizing and typography"
20
- },
21
17
  title: {
22
18
  control: { type: "text" },
23
19
  description: "Product title"
@@ -38,10 +34,6 @@ export default {
38
34
  control: { type: "boolean" },
39
35
  description: "Show/hide quantity picker"
40
36
  },
41
- showImageQuantityBadge: {
42
- control: { type: "boolean" },
43
- description: "Show/hide quantity badge overlay on image"
44
- },
45
37
  disableQuantityButtons: {
46
38
  control: { type: "boolean" },
47
39
  description: "Disable quantity buttons"
@@ -50,14 +42,6 @@ export default {
50
42
  control: { type: "boolean" },
51
43
  description: "Hide quantity buttons"
52
44
  },
53
- showIncrementButton: {
54
- control: { type: "boolean" },
55
- description: "Show/hide increment button"
56
- },
57
- showDecrementButton: {
58
- control: { type: "boolean" },
59
- description: "Show/hide decrement button"
60
- },
61
45
  incrementDisabled: {
62
46
  control: { type: "boolean" },
63
47
  description: "Disable increment button"
@@ -100,7 +84,6 @@ export const Playground = (args: ProductDisplayCardProps) => (
100
84
  </View>
101
85
  )
102
86
  Playground.args = {
103
- device: "mobile",
104
87
  title: "Recipe Name",
105
88
  subtext: "+£0.00/pouch",
106
89
  showSubtext: true,
@@ -127,20 +110,6 @@ export const Default = () => (
127
110
  </View>
128
111
  )
129
112
 
130
- export const Desktop = () => (
131
- <View style={styles.column}>
132
- <ProductDisplayCard
133
- title="Recipe Name"
134
- subtext="+£0.00/pouch"
135
- showSubtext
136
- quantity={1}
137
- showQuantityPicker
138
- showBanner={false}
139
- image={placeholderImageUrl}
140
- />
141
- </View>
142
- )
143
-
144
113
  export const Mobile = () => (
145
114
  <View style={styles.column}>
146
115
  <ProductDisplayCard
@@ -202,32 +171,6 @@ export const NoSubtext = () => (
202
171
  </View>
203
172
  )
204
173
 
205
- export const WithImageQuantityBadge = () => (
206
- <View style={styles.column}>
207
- <ProductDisplayCard
208
- title="Gobble Gobble Turkey"
209
- subtext="+£0.50/pouch"
210
- showSubtext
211
- quantity={4}
212
- showQuantityPicker={false}
213
- showImageQuantityBadge
214
- showBanner={false}
215
- image={placeholderImageUrl}
216
- />
217
-
218
- <ProductDisplayCard
219
- title="Salmon Delight"
220
- subtext="+£0.25/pouch"
221
- showSubtext
222
- quantity={2}
223
- showQuantityPicker
224
- showImageQuantityBadge
225
- showBanner={false}
226
- image={placeholderImageUrl}
227
- />
228
- </View>
229
- )
230
-
231
174
  export const AllVariants = () => (
232
175
  <View style={styles.column}>
233
176
  <ProductDisplayCard
@@ -263,27 +206,40 @@ export const AllVariants = () => (
263
206
  </View>
264
207
  )
265
208
 
266
- export const Borderless = () => (
209
+ export const WithMinQuantity = () => (
267
210
  <View style={styles.column}>
268
211
  <ProductDisplayCard
269
- title="Recipe Name"
270
- subtext="+£0.00/pouch"
212
+ title="Bulk Recipe Pack"
213
+ subtext="+£2.50/pouch"
271
214
  showSubtext
272
- quantity={1}
215
+ quantity={5}
216
+ minQuantity={5}
273
217
  showQuantityPicker
274
- borderless
275
- image={placeholderImageUrl}
276
- />
277
-
278
- <ProductDisplayCard
279
- title="Premium Recipe"
280
- subtext="+£0.50/pouch"
281
- showSubtext
282
- quantity={2}
283
- showQuantityPicker
284
- showImageQuantityBadge
285
- borderless
218
+ showBanner
219
+ bannerType="info"
220
+ showBannerIcon={false}
221
+ banner="Minimum quantity: 5 pouches required"
286
222
  image={placeholderImageUrl}
287
223
  />
288
224
  </View>
289
225
  )
226
+
227
+ export const WithImagePress = () => {
228
+ const [pressed, setPressed] = React.useState(false)
229
+ return (
230
+ <View style={styles.column}>
231
+ <ProductDisplayCard
232
+ title="Recipe Name"
233
+ subtext="+£0.00/pouch"
234
+ showSubtext
235
+ quantity={1}
236
+ showQuantityPicker
237
+ onImagePress={() => setPressed(true)}
238
+ showBanner={pressed}
239
+ bannerType="success"
240
+ banner="Image pressed!"
241
+ image={placeholderImageUrl}
242
+ />
243
+ </View>
244
+ )
245
+ }
@@ -95,13 +95,7 @@ describe("ProductDisplayCard (Native)", () => {
95
95
  expect(screen.queryByText("Test Banner")).toBeFalsy()
96
96
  })
97
97
 
98
- it("renders with desktop device variant by default", () => {
99
- renderWithTheme(<ProductDisplayCard title="Test Product" />)
100
-
101
- expect(screen.getByText("Test Product")).toBeTruthy()
102
- })
103
-
104
- it("renders with mobile device variant when specified", () => {
98
+ it("renders as a mobile card", () => {
105
99
  renderWithTheme(<ProductDisplayCard title="Test Product" />)
106
100
 
107
101
  expect(screen.getByText("Test Product")).toBeTruthy()