@butternutbox/pawprint-native 1.4.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -51,6 +51,10 @@ export const useSharedValue = <T,>(initial: T) => ({ value: initial })
51
51
 
52
52
  export const useAnimatedStyle = (_fn: () => object) => ({})
53
53
 
54
+ export const useReducedMotion = () => false
55
+
56
+ export const cancelAnimation = (_value: unknown) => {}
57
+
54
58
  export const runOnJS =
55
59
  <Args extends unknown[], R>(fn: (..._args: Args) => R) =>
56
60
  (...args: Args) =>
@@ -58,6 +62,7 @@ export const runOnJS =
58
62
 
59
63
  export const withTiming = <T,>(value: T) => value
60
64
  export const withSpring = <T,>(value: T) => value
65
+ export const withRepeat = <T,>(value: T) => value
61
66
 
62
67
  // Animation builder stub — supports chaining .duration() / .easing() / .delay()
63
68
  const createBuilder = () => {
@@ -25,3 +25,4 @@ export const Stop = passThrough("Stop")
25
25
  export const G = passThrough("G")
26
26
  export const Path = passThrough("Path")
27
27
  export const ClipPath = passThrough("ClipPath")
28
+ export const Polygon = passThrough("Polygon")
@@ -0,0 +1,21 @@
1
+ import React from "react"
2
+ import { ChefDogLoader } from "./ChefDogLoader"
3
+ import type { ChefDogLoaderProps } from "./ChefDogLoader"
4
+
5
+ export default {
6
+ title: "Atoms/ChefDogLoader",
7
+ component: ChefDogLoader,
8
+ argTypes: {
9
+ size: {
10
+ control: { type: "number" },
11
+ description: "Width and height in points"
12
+ }
13
+ }
14
+ }
15
+
16
+ export const Playground = (args: ChefDogLoaderProps) => (
17
+ <ChefDogLoader {...args} />
18
+ )
19
+ Playground.args = {
20
+ size: 240
21
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react"
2
+ import { View } from "react-native"
3
+ import { screen } from "@testing-library/react"
4
+ import { describe, it, expect } from "vitest"
5
+ import { renderWithTheme } from "../../../test-utils"
6
+ import { ChefDogLoader } from "./ChefDogLoader"
7
+
8
+ describe("ChefDogLoader", () => {
9
+ describe("when component is rendering", () => {
10
+ it("renders with progressbar role", () => {
11
+ renderWithTheme(<ChefDogLoader />)
12
+ expect(screen.getByRole("progressbar")).toBeInTheDocument()
13
+ })
14
+
15
+ it("has accessible loading label", () => {
16
+ renderWithTheme(<ChefDogLoader />)
17
+ expect(screen.getByLabelText("Loading")).toBeInTheDocument()
18
+ })
19
+
20
+ it("accepts a custom accessibility label", () => {
21
+ renderWithTheme(<ChefDogLoader accessibilityLabel="Fetching your plan" />)
22
+ expect(screen.getByLabelText("Fetching your plan")).toBeInTheDocument()
23
+ })
24
+ })
25
+
26
+ describe("when using with ref", () => {
27
+ it("forwards ref", () => {
28
+ const ref = React.createRef<View>()
29
+ renderWithTheme(<ChefDogLoader ref={ref} />)
30
+ expect(ref.current).toBeTruthy()
31
+ })
32
+ })
33
+ })
@@ -0,0 +1,115 @@
1
+ import React from "react"
2
+ import { StyleSheet, View, ViewProps } from "react-native"
3
+ import Svg, { ClipPath, Defs, G, Polygon } from "react-native-svg"
4
+ import Reanimated, {
5
+ cancelAnimation,
6
+ Easing,
7
+ useAnimatedStyle,
8
+ useReducedMotion,
9
+ useSharedValue,
10
+ withRepeat,
11
+ withTiming
12
+ } from "react-native-reanimated"
13
+ import { LabradorChefsHat } from "@butternutbox/pawprint-illustrations/breeds"
14
+
15
+ type ChefDogLoaderOwnProps = {
16
+ size?: number
17
+ }
18
+
19
+ type ChefDogLoaderProps = ChefDogLoaderOwnProps &
20
+ Omit<ViewProps, keyof ChefDogLoaderOwnProps>
21
+
22
+ const DEFAULT_SIZE = 240
23
+ const TAIL_REGION = "0,0 15.5,0 15.5,22.4 12,26.5 0,26.5"
24
+ const BODY_REGION = "15.5,0 48,0 48,48 0,48 0,26.5 12,26.5 15.5,22.4"
25
+ const TAIL_ORIGIN = { x: 13.4 / 48 - 0.5, y: 24.6 / 48 - 0.5 }
26
+ const TAIL_DOWN = -6
27
+ const TAIL_UP = 14
28
+ const WAG_DURATION = 180
29
+
30
+ /**
31
+ * The labrador in a chef's hat wagging its tail, for indeterminate loading states.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * import { ChefDogLoader } from "@butternutbox/pawprint-native"
36
+ *
37
+ * <ChefDogLoader />
38
+ * <ChefDogLoader size={160} />
39
+ * ```
40
+ *
41
+ * @param size - *(optional)* Width and height in points (default: 240)
42
+ */
43
+ const ChefDogLoader = React.forwardRef<View, ChefDogLoaderProps>(
44
+ ({ size = DEFAULT_SIZE, ...rest }, ref) => {
45
+ const reducedMotion = useReducedMotion()
46
+ const rotation = useSharedValue(TAIL_DOWN)
47
+ const clipId = React.useId().replace(/:/g, "")
48
+
49
+ React.useEffect(() => {
50
+ if (reducedMotion) return
51
+ rotation.value = withRepeat(
52
+ withTiming(TAIL_UP, {
53
+ duration: WAG_DURATION,
54
+ easing: Easing.inOut(Easing.quad)
55
+ }),
56
+ -1,
57
+ true
58
+ )
59
+ return () => cancelAnimation(rotation)
60
+ }, [reducedMotion, rotation])
61
+
62
+ const originX = TAIL_ORIGIN.x * size
63
+ const originY = TAIL_ORIGIN.y * size
64
+
65
+ const tailStyle = useAnimatedStyle(() => ({
66
+ transform: [
67
+ { translateX: originX },
68
+ { translateY: originY },
69
+ { rotate: `${rotation.value}deg` },
70
+ { translateX: -originX },
71
+ { translateY: -originY }
72
+ ]
73
+ }))
74
+
75
+ return (
76
+ <View
77
+ ref={ref}
78
+ accessibilityRole="progressbar"
79
+ accessibilityLabel="Loading"
80
+ {...rest}
81
+ >
82
+ <Svg width={size} height={size} viewBox="0 0 48 48">
83
+ <Defs>
84
+ <ClipPath id={`${clipId}-body`}>
85
+ <Polygon points={BODY_REGION} />
86
+ </ClipPath>
87
+ </Defs>
88
+ <G clipPath={`url(#${clipId}-body)`}>
89
+ <LabradorChefsHat />
90
+ </G>
91
+ </Svg>
92
+ <Reanimated.View
93
+ pointerEvents="none"
94
+ style={[StyleSheet.absoluteFill, tailStyle]}
95
+ >
96
+ <Svg width={size} height={size} viewBox="0 0 48 48">
97
+ <Defs>
98
+ <ClipPath id={`${clipId}-tail`}>
99
+ <Polygon points={TAIL_REGION} />
100
+ </ClipPath>
101
+ </Defs>
102
+ <G clipPath={`url(#${clipId}-tail)`}>
103
+ <LabradorChefsHat />
104
+ </G>
105
+ </Svg>
106
+ </Reanimated.View>
107
+ </View>
108
+ )
109
+ }
110
+ )
111
+
112
+ ChefDogLoader.displayName = "ChefDogLoader"
113
+
114
+ export { ChefDogLoader }
115
+ export type { ChefDogLoaderProps }
@@ -0,0 +1,2 @@
1
+ export { ChefDogLoader } from "./ChefDogLoader"
2
+ export type { ChefDogLoaderProps } from "./ChefDogLoader"
@@ -3,6 +3,7 @@ export * from "./CarouselControls"
3
3
  export * from "./Typography"
4
4
  export * from "./Badge"
5
5
  export * from "./Button"
6
+ export * from "./ChefDogLoader"
6
7
  export * from "./Divider"
7
8
  export * from "./Hint"
8
9
  export * from "./Icon"