@butternutbox/pawprint-native 0.3.2 → 0.4.1

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 (31) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +14 -0
  3. package/dist/index.cjs +1012 -173
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +252 -5
  6. package/dist/index.d.ts +252 -5
  7. package/dist/index.js +1007 -173
  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/NumberField/NumberFieldInput.tsx +14 -24
  17. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.stories.tsx +275 -0
  18. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.test.tsx +198 -0
  19. package/src/components/molecules/ProductDisplayCard/ProductDisplayCard.tsx +292 -0
  20. package/src/components/molecules/ProductDisplayCard/index.ts +5 -0
  21. package/src/components/molecules/ProductListingCard/Badge.tsx +65 -0
  22. package/src/components/molecules/ProductListingCard/Grid.tsx +59 -0
  23. package/src/components/molecules/ProductListingCard/ProductListingCard.stories.tsx +209 -0
  24. package/src/components/molecules/ProductListingCard/ProductListingCard.tsx +235 -0
  25. package/src/components/molecules/ProductListingCard/index.ts +2 -0
  26. package/src/components/molecules/TabNavigation/TabNavigation.stories.tsx +183 -0
  27. package/src/components/molecules/TabNavigation/TabNavigation.tsx +354 -0
  28. package/src/components/molecules/TabNavigation/index.ts +7 -0
  29. package/src/components/molecules/index.ts +4 -0
  30. package/src/utils/index.ts +1 -0
  31. package/src/utils/token.ts +1 -0
@@ -0,0 +1,354 @@
1
+ import React from "react"
2
+ import {
3
+ View,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ ViewProps
8
+ } from "react-native"
9
+ import styled from "@emotion/native"
10
+ import { useTheme } from "@emotion/react"
11
+ import * as TabsPrimitive from "@rn-primitives/tabs"
12
+ import { Typography, type TypographyProps } from "../../atoms/Typography"
13
+ import { Animated, type AnimatedVariant } from "../Animated"
14
+
15
+ type TabNavigationLayout = "fixed" | "intrinsic"
16
+
17
+ // Typography's token-mode `color` is typed against the semantic text-colour
18
+ // set. The selected/unselected label colour (#a43260) is part of it, but the
19
+ // inactive label (Figma uses brown-5) is not — hence the cast below.
20
+ type TabLabelColour = Extract<TypographyProps, { token: object }>["color"]
21
+
22
+ type TabNavigationContextValue = {
23
+ layout: TabNavigationLayout
24
+ animation: AnimatedVariant
25
+ }
26
+
27
+ const TabNavigationContext = React.createContext<
28
+ TabNavigationContextValue | undefined
29
+ >(undefined)
30
+
31
+ const useTabNavigationContext = (): TabNavigationContextValue => {
32
+ const context = React.useContext(TabNavigationContext)
33
+
34
+ if (!context) {
35
+ throw new Error(
36
+ "TabNavigation.List, TabNavigation.Tab and TabNavigation.Panel must be used within a TabNavigation component."
37
+ )
38
+ }
39
+
40
+ return context
41
+ }
42
+
43
+ const parseTokenValue = (value: string): number => parseFloat(value)
44
+
45
+ type TabNavigationOwnProps = {
46
+ value?: string
47
+ defaultValue?: string
48
+ onValueChange?: (value: string) => void
49
+ layout?: TabNavigationLayout
50
+ animation?: AnimatedVariant
51
+ children?: React.ReactNode
52
+ }
53
+
54
+ export type TabNavigationProps = TabNavigationOwnProps &
55
+ Omit<ViewProps, keyof TabNavigationOwnProps>
56
+
57
+ type TabNavigationListOwnProps = {
58
+ children?: React.ReactNode
59
+ }
60
+
61
+ export type TabNavigationListProps = TabNavigationListOwnProps &
62
+ Omit<ViewProps, keyof TabNavigationListOwnProps>
63
+
64
+ type TabNavigationTabOwnProps = {
65
+ value: string
66
+ disabled?: boolean
67
+ children: React.ReactNode
68
+ }
69
+
70
+ export type TabNavigationTabProps = TabNavigationTabOwnProps &
71
+ Omit<ViewProps, keyof TabNavigationTabOwnProps>
72
+
73
+ type TabNavigationPanelOwnProps = {
74
+ value: string
75
+ children?: React.ReactNode
76
+ }
77
+
78
+ export type TabNavigationPanelProps = TabNavigationPanelOwnProps &
79
+ Omit<ViewProps, keyof TabNavigationPanelOwnProps>
80
+
81
+ const StyledFixedList = styled(View)({
82
+ flexDirection: "row",
83
+ alignItems: "stretch",
84
+ width: "100%"
85
+ })
86
+
87
+ const StyledTab = styled(Pressable)<{
88
+ tabHeight: number
89
+ tabMinWidth: number
90
+ tabPaddingTop: number
91
+ tabPaddingBottom: number
92
+ tabPaddingHorizontal: number
93
+ tabGap: number
94
+ tabBorderWidth: number
95
+ tabBorderColor: string
96
+ tabFlex?: number
97
+ }>(
98
+ ({
99
+ tabHeight,
100
+ tabMinWidth,
101
+ tabPaddingTop,
102
+ tabPaddingBottom,
103
+ tabPaddingHorizontal,
104
+ tabGap,
105
+ tabBorderWidth,
106
+ tabBorderColor,
107
+ tabFlex
108
+ }) => ({
109
+ flexDirection: "row",
110
+ alignItems: "center",
111
+ justifyContent: "center",
112
+ gap: tabGap,
113
+ height: tabHeight,
114
+ minWidth: tabMinWidth,
115
+ paddingTop: tabPaddingTop,
116
+ paddingBottom: tabPaddingBottom,
117
+ paddingHorizontal: tabPaddingHorizontal,
118
+ borderBottomWidth: tabBorderWidth,
119
+ borderBottomColor: tabBorderColor,
120
+ ...(tabFlex !== undefined ? { flex: tabFlex } : { flexShrink: 0 })
121
+ })
122
+ )
123
+
124
+ /**
125
+ * Wraps the tab buttons. In `fixed` layout the tabs share the available width
126
+ * equally; in `intrinsic` layout the tabs size to their content and the list
127
+ * scrolls horizontally when they overflow.
128
+ *
129
+ * Must be used within a `TabNavigation` component.
130
+ */
131
+ const TabNavigationList = React.forwardRef<View, TabNavigationListProps>(
132
+ ({ children, ...rest }, ref) => {
133
+ const { layout } = useTabNavigationContext()
134
+
135
+ if (layout === "intrinsic") {
136
+ return (
137
+ <TabsPrimitive.List asChild>
138
+ <View ref={ref} {...rest}>
139
+ <ScrollView
140
+ horizontal
141
+ showsHorizontalScrollIndicator={false}
142
+ contentContainerStyle={styles.scrollContent}
143
+ >
144
+ {children}
145
+ </ScrollView>
146
+ </View>
147
+ </TabsPrimitive.List>
148
+ )
149
+ }
150
+
151
+ return (
152
+ <TabsPrimitive.List asChild>
153
+ <StyledFixedList ref={ref} {...rest}>
154
+ {children}
155
+ </StyledFixedList>
156
+ </TabsPrimitive.List>
157
+ )
158
+ }
159
+ )
160
+
161
+ TabNavigationList.displayName = "TabNavigation.List"
162
+
163
+ /**
164
+ * An individual tab button within a `TabNavigation`. Selected tabs render with
165
+ * a brand bottom border and bold label; unselected tabs use a neutral border
166
+ * and semibold label. Disabled tabs are muted and non-interactive.
167
+ *
168
+ * Wraps `@rn-primitives/tabs` `Trigger` for accessibility (role, selected and
169
+ * disabled state). Unlike the web version there are no hover or focus-visible
170
+ * states (not applicable on mobile).
171
+ *
172
+ * @param {string} value - Unique identifier; matches the `value` of a `TabNavigation.Panel`.
173
+ * @param {boolean} [disabled=false] - Whether this tab is disabled.
174
+ * @param {React.ReactNode} children - Tab label (and optional leading content).
175
+ */
176
+ const TabNavigationTab = React.forwardRef<View, TabNavigationTabProps>(
177
+ ({ value, disabled = false, children, ...rest }, ref) => {
178
+ const { layout } = useTabNavigationContext()
179
+ const { value: selectedValue } = TabsPrimitive.useRootContext()
180
+ const theme = useTheme()
181
+ const { tabItem, tabitem } = theme.tokens.components.tabNavigation
182
+ const { spacing, colour, borderWidth, typography } = tabItem
183
+
184
+ const selected = selectedValue === value
185
+ const token = selected ? typography.selected : typography.default
186
+ const borderColour = selected
187
+ ? colour.border.default
188
+ : colour.border.disabled
189
+ // TODO: Add token — no component/semantic colour exists for the inactive
190
+ // tab label, so this falls back to the brown-5 primitive (the Figma value).
191
+ const textColour: TabLabelColour = disabled
192
+ ? (theme.tokens.primitives.colour.brand.brown[
193
+ "5"
194
+ ] as unknown as TabLabelColour)
195
+ : colour.text.default
196
+
197
+ return (
198
+ <TabsPrimitive.Trigger value={value} disabled={disabled} asChild>
199
+ <StyledTab
200
+ ref={ref}
201
+ tabHeight={parseTokenValue(tabitem.size.height)}
202
+ tabMinWidth={parseTokenValue(tabitem.size.minWidth)}
203
+ tabPaddingTop={parseTokenValue(spacing.topPadding)}
204
+ tabPaddingBottom={parseTokenValue(spacing.bottomPadding)}
205
+ tabPaddingHorizontal={parseTokenValue(spacing.horizontalPadding)}
206
+ tabGap={parseTokenValue(spacing.gap)}
207
+ tabBorderWidth={parseTokenValue(borderWidth.default)}
208
+ tabBorderColor={borderColour}
209
+ tabFlex={layout === "fixed" ? 1 : undefined}
210
+ {...rest}
211
+ >
212
+ <Typography token={token} color={textColour}>
213
+ {children}
214
+ </Typography>
215
+ </StyledTab>
216
+ </TabsPrimitive.Trigger>
217
+ )
218
+ }
219
+ )
220
+
221
+ TabNavigationTab.displayName = "TabNavigation.Tab"
222
+
223
+ /**
224
+ * Content area shown when the tab with the matching `value` is selected. Wraps
225
+ * `@rn-primitives/tabs` `Content` (which unmounts the inactive panels) and
226
+ * animates the content with the `Animated` atom (variant set by the parent
227
+ * `TabNavigation`'s `animation` prop). The enter animation replays each time
228
+ * the tab becomes active.
229
+ *
230
+ * @param {string} value - Matches the `value` of a `TabNavigation.Tab`.
231
+ * @param {React.ReactNode} [children] - Panel content.
232
+ */
233
+ const TabNavigationPanel = React.forwardRef<View, TabNavigationPanelProps>(
234
+ ({ value, children, ...rest }, ref) => {
235
+ const { animation } = useTabNavigationContext()
236
+
237
+ return (
238
+ <TabsPrimitive.Content value={value} asChild>
239
+ <Animated ref={ref} variant={animation} exit={false} {...rest}>
240
+ {children}
241
+ </Animated>
242
+ </TabsPrimitive.Content>
243
+ )
244
+ }
245
+ )
246
+
247
+ TabNavigationPanel.displayName = "TabNavigation.Panel"
248
+
249
+ /**
250
+ * Top-level navigation that organises content into mutually exclusive views.
251
+ * Selecting a tab replaces the content area below it. Wraps
252
+ * `@rn-primitives/tabs` for accessibility.
253
+ *
254
+ * Compose with the compound parts:
255
+ * - `TabNavigation.List` — wraps the tab buttons.
256
+ * - `TabNavigation.Tab` — an individual tab button.
257
+ * - `TabNavigation.Panel` — content shown for the matching tab, animated via
258
+ * the `Animated` atom.
259
+ *
260
+ * Note: API parity with the web `TabNavigation`. The native version uses a
261
+ * horizontal `ScrollView` for the intrinsic layout and has no hover or
262
+ * focus-visible states (not applicable on mobile).
263
+ *
264
+ * @param {string} [value] - Controlled selected tab value.
265
+ * @param {string} [defaultValue] - Uncontrolled initial selected tab value. Provide this (or `value`) so a tab is selected on mount.
266
+ * @param {(value: string) => void} [onValueChange] - Called when the selected tab changes.
267
+ * @param {"fixed" | "intrinsic"} [layout="fixed"] - `fixed`: tabs share the width equally. `intrinsic`: tabs size to content and the list scrolls horizontally when they overflow.
268
+ * @param {AnimatedVariant} [animation="slideIn"] - Animation preset applied to each panel's content.
269
+ *
270
+ * @example
271
+ * ```tsx
272
+ * import { TabNavigation } from "@butternutbox/pawprint-native"
273
+ *
274
+ * <TabNavigation defaultValue="give">
275
+ * <TabNavigation.List>
276
+ * <TabNavigation.Tab value="give">Give & get</TabNavigation.Tab>
277
+ * <TabNavigation.Tab value="pack">My pack</TabNavigation.Tab>
278
+ * </TabNavigation.List>
279
+ * <TabNavigation.Panel value="give">Give content</TabNavigation.Panel>
280
+ * <TabNavigation.Panel value="pack">Pack content</TabNavigation.Panel>
281
+ * </TabNavigation>
282
+ * ```
283
+ */
284
+ const TabNavigationRoot = React.forwardRef<View, TabNavigationProps>(
285
+ (
286
+ {
287
+ value,
288
+ defaultValue,
289
+ onValueChange,
290
+ layout = "fixed",
291
+ animation = "slideIn",
292
+ children,
293
+ ...rest
294
+ },
295
+ ref
296
+ ) => {
297
+ const isControlled = typeof value === "string"
298
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
299
+ () => defaultValue ?? ""
300
+ )
301
+ const currentValue = isControlled ? value : uncontrolledValue
302
+
303
+ const handleValueChange = React.useCallback(
304
+ (newValue: string) => {
305
+ if (!isControlled) {
306
+ setUncontrolledValue(newValue)
307
+ }
308
+ onValueChange?.(newValue)
309
+ },
310
+ [isControlled, onValueChange]
311
+ )
312
+
313
+ const contextValue = React.useMemo<TabNavigationContextValue>(
314
+ () => ({ layout, animation }),
315
+ [layout, animation]
316
+ )
317
+
318
+ return (
319
+ <TabNavigationContext.Provider value={contextValue}>
320
+ <TabsPrimitive.Root
321
+ ref={ref}
322
+ value={currentValue}
323
+ onValueChange={handleValueChange}
324
+ {...rest}
325
+ >
326
+ {children}
327
+ </TabsPrimitive.Root>
328
+ </TabNavigationContext.Provider>
329
+ )
330
+ }
331
+ )
332
+
333
+ TabNavigationRoot.displayName = "TabNavigation"
334
+
335
+ const styles = StyleSheet.create({
336
+ scrollContent: {
337
+ flexDirection: "row",
338
+ alignItems: "stretch"
339
+ }
340
+ })
341
+
342
+ type TabNavigationComponent = React.ForwardRefExoticComponent<
343
+ TabNavigationProps & React.RefAttributes<View>
344
+ > & {
345
+ List: typeof TabNavigationList
346
+ Tab: typeof TabNavigationTab
347
+ Panel: typeof TabNavigationPanel
348
+ }
349
+
350
+ export const TabNavigation = Object.assign(TabNavigationRoot, {
351
+ List: TabNavigationList,
352
+ Tab: TabNavigationTab,
353
+ Panel: TabNavigationPanel
354
+ }) as TabNavigationComponent
@@ -0,0 +1,7 @@
1
+ export { TabNavigation } from "./TabNavigation"
2
+ export type {
3
+ TabNavigationProps,
4
+ TabNavigationListProps,
5
+ TabNavigationTabProps,
6
+ TabNavigationPanelProps
7
+ } from "./TabNavigation"
@@ -19,3 +19,7 @@ export * from "./Tooltip"
19
19
  export * from "./MessageCard"
20
20
  export * from "./DatePicker"
21
21
  export * from "./PictureSelector"
22
+ export * from "./Countdown"
23
+ export * from "./ProductListingCard"
24
+ export * from "./ProductDisplayCard"
25
+ export * from "./TabNavigation"
@@ -0,0 +1 @@
1
+ export { parseTokenValue } from "./token"
@@ -0,0 +1 @@
1
+ export const parseTokenValue = (value: string): number => parseFloat(value)