@butternutbox/pawprint-native 1.3.1 → 1.5.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +21 -0
- package/dist/index.cjs +2668 -649
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -3
- package/dist/index.d.ts +81 -3
- package/dist/index.js +2483 -468
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__mocks__/react-native-reanimated.tsx +5 -0
- package/src/__mocks__/react-native-svg.tsx +1 -0
- package/src/components/atoms/Button/Button.stories.tsx +4 -4
- package/src/components/atoms/Button/Button.test.tsx +9 -2
- package/src/components/atoms/Button/Button.tsx +39 -7
- package/src/components/atoms/ChefDogLoader/ChefDogLoader.stories.tsx +21 -0
- package/src/components/atoms/ChefDogLoader/ChefDogLoader.test.tsx +33 -0
- package/src/components/atoms/ChefDogLoader/ChefDogLoader.tsx +115 -0
- package/src/components/atoms/ChefDogLoader/index.ts +2 -0
- package/src/components/atoms/index.ts +1 -0
- package/src/components/molecules/NavigationHeader/NavigationHeader.stories.tsx +140 -0
- package/src/components/molecules/NavigationHeader/NavigationHeader.test.tsx +110 -0
- package/src/components/molecules/NavigationHeader/NavigationHeader.tsx +273 -0
- package/src/components/molecules/NavigationHeader/NavigationHeaderButton.tsx +87 -0
- package/src/components/molecules/NavigationHeader/NavigationHeaderContext.ts +38 -0
- package/src/components/molecules/NavigationHeader/index.ts +14 -0
- package/src/components/molecules/index.ts +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import Svg, { Path } from "react-native-svg"
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
|
5
|
+
import styled from "@emotion/native"
|
|
6
|
+
import { useTheme, type Theme } from "@emotion/react"
|
|
7
|
+
import { ArrowBack } from "@butternutbox/pawprint-icons/core"
|
|
8
|
+
import { IconButton } from "../../atoms/IconButton"
|
|
9
|
+
import { Typography } from "../../atoms/Typography"
|
|
10
|
+
import {
|
|
11
|
+
NavigationHeaderButton,
|
|
12
|
+
NavigationHeaderIconButton
|
|
13
|
+
} from "./NavigationHeaderButton"
|
|
14
|
+
import {
|
|
15
|
+
NavigationHeaderContext,
|
|
16
|
+
getIconButtonColour,
|
|
17
|
+
type NavigationHeaderContextValue,
|
|
18
|
+
type NavigationHeaderSurface
|
|
19
|
+
} from "./NavigationHeaderContext"
|
|
20
|
+
|
|
21
|
+
type NavigationHeaderVariant = "primary" | "secondary"
|
|
22
|
+
|
|
23
|
+
type NavigationHeaderOwnProps = {
|
|
24
|
+
title: string
|
|
25
|
+
variant?: NavigationHeaderVariant
|
|
26
|
+
surface?: NavigationHeaderSurface
|
|
27
|
+
onBack?: () => void
|
|
28
|
+
backLabel?: string
|
|
29
|
+
actions?: React.ReactNode
|
|
30
|
+
showRadiusPips?: boolean
|
|
31
|
+
safeAreaTop?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type NavigationHeaderProps = NavigationHeaderOwnProps &
|
|
35
|
+
Omit<ViewProps, keyof NavigationHeaderOwnProps | "children">
|
|
36
|
+
|
|
37
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
38
|
+
|
|
39
|
+
const getSurfaceColour = (theme: Theme, surface: NavigationHeaderSurface) => {
|
|
40
|
+
const { navigationHeader } = theme.tokens.components
|
|
41
|
+
const { background } = theme.tokens.semantics.colour
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
brand: navigationHeader.colour.surface.default,
|
|
45
|
+
subtle: background.surface.secondary,
|
|
46
|
+
plain: background.surface.default
|
|
47
|
+
}[surface]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const StyledNavContainer = styled(View)<{
|
|
51
|
+
containerPaddingTop: number
|
|
52
|
+
containerPaddingV: number
|
|
53
|
+
containerPaddingH: number
|
|
54
|
+
containerGap: number
|
|
55
|
+
containerBgColor: string
|
|
56
|
+
}>(
|
|
57
|
+
({
|
|
58
|
+
containerPaddingTop,
|
|
59
|
+
containerPaddingV,
|
|
60
|
+
containerPaddingH,
|
|
61
|
+
containerGap,
|
|
62
|
+
containerBgColor
|
|
63
|
+
}) => ({
|
|
64
|
+
flexDirection: "row",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
paddingTop: containerPaddingTop + containerPaddingV,
|
|
67
|
+
paddingBottom: containerPaddingV,
|
|
68
|
+
paddingHorizontal: containerPaddingH,
|
|
69
|
+
gap: containerGap,
|
|
70
|
+
backgroundColor: containerBgColor
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const StyledTitleGroup = styled(View)<{
|
|
75
|
+
titleGroupGap: number
|
|
76
|
+
titleGroupMinHeight: number
|
|
77
|
+
}>(({ titleGroupGap, titleGroupMinHeight }) => ({
|
|
78
|
+
flex: 1,
|
|
79
|
+
flexDirection: "row",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
minWidth: 0,
|
|
82
|
+
minHeight: titleGroupMinHeight,
|
|
83
|
+
gap: titleGroupGap
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
const StyledTitle = styled(View)({
|
|
87
|
+
flex: 1,
|
|
88
|
+
minWidth: 0
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const StyledActionGroup = styled(View)<{ actionGroupGap: number }>(
|
|
92
|
+
({ actionGroupGap }) => ({
|
|
93
|
+
flexDirection: "row",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
flexShrink: 0,
|
|
96
|
+
gap: actionGroupGap
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const StyledRadiusPips = styled(View)({
|
|
101
|
+
flexDirection: "row",
|
|
102
|
+
justifyContent: "space-between"
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const RadiusPips = ({ radius, colour }: { radius: number; colour: string }) => (
|
|
106
|
+
<StyledRadiusPips
|
|
107
|
+
accessible={false}
|
|
108
|
+
importantForAccessibility="no-hide-descendants"
|
|
109
|
+
>
|
|
110
|
+
<Svg width={radius} height={radius}>
|
|
111
|
+
<Path
|
|
112
|
+
d={`M0 0H${radius}A${radius} ${radius} 0 0 0 0 ${radius}Z`}
|
|
113
|
+
fill={colour}
|
|
114
|
+
/>
|
|
115
|
+
</Svg>
|
|
116
|
+
<Svg width={radius} height={radius}>
|
|
117
|
+
<Path
|
|
118
|
+
d={`M${radius} 0H0A${radius} ${radius} 0 0 1 ${radius} ${radius}Z`}
|
|
119
|
+
fill={colour}
|
|
120
|
+
/>
|
|
121
|
+
</Svg>
|
|
122
|
+
</StyledRadiusPips>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Navigation header for the native app. Gives users context of where they
|
|
127
|
+
* are, a way back, and the most important page-level actions.
|
|
128
|
+
*
|
|
129
|
+
* - `primary` — landing pages: large title, no back button.
|
|
130
|
+
* - `secondary` — child pages: back button and a smaller title.
|
|
131
|
+
*
|
|
132
|
+
* Actions are passed via `actions`. Use `NavigationHeader.Button` and
|
|
133
|
+
* `NavigationHeader.IconButton` for the header defaults, which follow the
|
|
134
|
+
* surface; their `variant` and `colour` can be overridden. Keep to at most
|
|
135
|
+
* three.
|
|
136
|
+
*
|
|
137
|
+
* Note: Unlike the web version, there is no desktop layout, the header extends
|
|
138
|
+
* behind the status bar by applying the top safe area inset (`safeAreaTop`),
|
|
139
|
+
* and `onBack` is a press handler.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} title - Page title, truncated to one line.
|
|
142
|
+
* @param {"primary" | "secondary"} [variant="primary"] - Header type.
|
|
143
|
+
* @param {"brand" | "subtle" | "plain"} [surface="brand"] - Background colour.
|
|
144
|
+
* `brand` is the yellow navigation surface, `subtle` the cream page surface
|
|
145
|
+
* and `plain` the default page surface.
|
|
146
|
+
* @param {() => void} [onBack] - Back button press handler. `secondary` only.
|
|
147
|
+
* @param {string} [backLabel="Back"] - Accessible label for the back button.
|
|
148
|
+
* The back button is `tertiary` on the `brand` surface and `secondary`
|
|
149
|
+
* otherwise.
|
|
150
|
+
* @param {React.ReactNode} [actions] - Right-hand action group.
|
|
151
|
+
* @param {boolean} [showRadiusPips] - Shows the rounded corners below the
|
|
152
|
+
* header. Defaults to `true` on the `brand` surface only.
|
|
153
|
+
* @param {boolean} [safeAreaTop=true] - Pads the header by the top safe area
|
|
154
|
+
* inset.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* import { NavigationHeader } from "@butternutbox/pawprint-native"
|
|
159
|
+
*
|
|
160
|
+
* <NavigationHeader
|
|
161
|
+
* variant="secondary"
|
|
162
|
+
* title="Recipes"
|
|
163
|
+
* onBack={navigation.goBack}
|
|
164
|
+
* actions={
|
|
165
|
+
* <NavigationHeader.Button onPress={editQuantity}>
|
|
166
|
+
* Pouch quantity
|
|
167
|
+
* </NavigationHeader.Button>
|
|
168
|
+
* }
|
|
169
|
+
* />
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
const NavigationHeaderRoot = React.forwardRef<View, NavigationHeaderProps>(
|
|
173
|
+
(
|
|
174
|
+
{
|
|
175
|
+
title,
|
|
176
|
+
variant = "primary",
|
|
177
|
+
surface = "brand",
|
|
178
|
+
onBack,
|
|
179
|
+
backLabel = "Back",
|
|
180
|
+
actions,
|
|
181
|
+
showRadiusPips = surface === "brand",
|
|
182
|
+
safeAreaTop = true,
|
|
183
|
+
...rest
|
|
184
|
+
},
|
|
185
|
+
ref
|
|
186
|
+
) => {
|
|
187
|
+
const theme = useTheme()
|
|
188
|
+
const { top } = useSafeAreaInsets()
|
|
189
|
+
const { navigationHeader, buttons } = theme.tokens.components
|
|
190
|
+
const { spacing } = navigationHeader
|
|
191
|
+
const surfaceColour = getSurfaceColour(theme, surface)
|
|
192
|
+
const contextValue = React.useMemo<NavigationHeaderContextValue>(
|
|
193
|
+
() => ({ surface }),
|
|
194
|
+
[surface]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<NavigationHeaderContext.Provider value={contextValue}>
|
|
199
|
+
<View ref={ref} {...rest}>
|
|
200
|
+
<StyledNavContainer
|
|
201
|
+
containerPaddingTop={safeAreaTop ? top : 0}
|
|
202
|
+
containerPaddingV={parseTokenValue(
|
|
203
|
+
spacing.navContainer.verticalPadding
|
|
204
|
+
)}
|
|
205
|
+
containerPaddingH={parseTokenValue(
|
|
206
|
+
spacing.navContainer.horizontalPadding
|
|
207
|
+
)}
|
|
208
|
+
containerGap={parseTokenValue(spacing.navContainer.gap)}
|
|
209
|
+
containerBgColor={surfaceColour}
|
|
210
|
+
>
|
|
211
|
+
<StyledTitleGroup
|
|
212
|
+
titleGroupGap={parseTokenValue(spacing.titleGroup.gap)}
|
|
213
|
+
titleGroupMinHeight={parseTokenValue(buttons.size.md.minHeight)}
|
|
214
|
+
>
|
|
215
|
+
{variant === "secondary" && (
|
|
216
|
+
<IconButton
|
|
217
|
+
icon={ArrowBack}
|
|
218
|
+
variant="filled"
|
|
219
|
+
colour={getIconButtonColour(contextValue)}
|
|
220
|
+
size="md"
|
|
221
|
+
aria-label={backLabel}
|
|
222
|
+
accessibilityRole="button"
|
|
223
|
+
onPress={onBack}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
<StyledTitle>
|
|
227
|
+
<Typography
|
|
228
|
+
token={navigationHeader.typography[variant]}
|
|
229
|
+
color={navigationHeader.colour.text.default}
|
|
230
|
+
accessibilityRole="header"
|
|
231
|
+
noWrap
|
|
232
|
+
>
|
|
233
|
+
{title}
|
|
234
|
+
</Typography>
|
|
235
|
+
</StyledTitle>
|
|
236
|
+
</StyledTitleGroup>
|
|
237
|
+
{actions && (
|
|
238
|
+
<StyledActionGroup
|
|
239
|
+
actionGroupGap={parseTokenValue(spacing.actionGroup.gap)}
|
|
240
|
+
>
|
|
241
|
+
{actions}
|
|
242
|
+
</StyledActionGroup>
|
|
243
|
+
)}
|
|
244
|
+
</StyledNavContainer>
|
|
245
|
+
{showRadiusPips && (
|
|
246
|
+
<RadiusPips
|
|
247
|
+
radius={parseTokenValue(
|
|
248
|
+
theme.tokens.semantics.dimensions.borderRadius.lg
|
|
249
|
+
)}
|
|
250
|
+
colour={surfaceColour}
|
|
251
|
+
/>
|
|
252
|
+
)}
|
|
253
|
+
</View>
|
|
254
|
+
</NavigationHeaderContext.Provider>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
NavigationHeaderRoot.displayName = "NavigationHeader"
|
|
260
|
+
|
|
261
|
+
type NavigationHeaderComponent = React.ForwardRefExoticComponent<
|
|
262
|
+
NavigationHeaderProps & React.RefAttributes<View>
|
|
263
|
+
> & {
|
|
264
|
+
Button: typeof NavigationHeaderButton
|
|
265
|
+
IconButton: typeof NavigationHeaderIconButton
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const NavigationHeader = Object.assign(NavigationHeaderRoot, {
|
|
269
|
+
Button: NavigationHeaderButton,
|
|
270
|
+
IconButton: NavigationHeaderIconButton
|
|
271
|
+
}) as NavigationHeaderComponent
|
|
272
|
+
|
|
273
|
+
export type { NavigationHeaderVariant, NavigationHeaderSurface }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import { Button, type ButtonProps } from "../../atoms/Button"
|
|
4
|
+
import { IconButton, type IconButtonProps } from "../../atoms/IconButton"
|
|
5
|
+
import {
|
|
6
|
+
getButtonDefaults,
|
|
7
|
+
getIconButtonColour,
|
|
8
|
+
useNavigationHeaderContext
|
|
9
|
+
} from "./NavigationHeaderContext"
|
|
10
|
+
|
|
11
|
+
export type NavigationHeaderButtonProps = Omit<
|
|
12
|
+
ButtonProps,
|
|
13
|
+
"size" | "fullWidth"
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type NavigationHeaderIconButtonProps = Omit<IconButtonProps, "size">
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Text button for the navigation header action group. A medium `Button` with
|
|
20
|
+
* the header defaults: white `filled` / `tertiary` on the `brand` surface and
|
|
21
|
+
* `outlined` on other surfaces.
|
|
22
|
+
*
|
|
23
|
+
* Passing `variant` or `colour` overrides the default.
|
|
24
|
+
*
|
|
25
|
+
* @param {"filled" | "outlined" | "text"} [variant] - Button style.
|
|
26
|
+
* @param {"primary" | "secondary" | "tertiary"} [colour] - Filled colour.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <NavigationHeader.Button onPress={editQuantity}>
|
|
31
|
+
* Pouch quantity
|
|
32
|
+
* </NavigationHeader.Button>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const NavigationHeaderButton = React.forwardRef<
|
|
36
|
+
View,
|
|
37
|
+
NavigationHeaderButtonProps
|
|
38
|
+
>(({ variant, colour, ...props }, ref) => {
|
|
39
|
+
const defaults = getButtonDefaults(useNavigationHeaderContext())
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Button
|
|
43
|
+
ref={ref}
|
|
44
|
+
size="md"
|
|
45
|
+
variant={variant ?? defaults.variant}
|
|
46
|
+
colour={colour ?? defaults.colour}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
NavigationHeaderButton.displayName = "NavigationHeader.Button"
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Icon button for the navigation header action group. A medium filled
|
|
56
|
+
* `IconButton`, `tertiary` on the `brand` surface and `secondary` on other
|
|
57
|
+
* surfaces.
|
|
58
|
+
*
|
|
59
|
+
* Passing `variant` or `colour` overrides the default.
|
|
60
|
+
*
|
|
61
|
+
* @param {PawprintIcon} icon - **(required)** Icon component.
|
|
62
|
+
* @param {string} aria-label - **(required)** Accessible label.
|
|
63
|
+
* @param {"filled" | "outlined" | "text"} [variant="filled"] - Button style.
|
|
64
|
+
* @param {"primary" | "secondary" | "tertiary"} [colour] - Filled colour.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* <NavigationHeader.IconButton icon={Add} aria-label="Add dog" />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export const NavigationHeaderIconButton = React.forwardRef<
|
|
72
|
+
View,
|
|
73
|
+
NavigationHeaderIconButtonProps
|
|
74
|
+
>(({ colour, ...props }, ref) => {
|
|
75
|
+
const context = useNavigationHeaderContext()
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<IconButton
|
|
79
|
+
ref={ref}
|
|
80
|
+
size="md"
|
|
81
|
+
colour={colour ?? getIconButtonColour(context)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
NavigationHeaderIconButton.displayName = "NavigationHeader.IconButton"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { ButtonColour, ButtonVariant } from "../../atoms/Button"
|
|
3
|
+
import type { IconButtonColour } from "../../atoms/IconButton"
|
|
4
|
+
|
|
5
|
+
type NavigationHeaderSurface = "brand" | "subtle" | "plain"
|
|
6
|
+
|
|
7
|
+
type NavigationHeaderContextValue = {
|
|
8
|
+
surface: NavigationHeaderSurface
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const NavigationHeaderContext =
|
|
12
|
+
React.createContext<NavigationHeaderContextValue>({ surface: "brand" })
|
|
13
|
+
|
|
14
|
+
const useNavigationHeaderContext = () =>
|
|
15
|
+
React.useContext(NavigationHeaderContext)
|
|
16
|
+
|
|
17
|
+
const getButtonDefaults = ({
|
|
18
|
+
surface
|
|
19
|
+
}: NavigationHeaderContextValue): {
|
|
20
|
+
variant: ButtonVariant
|
|
21
|
+
colour: ButtonColour
|
|
22
|
+
} =>
|
|
23
|
+
surface === "brand"
|
|
24
|
+
? { variant: "filled", colour: "tertiary" }
|
|
25
|
+
: { variant: "outlined", colour: "primary" }
|
|
26
|
+
|
|
27
|
+
const getIconButtonColour = ({
|
|
28
|
+
surface
|
|
29
|
+
}: NavigationHeaderContextValue): IconButtonColour =>
|
|
30
|
+
surface === "brand" ? "tertiary" : "secondary"
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
NavigationHeaderContext,
|
|
34
|
+
useNavigationHeaderContext,
|
|
35
|
+
getButtonDefaults,
|
|
36
|
+
getIconButtonColour
|
|
37
|
+
}
|
|
38
|
+
export type { NavigationHeaderSurface, NavigationHeaderContextValue }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { NavigationHeader } from "./NavigationHeader"
|
|
2
|
+
export type {
|
|
3
|
+
NavigationHeaderProps,
|
|
4
|
+
NavigationHeaderVariant,
|
|
5
|
+
NavigationHeaderSurface
|
|
6
|
+
} from "./NavigationHeader"
|
|
7
|
+
export {
|
|
8
|
+
NavigationHeaderButton,
|
|
9
|
+
NavigationHeaderIconButton
|
|
10
|
+
} from "./NavigationHeaderButton"
|
|
11
|
+
export type {
|
|
12
|
+
NavigationHeaderButtonProps,
|
|
13
|
+
NavigationHeaderIconButtonProps
|
|
14
|
+
} from "./NavigationHeaderButton"
|