@butternutbox/pawprint-native 0.0.1 → 0.2.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 +15 -15
- package/CHANGELOG.md +30 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12413 -1459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1111 -13
- package/dist/index.d.ts +1111 -13
- package/dist/index.js +12365 -1457
- package/dist/index.js.map +1 -1
- package/package.json +29 -11
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Illustration/Illustration.tsx +3 -3
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Link/Link.tsx +7 -6
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +28 -17
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, Pressable, ViewProps, PressableProps } from "react-native"
|
|
3
|
+
import Svg, { Path } from "react-native-svg"
|
|
4
|
+
import styled from "@emotion/native"
|
|
5
|
+
import { useTheme } from "@emotion/react"
|
|
6
|
+
import { Illustration } from "../../atoms/Illustration"
|
|
7
|
+
import { Typography } from "../../atoms/Typography"
|
|
8
|
+
import { MessageCard } from "../MessageCard"
|
|
9
|
+
|
|
10
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type PawprintIllustration = React.ComponentType<{
|
|
13
|
+
width?: number
|
|
14
|
+
height?: number
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
export type PictureSelectorItem = {
|
|
18
|
+
value: string
|
|
19
|
+
illustration?: PawprintIllustration
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
ariaLabel?: string
|
|
22
|
+
insightTitle?: string
|
|
23
|
+
insightDescription?: React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PictureSelectorOwnProps = {
|
|
27
|
+
label?: string
|
|
28
|
+
items: PictureSelectorItem[]
|
|
29
|
+
value?: string
|
|
30
|
+
defaultValue?: string
|
|
31
|
+
onValueChange?: (value: string) => void
|
|
32
|
+
/** Optional fixed card width (px). Defaults to `100%` (fluid). */
|
|
33
|
+
itemWidth?: number
|
|
34
|
+
/** Card aspect ratio (width / height). Defaults to `1` (square). */
|
|
35
|
+
itemAspectRatio?: number
|
|
36
|
+
/** Optional floor on each card's width (px). No default. */
|
|
37
|
+
itemMinWidth?: number
|
|
38
|
+
/** Optional floor on each card's height (px). No default. */
|
|
39
|
+
itemMinHeight?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type PictureSelectorProps = PictureSelectorOwnProps &
|
|
43
|
+
Omit<ViewProps, keyof PictureSelectorOwnProps>
|
|
44
|
+
|
|
45
|
+
type PressableOwnForItem = Omit<PressableProps, "onPress" | "disabled">
|
|
46
|
+
|
|
47
|
+
// ─── Pointer shape ────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
// TODO: add to pictureSelector.json — pointer is 24w × 12h triangle in Figma.
|
|
50
|
+
const POINTER_WIDTH = 24
|
|
51
|
+
const POINTER_HEIGHT = 12
|
|
52
|
+
|
|
53
|
+
// Build an SVG path for the rounded-corner pointer: apex centered at the top
|
|
54
|
+
// of a 24×12 box, bottom edge along the bottom. `r` is the corner radius. The
|
|
55
|
+
// triangle is isoceles with apex interior angle 90° and bottom interior angles
|
|
56
|
+
// 45°, so the inset distances along each edge from a vertex are r·cot(θ/2) —
|
|
57
|
+
// i.e. r at the apex and r·(√2 + 1) at the bottom corners.
|
|
58
|
+
const buildPointerPath = (r: number): string => {
|
|
59
|
+
const w = POINTER_WIDTH
|
|
60
|
+
const h = POINTER_HEIGHT
|
|
61
|
+
const apexInset = r
|
|
62
|
+
const botInset = r * (Math.SQRT2 + 1)
|
|
63
|
+
const sin45 = Math.SQRT1_2
|
|
64
|
+
const apexAxis = apexInset * sin45
|
|
65
|
+
const botAxis = botInset * sin45
|
|
66
|
+
|
|
67
|
+
const apexLeftX = w / 2 - apexAxis
|
|
68
|
+
const apexRightX = w / 2 + apexAxis
|
|
69
|
+
const apexY = apexAxis
|
|
70
|
+
const blEdgeX = botAxis
|
|
71
|
+
const blEdgeY = h - botAxis
|
|
72
|
+
const blBottomX = botInset
|
|
73
|
+
const brBottomX = w - botInset
|
|
74
|
+
const brEdgeX = w - botAxis
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
`M ${blEdgeX} ${blEdgeY}`,
|
|
78
|
+
`L ${apexLeftX} ${apexY}`,
|
|
79
|
+
`A ${r} ${r} 0 0 1 ${apexRightX} ${apexY}`,
|
|
80
|
+
`L ${brEdgeX} ${blEdgeY}`,
|
|
81
|
+
`A ${r} ${r} 0 0 1 ${brBottomX} ${h}`,
|
|
82
|
+
`L ${blBottomX} ${h}`,
|
|
83
|
+
`A ${r} ${r} 0 0 1 ${blEdgeX} ${blEdgeY}`,
|
|
84
|
+
"Z"
|
|
85
|
+
].join(" ")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
91
|
+
|
|
92
|
+
// ─── Styled primitives ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
95
|
+
flexDirection: "column",
|
|
96
|
+
gap: rootGap,
|
|
97
|
+
width: "100%"
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const StyledRow = styled(View)<{ rowGap: number }>(({ rowGap }) => ({
|
|
101
|
+
flexDirection: "row",
|
|
102
|
+
alignItems: "flex-start",
|
|
103
|
+
gap: rowGap,
|
|
104
|
+
width: "100%"
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
const StyledItemStack = styled(View)<{ stackGap: number }>(({ stackGap }) => ({
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
gap: stackGap,
|
|
111
|
+
flex: 1,
|
|
112
|
+
minWidth: 0
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const StyledPictureButton = styled(Pressable)<{
|
|
116
|
+
buttonBorderRadius: number
|
|
117
|
+
buttonBorderWidth: number
|
|
118
|
+
buttonBorderColor: string
|
|
119
|
+
buttonBgColor: string
|
|
120
|
+
buttonPadding: number
|
|
121
|
+
buttonOpacity: number
|
|
122
|
+
buttonWidth?: number
|
|
123
|
+
buttonAspectRatio: number
|
|
124
|
+
buttonMinWidth?: number
|
|
125
|
+
buttonMinHeight?: number
|
|
126
|
+
}>(
|
|
127
|
+
({
|
|
128
|
+
buttonBorderRadius,
|
|
129
|
+
buttonBorderWidth,
|
|
130
|
+
buttonBorderColor,
|
|
131
|
+
buttonBgColor,
|
|
132
|
+
buttonPadding,
|
|
133
|
+
buttonOpacity,
|
|
134
|
+
buttonWidth,
|
|
135
|
+
buttonAspectRatio,
|
|
136
|
+
buttonMinWidth,
|
|
137
|
+
buttonMinHeight
|
|
138
|
+
}) => ({
|
|
139
|
+
alignItems: "center",
|
|
140
|
+
justifyContent: "center",
|
|
141
|
+
width: buttonWidth !== undefined ? buttonWidth : "100%",
|
|
142
|
+
aspectRatio: buttonAspectRatio,
|
|
143
|
+
...(buttonMinWidth !== undefined && { minWidth: buttonMinWidth }),
|
|
144
|
+
...(buttonMinHeight !== undefined && { minHeight: buttonMinHeight }),
|
|
145
|
+
padding: buttonPadding,
|
|
146
|
+
borderRadius: buttonBorderRadius,
|
|
147
|
+
borderWidth: buttonBorderWidth,
|
|
148
|
+
borderColor: buttonBorderColor,
|
|
149
|
+
backgroundColor: buttonBgColor,
|
|
150
|
+
opacity: buttonOpacity,
|
|
151
|
+
overflow: "hidden"
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const StyledPointer = styled(Svg)<{ pointerVisible: boolean }>(
|
|
156
|
+
({ pointerVisible }) => ({
|
|
157
|
+
width: POINTER_WIDTH,
|
|
158
|
+
height: POINTER_HEIGHT,
|
|
159
|
+
opacity: pointerVisible ? 1 : 0
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A horizontal group of image/illustration buttons where one is selectable at
|
|
167
|
+
* a time. When the selected item has `insightTitle`/`insightDescription`, a
|
|
168
|
+
* `MessageCard.Insight` (standalone variant) renders below the row with a
|
|
169
|
+
* pointer beneath the selected button linking the two. The pointer moves with
|
|
170
|
+
* the selection.
|
|
171
|
+
*
|
|
172
|
+
* By default each card fills the width of its flex item (`width: 100%`)
|
|
173
|
+
* and is square (`aspectRatio: 1`). Override with `itemWidth` for a fixed
|
|
174
|
+
* width, `itemAspectRatio` for a non-square shape, and `itemMinWidth` /
|
|
175
|
+
* `itemMinHeight` for per-dim minimum floors.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} [label] - Optional headline above the row.
|
|
178
|
+
* @param {PictureSelectorItem[]} items - 3 or 4 items to choose between.
|
|
179
|
+
* @param {string} [value] - Controlled selected value.
|
|
180
|
+
* @param {string} [defaultValue] - Uncontrolled initial selected value.
|
|
181
|
+
* @param {(value: string) => void} [onValueChange] - Fires when selection changes.
|
|
182
|
+
* @param {number} [itemWidth] - Fixed card width (px). Defaults to `100%`.
|
|
183
|
+
* @param {number} [itemAspectRatio=1] - Card aspect ratio (width / height).
|
|
184
|
+
* @param {number} [itemMinWidth] - Optional override for the card's min-width.
|
|
185
|
+
* Defaults to `pictureButton.size.minWidth` (80px).
|
|
186
|
+
* @param {number} [itemMinHeight] - Optional override for the card's min-height.
|
|
187
|
+
* Defaults to `pictureButton.size.minHeight` (120px).
|
|
188
|
+
*/
|
|
189
|
+
export const PictureSelector = React.forwardRef<View, PictureSelectorProps>(
|
|
190
|
+
(
|
|
191
|
+
{
|
|
192
|
+
label,
|
|
193
|
+
items,
|
|
194
|
+
value,
|
|
195
|
+
defaultValue,
|
|
196
|
+
onValueChange,
|
|
197
|
+
itemWidth,
|
|
198
|
+
itemAspectRatio = 1,
|
|
199
|
+
itemMinWidth,
|
|
200
|
+
itemMinHeight,
|
|
201
|
+
...rest
|
|
202
|
+
},
|
|
203
|
+
ref
|
|
204
|
+
) => {
|
|
205
|
+
const theme = useTheme()
|
|
206
|
+
const { pictureSelector } = theme.tokens.components
|
|
207
|
+
const { pictureButton } = pictureSelector
|
|
208
|
+
|
|
209
|
+
const isControlled = value !== undefined
|
|
210
|
+
const [internalValue, setInternalValue] = React.useState<
|
|
211
|
+
string | undefined
|
|
212
|
+
>(defaultValue)
|
|
213
|
+
const selectedValue = isControlled ? value : internalValue
|
|
214
|
+
const selectedItem = items.find((item) => item.value === selectedValue)
|
|
215
|
+
const hasInsight = Boolean(
|
|
216
|
+
selectedItem?.insightTitle || selectedItem?.insightDescription
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const handleSelect = (nextValue: string) => {
|
|
220
|
+
if (!isControlled) setInternalValue(nextValue)
|
|
221
|
+
onValueChange?.(nextValue)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rootGap = parseTokenValue(pictureSelector.spacing.container.gap)
|
|
225
|
+
const rowGap = parseTokenValue(pictureSelector.spacing.buttons.gap)
|
|
226
|
+
const stackGap = parseTokenValue(pictureButton.spacing.container.gap)
|
|
227
|
+
const buttonPadding = parseTokenValue(pictureButton.spacing.padding)
|
|
228
|
+
const buttonBorderRadius = parseTokenValue(
|
|
229
|
+
pictureButton.borderRadius.default
|
|
230
|
+
)
|
|
231
|
+
const borderWidthDefault = parseTokenValue(
|
|
232
|
+
pictureButton.borderWidth.default
|
|
233
|
+
)
|
|
234
|
+
const borderWidthActive = parseTokenValue(pictureButton.borderWidth.active)
|
|
235
|
+
const pointerPath = buildPointerPath(
|
|
236
|
+
parseTokenValue(pictureButton.pointer.borderRadius.default)
|
|
237
|
+
)
|
|
238
|
+
const resolvedMinWidth =
|
|
239
|
+
itemMinWidth ?? parseTokenValue(pictureButton.size.minWidth)
|
|
240
|
+
// Default min-height to min-width so the cards stay square at the floor,
|
|
241
|
+
// regardless of how many items are in the row.
|
|
242
|
+
const resolvedMinHeight = itemMinHeight ?? resolvedMinWidth
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<StyledRoot
|
|
246
|
+
ref={ref}
|
|
247
|
+
rootGap={rootGap}
|
|
248
|
+
accessible
|
|
249
|
+
accessibilityRole="radiogroup"
|
|
250
|
+
accessibilityLabel={label}
|
|
251
|
+
{...rest}
|
|
252
|
+
>
|
|
253
|
+
{label && (
|
|
254
|
+
<Typography
|
|
255
|
+
token={pictureSelector.typography.label}
|
|
256
|
+
color={pictureSelector.colour.text.default}
|
|
257
|
+
>
|
|
258
|
+
{label}
|
|
259
|
+
</Typography>
|
|
260
|
+
)}
|
|
261
|
+
<StyledRow rowGap={rowGap}>
|
|
262
|
+
{items.map((item) => {
|
|
263
|
+
const isSelected = item.value === selectedValue
|
|
264
|
+
const Visual = item.illustration
|
|
265
|
+
const buttonProps: Omit<
|
|
266
|
+
React.ComponentProps<typeof StyledPictureButton>,
|
|
267
|
+
keyof PressableOwnForItem
|
|
268
|
+
> = {
|
|
269
|
+
buttonBorderRadius,
|
|
270
|
+
buttonBorderWidth: isSelected
|
|
271
|
+
? borderWidthActive
|
|
272
|
+
: borderWidthDefault,
|
|
273
|
+
buttonBorderColor: isSelected
|
|
274
|
+
? pictureButton.colour.border.active
|
|
275
|
+
: pictureButton.colour.border.default,
|
|
276
|
+
buttonBgColor: isSelected
|
|
277
|
+
? pictureButton.colour.background.active
|
|
278
|
+
: pictureButton.colour.background.default,
|
|
279
|
+
buttonPadding,
|
|
280
|
+
buttonOpacity: item.disabled ? 0.5 : 1,
|
|
281
|
+
buttonWidth: itemWidth,
|
|
282
|
+
buttonAspectRatio: itemAspectRatio,
|
|
283
|
+
buttonMinWidth: resolvedMinWidth,
|
|
284
|
+
buttonMinHeight: resolvedMinHeight
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<StyledItemStack key={item.value} stackGap={stackGap}>
|
|
289
|
+
<StyledPictureButton
|
|
290
|
+
accessible
|
|
291
|
+
accessibilityRole="radio"
|
|
292
|
+
accessibilityState={{
|
|
293
|
+
selected: isSelected,
|
|
294
|
+
disabled: item.disabled
|
|
295
|
+
}}
|
|
296
|
+
accessibilityLabel={item.ariaLabel}
|
|
297
|
+
disabled={item.disabled}
|
|
298
|
+
onPress={() => {
|
|
299
|
+
if (!item.disabled) handleSelect(item.value)
|
|
300
|
+
}}
|
|
301
|
+
{...buttonProps}
|
|
302
|
+
>
|
|
303
|
+
{Visual ? (
|
|
304
|
+
<Illustration illustration={Visual} size="xl" />
|
|
305
|
+
) : null}
|
|
306
|
+
</StyledPictureButton>
|
|
307
|
+
<StyledPointer
|
|
308
|
+
pointerVisible={isSelected && hasInsight}
|
|
309
|
+
width={POINTER_WIDTH}
|
|
310
|
+
height={POINTER_HEIGHT}
|
|
311
|
+
viewBox={`0 0 ${POINTER_WIDTH} ${POINTER_HEIGHT}`}
|
|
312
|
+
>
|
|
313
|
+
<Path
|
|
314
|
+
d={pointerPath}
|
|
315
|
+
fill={pictureButton.colour.pointer.default}
|
|
316
|
+
/>
|
|
317
|
+
</StyledPointer>
|
|
318
|
+
</StyledItemStack>
|
|
319
|
+
)
|
|
320
|
+
})}
|
|
321
|
+
</StyledRow>
|
|
322
|
+
{hasInsight && selectedItem && (
|
|
323
|
+
<MessageCard.Insight
|
|
324
|
+
variant="standalone"
|
|
325
|
+
title={selectedItem.insightTitle}
|
|
326
|
+
>
|
|
327
|
+
{selectedItem.insightDescription}
|
|
328
|
+
</MessageCard.Insight>
|
|
329
|
+
)}
|
|
330
|
+
</StyledRoot>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
PictureSelector.displayName = "PictureSelector"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { Progress } from "./Progress"
|
|
4
|
+
import type { ProgressProps } from "./Progress"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: "Molecules/Progress",
|
|
9
|
+
component: Progress,
|
|
10
|
+
argTypes: {
|
|
11
|
+
value: {
|
|
12
|
+
control: { type: "range", min: 0, max: 100, step: 1 },
|
|
13
|
+
description: "Current progress value (0 to max)"
|
|
14
|
+
},
|
|
15
|
+
max: {
|
|
16
|
+
control: { type: "number" },
|
|
17
|
+
description: "Maximum value representing 100% completion"
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
control: { type: "select" },
|
|
21
|
+
options: ["sm", "lg"],
|
|
22
|
+
description: "Track height (sm: 8px, lg: 16px)"
|
|
23
|
+
},
|
|
24
|
+
colour: {
|
|
25
|
+
control: { type: "select" },
|
|
26
|
+
options: ["primary", "secondary"],
|
|
27
|
+
description: "Indicator fill colour"
|
|
28
|
+
},
|
|
29
|
+
label: {
|
|
30
|
+
control: { type: "text" },
|
|
31
|
+
description: "Label rendered above the track (left)"
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
control: { type: "text" },
|
|
35
|
+
description: "Helper text rendered above the track (right)"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Playground = {
|
|
41
|
+
args: {
|
|
42
|
+
value: 50,
|
|
43
|
+
max: 100,
|
|
44
|
+
size: "sm",
|
|
45
|
+
colour: "primary",
|
|
46
|
+
label: "Label",
|
|
47
|
+
description: "Help text"
|
|
48
|
+
},
|
|
49
|
+
render: (args: ProgressProps) => (
|
|
50
|
+
<View style={styles.container}>
|
|
51
|
+
<Progress {...args} />
|
|
52
|
+
</View>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Small = {
|
|
57
|
+
name: "Small",
|
|
58
|
+
render: () => (
|
|
59
|
+
<View style={styles.column}>
|
|
60
|
+
<Progress size="sm" value={0} label="Label" description="0%" />
|
|
61
|
+
<Progress size="sm" value={30} label="Label" description="30%" />
|
|
62
|
+
<Progress size="sm" value={60} label="Label" description="60%" />
|
|
63
|
+
<Progress size="sm" value={100} label="Label" description="100%" />
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Large = {
|
|
69
|
+
name: "Large",
|
|
70
|
+
render: () => (
|
|
71
|
+
<View style={styles.column}>
|
|
72
|
+
<Progress size="lg" value={0} label="Label" description="0%" />
|
|
73
|
+
<Progress size="lg" value={30} label="Label" description="30%" />
|
|
74
|
+
<Progress size="lg" value={60} label="Label" description="60%" />
|
|
75
|
+
<Progress size="lg" value={100} label="Label" description="100%" />
|
|
76
|
+
</View>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const Colours = {
|
|
81
|
+
name: "Colours",
|
|
82
|
+
render: () => (
|
|
83
|
+
<View style={styles.column}>
|
|
84
|
+
<View style={styles.section}>
|
|
85
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
86
|
+
Primary (Yellow)
|
|
87
|
+
</Typography>
|
|
88
|
+
<Progress value={60} colour="primary" />
|
|
89
|
+
</View>
|
|
90
|
+
<View style={styles.section}>
|
|
91
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
92
|
+
Secondary (Brown)
|
|
93
|
+
</Typography>
|
|
94
|
+
<Progress value={60} colour="secondary" />
|
|
95
|
+
</View>
|
|
96
|
+
</View>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const WithoutHeader = {
|
|
101
|
+
name: "Without Header",
|
|
102
|
+
render: () => (
|
|
103
|
+
<View style={styles.column}>
|
|
104
|
+
<Progress size="sm" value={40} />
|
|
105
|
+
<Progress size="lg" value={70} />
|
|
106
|
+
</View>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const FractionalValue = {
|
|
111
|
+
name: "Fractional Value",
|
|
112
|
+
render: () => (
|
|
113
|
+
<View style={styles.column}>
|
|
114
|
+
<Progress
|
|
115
|
+
size="lg"
|
|
116
|
+
value={14}
|
|
117
|
+
max={14}
|
|
118
|
+
label="Box capacity"
|
|
119
|
+
description="14/14"
|
|
120
|
+
/>
|
|
121
|
+
<Progress
|
|
122
|
+
size="lg"
|
|
123
|
+
value={9}
|
|
124
|
+
max={14}
|
|
125
|
+
label="Box capacity"
|
|
126
|
+
description="9/14"
|
|
127
|
+
/>
|
|
128
|
+
</View>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const styles = StyleSheet.create({
|
|
133
|
+
container: {
|
|
134
|
+
padding: 16
|
|
135
|
+
},
|
|
136
|
+
column: {
|
|
137
|
+
padding: 16,
|
|
138
|
+
flexDirection: "column",
|
|
139
|
+
gap: 16
|
|
140
|
+
},
|
|
141
|
+
section: {
|
|
142
|
+
flexDirection: "column",
|
|
143
|
+
gap: 8
|
|
144
|
+
}
|
|
145
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import * as ProgressPrimitive from "@rn-primitives/progress"
|
|
6
|
+
import { Typography } from "../../atoms/Typography"
|
|
7
|
+
|
|
8
|
+
type ProgressSize = "sm" | "lg"
|
|
9
|
+
type ProgressColour = "primary" | "secondary"
|
|
10
|
+
|
|
11
|
+
type ProgressOwnProps = {
|
|
12
|
+
value?: number
|
|
13
|
+
max?: number
|
|
14
|
+
size?: ProgressSize
|
|
15
|
+
colour?: ProgressColour
|
|
16
|
+
label?: React.ReactNode
|
|
17
|
+
description?: React.ReactNode
|
|
18
|
+
getValueLabel?: (value: number, max: number) => string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProgressProps = ProgressOwnProps &
|
|
22
|
+
Omit<ViewProps, keyof ProgressOwnProps>
|
|
23
|
+
|
|
24
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
25
|
+
|
|
26
|
+
const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
27
|
+
flexDirection: "column",
|
|
28
|
+
alignItems: "stretch",
|
|
29
|
+
gap: rootGap,
|
|
30
|
+
width: "100%"
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
const StyledHeader = styled(View)({
|
|
34
|
+
flexDirection: "row",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
justifyContent: "space-between",
|
|
37
|
+
width: "100%"
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const StyledTrack = styled(View)<{
|
|
41
|
+
trackHeight: number
|
|
42
|
+
trackMinWidth: number
|
|
43
|
+
trackBorderRadius: number
|
|
44
|
+
trackBgColor: string
|
|
45
|
+
}>(({ trackHeight, trackMinWidth, trackBorderRadius, trackBgColor }) => ({
|
|
46
|
+
position: "relative",
|
|
47
|
+
width: "100%",
|
|
48
|
+
minWidth: trackMinWidth,
|
|
49
|
+
height: trackHeight,
|
|
50
|
+
borderRadius: trackBorderRadius,
|
|
51
|
+
backgroundColor: trackBgColor,
|
|
52
|
+
overflow: "hidden"
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
const StyledIndicator = styled(View)<{
|
|
56
|
+
indicatorBorderRadius: number
|
|
57
|
+
indicatorBgColor: string
|
|
58
|
+
}>(({ indicatorBorderRadius, indicatorBgColor }) => ({
|
|
59
|
+
height: "100%",
|
|
60
|
+
borderRadius: indicatorBorderRadius,
|
|
61
|
+
backgroundColor: indicatorBgColor
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
65
|
+
Math.max(min, Math.min(max, value))
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A horizontal progress bar that visualises the completion of a determinate
|
|
69
|
+
* task. Supports an optional label (top-left) and description (top-right),
|
|
70
|
+
* two sizes, and two indicator colours.
|
|
71
|
+
*
|
|
72
|
+
* Wraps `@rn-primitives/progress` for accessibility.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} [value=0] - Current progress value (0 to `max`). Clamped.
|
|
75
|
+
* @param {number} [max=100] - Maximum value representing 100% completion.
|
|
76
|
+
* @param {"sm" | "lg"} [size="sm"] - Track height: sm (8px) or lg (16px).
|
|
77
|
+
* @param {"primary" | "secondary"} [colour="primary"] - Indicator fill colour.
|
|
78
|
+
* @param {React.ReactNode} [label] - Label rendered in the top-left above the track.
|
|
79
|
+
* @param {React.ReactNode} [description] - Helper text rendered in the top-right above the track.
|
|
80
|
+
* @param {(value: number, max: number) => string} [getValueLabel] - Accessible value label.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* import { Progress } from "@butternutbox/pawprint-native"
|
|
85
|
+
*
|
|
86
|
+
* <Progress label="Box capacity" description="14/14" value={100} />
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export const Progress = React.forwardRef<View, ProgressProps>(
|
|
90
|
+
(
|
|
91
|
+
{
|
|
92
|
+
value = 0,
|
|
93
|
+
max = 100,
|
|
94
|
+
size = "sm",
|
|
95
|
+
colour = "primary",
|
|
96
|
+
label,
|
|
97
|
+
description,
|
|
98
|
+
getValueLabel,
|
|
99
|
+
...rest
|
|
100
|
+
},
|
|
101
|
+
ref
|
|
102
|
+
) => {
|
|
103
|
+
const theme = useTheme()
|
|
104
|
+
const {
|
|
105
|
+
track,
|
|
106
|
+
indicator,
|
|
107
|
+
borderRadius,
|
|
108
|
+
spacing,
|
|
109
|
+
typography,
|
|
110
|
+
colour: textColour
|
|
111
|
+
} = theme.tokens.components.progress
|
|
112
|
+
|
|
113
|
+
const safeMax = max > 0 ? max : 100
|
|
114
|
+
const clampedValue = clamp(value, 0, safeMax)
|
|
115
|
+
const percent = (clampedValue / safeMax) * 100
|
|
116
|
+
const showHeader = Boolean(label) || Boolean(description)
|
|
117
|
+
const typographyForSize =
|
|
118
|
+
size === "lg" ? typography.large : typography.small
|
|
119
|
+
|
|
120
|
+
const trackSize = { lg: track.size.large, sm: track.size.small }[size]
|
|
121
|
+
const trackHeight = parseTokenValue(trackSize.height)
|
|
122
|
+
const trackMinWidth = parseTokenValue(trackSize.mindWidth)
|
|
123
|
+
const trackBorderRadius = parseTokenValue(borderRadius.track.default)
|
|
124
|
+
const indicatorBorderRadius = parseTokenValue(
|
|
125
|
+
borderRadius.indicator.default
|
|
126
|
+
)
|
|
127
|
+
const indicatorBgColor =
|
|
128
|
+
colour === "secondary"
|
|
129
|
+
? indicator.colour.background.secondary
|
|
130
|
+
: indicator.colour.background.primary
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<StyledRoot ref={ref} rootGap={parseTokenValue(spacing.gap)} {...rest}>
|
|
134
|
+
{showHeader && (
|
|
135
|
+
<StyledHeader>
|
|
136
|
+
{label ? (
|
|
137
|
+
<Typography
|
|
138
|
+
token={typographyForSize.label}
|
|
139
|
+
color={textColour.text.label}
|
|
140
|
+
>
|
|
141
|
+
{label}
|
|
142
|
+
</Typography>
|
|
143
|
+
) : (
|
|
144
|
+
<View />
|
|
145
|
+
)}
|
|
146
|
+
{description ? (
|
|
147
|
+
<Typography
|
|
148
|
+
token={typographyForSize.description}
|
|
149
|
+
color={textColour.text.helper}
|
|
150
|
+
>
|
|
151
|
+
{description}
|
|
152
|
+
</Typography>
|
|
153
|
+
) : (
|
|
154
|
+
<View />
|
|
155
|
+
)}
|
|
156
|
+
</StyledHeader>
|
|
157
|
+
)}
|
|
158
|
+
<ProgressPrimitive.Root
|
|
159
|
+
value={clampedValue}
|
|
160
|
+
max={safeMax}
|
|
161
|
+
getValueLabel={getValueLabel}
|
|
162
|
+
asChild
|
|
163
|
+
>
|
|
164
|
+
<StyledTrack
|
|
165
|
+
trackHeight={trackHeight}
|
|
166
|
+
trackMinWidth={trackMinWidth}
|
|
167
|
+
trackBorderRadius={trackBorderRadius}
|
|
168
|
+
trackBgColor={track.colour.background.default}
|
|
169
|
+
>
|
|
170
|
+
<ProgressPrimitive.Indicator asChild>
|
|
171
|
+
<StyledIndicator
|
|
172
|
+
indicatorBorderRadius={indicatorBorderRadius}
|
|
173
|
+
indicatorBgColor={indicatorBgColor}
|
|
174
|
+
style={{ width: `${percent}%` }}
|
|
175
|
+
/>
|
|
176
|
+
</ProgressPrimitive.Indicator>
|
|
177
|
+
</StyledTrack>
|
|
178
|
+
</ProgressPrimitive.Root>
|
|
179
|
+
</StyledRoot>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
Progress.displayName = "Progress"
|