@butternutbox/pawprint-native 0.0.1 → 0.1.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 +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- 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.test.tsx +55 -0
- 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/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 +16 -13
- 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.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- 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 +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -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 +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -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,313 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, Pressable, ViewProps, PressableProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Icon, type PawprintIcon } from "../../atoms/Icon"
|
|
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
|
+
icon?: PawprintIcon
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
ariaLabel?: string
|
|
23
|
+
insightTitle?: string
|
|
24
|
+
insightDescription?: React.ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type PictureSelectorOwnProps = {
|
|
28
|
+
label?: string
|
|
29
|
+
items: PictureSelectorItem[]
|
|
30
|
+
value?: string
|
|
31
|
+
defaultValue?: string
|
|
32
|
+
onValueChange?: (value: string) => void
|
|
33
|
+
/** Optional fixed card width (px). Defaults to `100%` (fluid). */
|
|
34
|
+
itemWidth?: number
|
|
35
|
+
/** Card aspect ratio (width / height). Defaults to `1` (square). */
|
|
36
|
+
itemAspectRatio?: number
|
|
37
|
+
/** Optional floor on each card's width (px). No default. */
|
|
38
|
+
itemMinWidth?: number
|
|
39
|
+
/** Optional floor on each card's height (px). No default. */
|
|
40
|
+
itemMinHeight?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PictureSelectorProps = PictureSelectorOwnProps &
|
|
44
|
+
Omit<ViewProps, keyof PictureSelectorOwnProps>
|
|
45
|
+
|
|
46
|
+
type PressableOwnForItem = Omit<PressableProps, "onPress" | "disabled">
|
|
47
|
+
|
|
48
|
+
// ─── Pointer shape ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
// TODO: add to pictureSelector.json — pointer is 24w × 12h triangle in Figma.
|
|
51
|
+
// These aren't container dimensions — they're the triangle's border widths
|
|
52
|
+
// (the CSS/RN triangle trick), which *are* the shape itself.
|
|
53
|
+
const POINTER_HALF_WIDTH = 12
|
|
54
|
+
const POINTER_HEIGHT = 12
|
|
55
|
+
|
|
56
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
59
|
+
|
|
60
|
+
// ─── Styled primitives ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
63
|
+
flexDirection: "column",
|
|
64
|
+
gap: rootGap,
|
|
65
|
+
width: "100%"
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
const StyledRow = styled(View)<{ rowGap: number }>(({ rowGap }) => ({
|
|
69
|
+
flexDirection: "row",
|
|
70
|
+
alignItems: "flex-start",
|
|
71
|
+
gap: rowGap,
|
|
72
|
+
width: "100%"
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
const StyledItemStack = styled(View)<{ stackGap: number }>(({ stackGap }) => ({
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
gap: stackGap,
|
|
79
|
+
flex: 1,
|
|
80
|
+
minWidth: 0
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const StyledPictureButton = styled(Pressable)<{
|
|
84
|
+
buttonBorderRadius: number
|
|
85
|
+
buttonBorderWidth: number
|
|
86
|
+
buttonBorderColor: string
|
|
87
|
+
buttonBgColor: string
|
|
88
|
+
buttonPadding: number
|
|
89
|
+
buttonOpacity: number
|
|
90
|
+
buttonWidth?: number
|
|
91
|
+
buttonAspectRatio: number
|
|
92
|
+
buttonMinWidth?: number
|
|
93
|
+
buttonMinHeight?: number
|
|
94
|
+
}>(
|
|
95
|
+
({
|
|
96
|
+
buttonBorderRadius,
|
|
97
|
+
buttonBorderWidth,
|
|
98
|
+
buttonBorderColor,
|
|
99
|
+
buttonBgColor,
|
|
100
|
+
buttonPadding,
|
|
101
|
+
buttonOpacity,
|
|
102
|
+
buttonWidth,
|
|
103
|
+
buttonAspectRatio,
|
|
104
|
+
buttonMinWidth,
|
|
105
|
+
buttonMinHeight
|
|
106
|
+
}) => ({
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
justifyContent: "center",
|
|
109
|
+
width: buttonWidth !== undefined ? buttonWidth : "100%",
|
|
110
|
+
aspectRatio: buttonAspectRatio,
|
|
111
|
+
...(buttonMinWidth !== undefined && { minWidth: buttonMinWidth }),
|
|
112
|
+
...(buttonMinHeight !== undefined && { minHeight: buttonMinHeight }),
|
|
113
|
+
padding: buttonPadding,
|
|
114
|
+
borderRadius: buttonBorderRadius,
|
|
115
|
+
borderWidth: buttonBorderWidth,
|
|
116
|
+
borderColor: buttonBorderColor,
|
|
117
|
+
backgroundColor: buttonBgColor,
|
|
118
|
+
opacity: buttonOpacity,
|
|
119
|
+
overflow: "hidden"
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const StyledPointerTriangle = styled(View)<{
|
|
124
|
+
pointerColor: string
|
|
125
|
+
pointerVisible: boolean
|
|
126
|
+
}>(({ pointerColor, pointerVisible }) => ({
|
|
127
|
+
// 0×0 is part of the RN triangle-via-borders trick, not a layout
|
|
128
|
+
// dimension. Borders compose into a down-pointing triangle (apex at
|
|
129
|
+
// bottom) on RN; the `rotate 180deg` transform flips it to apex-up so
|
|
130
|
+
// the pointer visually connects the selected button to the insight
|
|
131
|
+
// card below. Using rotate instead of `borderBottom` because RN's
|
|
132
|
+
// triangle-via-borders rendering is inconsistent when the coloured
|
|
133
|
+
// border is on the bottom with transparent left/right on certain
|
|
134
|
+
// platforms.
|
|
135
|
+
width: 0,
|
|
136
|
+
height: 0,
|
|
137
|
+
borderLeftWidth: POINTER_HALF_WIDTH,
|
|
138
|
+
borderRightWidth: POINTER_HALF_WIDTH,
|
|
139
|
+
borderTopWidth: POINTER_HEIGHT,
|
|
140
|
+
borderLeftColor: "transparent",
|
|
141
|
+
borderRightColor: "transparent",
|
|
142
|
+
borderTopColor: pointerColor,
|
|
143
|
+
borderStyle: "solid",
|
|
144
|
+
transform: [{ rotate: "180deg" }],
|
|
145
|
+
opacity: pointerVisible ? 1 : 0
|
|
146
|
+
}))
|
|
147
|
+
|
|
148
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A horizontal group of image/illustration buttons where one is selectable at
|
|
152
|
+
* a time. When the selected item has `insightTitle`/`insightDescription`, a
|
|
153
|
+
* `MessageCard.Insight` (standalone variant) renders below the row with a
|
|
154
|
+
* pointer beneath the selected button linking the two. The pointer moves with
|
|
155
|
+
* the selection.
|
|
156
|
+
*
|
|
157
|
+
* By default each card fills the width of its flex item (`width: 100%`)
|
|
158
|
+
* and is square (`aspectRatio: 1`). Override with `itemWidth` for a fixed
|
|
159
|
+
* width, `itemAspectRatio` for a non-square shape, and `itemMinWidth` /
|
|
160
|
+
* `itemMinHeight` for per-dim minimum floors.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} [label] - Optional headline above the row.
|
|
163
|
+
* @param {PictureSelectorItem[]} items - 3 or 4 items to choose between.
|
|
164
|
+
* @param {string} [value] - Controlled selected value.
|
|
165
|
+
* @param {string} [defaultValue] - Uncontrolled initial selected value.
|
|
166
|
+
* @param {(value: string) => void} [onValueChange] - Fires when selection changes.
|
|
167
|
+
* @param {number} [itemWidth] - Fixed card width (px). Defaults to `100%`.
|
|
168
|
+
* @param {number} [itemAspectRatio=1] - Card aspect ratio (width / height).
|
|
169
|
+
* @param {number} [itemMinWidth] - Optional override for the card's min-width.
|
|
170
|
+
* Defaults to `pictureButton.size.minWidth` (80px).
|
|
171
|
+
* @param {number} [itemMinHeight] - Optional override for the card's min-height.
|
|
172
|
+
* Defaults to `pictureButton.size.minHeight` (120px).
|
|
173
|
+
*/
|
|
174
|
+
export const PictureSelector = React.forwardRef<View, PictureSelectorProps>(
|
|
175
|
+
(
|
|
176
|
+
{
|
|
177
|
+
label,
|
|
178
|
+
items,
|
|
179
|
+
value,
|
|
180
|
+
defaultValue,
|
|
181
|
+
onValueChange,
|
|
182
|
+
itemWidth,
|
|
183
|
+
itemAspectRatio = 1,
|
|
184
|
+
itemMinWidth,
|
|
185
|
+
itemMinHeight,
|
|
186
|
+
...rest
|
|
187
|
+
},
|
|
188
|
+
ref
|
|
189
|
+
) => {
|
|
190
|
+
const theme = useTheme()
|
|
191
|
+
const { pictureSelector } = theme.tokens.components
|
|
192
|
+
const { pictureButton } = pictureSelector
|
|
193
|
+
|
|
194
|
+
const isControlled = value !== undefined
|
|
195
|
+
const [internalValue, setInternalValue] = React.useState<
|
|
196
|
+
string | undefined
|
|
197
|
+
>(defaultValue)
|
|
198
|
+
const selectedValue = isControlled ? value : internalValue
|
|
199
|
+
const selectedItem = items.find((item) => item.value === selectedValue)
|
|
200
|
+
const hasInsight = Boolean(
|
|
201
|
+
selectedItem?.insightTitle || selectedItem?.insightDescription
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const handleSelect = (nextValue: string) => {
|
|
205
|
+
if (!isControlled) setInternalValue(nextValue)
|
|
206
|
+
onValueChange?.(nextValue)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rootGap = parseTokenValue(pictureSelector.spacing.container.gap)
|
|
210
|
+
const rowGap = parseTokenValue(pictureSelector.spacing.buttons.gap)
|
|
211
|
+
const stackGap = parseTokenValue(pictureButton.spacing.container.gap)
|
|
212
|
+
const buttonPadding = parseTokenValue(pictureButton.spacing.padding)
|
|
213
|
+
const buttonBorderRadius = parseTokenValue(
|
|
214
|
+
pictureButton.borderRadius.default
|
|
215
|
+
)
|
|
216
|
+
const borderWidthDefault = parseTokenValue(
|
|
217
|
+
pictureButton.borderWidth.default
|
|
218
|
+
)
|
|
219
|
+
const borderWidthActive = parseTokenValue(pictureButton.borderWidth.active)
|
|
220
|
+
const resolvedMinWidth =
|
|
221
|
+
itemMinWidth ?? parseTokenValue(pictureButton.size.minWidth)
|
|
222
|
+
// Default min-height to min-width so the cards stay square at the floor,
|
|
223
|
+
// regardless of how many items are in the row.
|
|
224
|
+
const resolvedMinHeight = itemMinHeight ?? resolvedMinWidth
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<StyledRoot
|
|
228
|
+
ref={ref}
|
|
229
|
+
rootGap={rootGap}
|
|
230
|
+
accessible
|
|
231
|
+
accessibilityRole="radiogroup"
|
|
232
|
+
accessibilityLabel={label}
|
|
233
|
+
{...rest}
|
|
234
|
+
>
|
|
235
|
+
{label && (
|
|
236
|
+
<Typography
|
|
237
|
+
token={pictureSelector.typography.label}
|
|
238
|
+
color={pictureSelector.colour.text.default}
|
|
239
|
+
>
|
|
240
|
+
{label}
|
|
241
|
+
</Typography>
|
|
242
|
+
)}
|
|
243
|
+
<StyledRow rowGap={rowGap}>
|
|
244
|
+
{items.map((item) => {
|
|
245
|
+
const isSelected = item.value === selectedValue
|
|
246
|
+
const Visual = item.illustration
|
|
247
|
+
const IconComp = item.icon
|
|
248
|
+
const buttonProps: Omit<
|
|
249
|
+
React.ComponentProps<typeof StyledPictureButton>,
|
|
250
|
+
keyof PressableOwnForItem
|
|
251
|
+
> = {
|
|
252
|
+
buttonBorderRadius,
|
|
253
|
+
buttonBorderWidth: isSelected
|
|
254
|
+
? borderWidthActive
|
|
255
|
+
: borderWidthDefault,
|
|
256
|
+
buttonBorderColor: isSelected
|
|
257
|
+
? pictureButton.colour.border.active
|
|
258
|
+
: pictureButton.colour.border.default,
|
|
259
|
+
buttonBgColor: isSelected
|
|
260
|
+
? pictureButton.colour.background.active
|
|
261
|
+
: pictureButton.colour.background.default,
|
|
262
|
+
buttonPadding,
|
|
263
|
+
buttonOpacity: item.disabled ? 0.5 : 1,
|
|
264
|
+
buttonWidth: itemWidth,
|
|
265
|
+
buttonAspectRatio: itemAspectRatio,
|
|
266
|
+
buttonMinWidth: resolvedMinWidth,
|
|
267
|
+
buttonMinHeight: resolvedMinHeight
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<StyledItemStack key={item.value} stackGap={stackGap}>
|
|
272
|
+
<StyledPictureButton
|
|
273
|
+
accessible
|
|
274
|
+
accessibilityRole="radio"
|
|
275
|
+
accessibilityState={{
|
|
276
|
+
selected: isSelected,
|
|
277
|
+
disabled: item.disabled
|
|
278
|
+
}}
|
|
279
|
+
accessibilityLabel={item.ariaLabel}
|
|
280
|
+
disabled={item.disabled}
|
|
281
|
+
onPress={() => {
|
|
282
|
+
if (!item.disabled) handleSelect(item.value)
|
|
283
|
+
}}
|
|
284
|
+
{...buttonProps}
|
|
285
|
+
>
|
|
286
|
+
{Visual ? (
|
|
287
|
+
<Illustration illustration={Visual} size="sm" />
|
|
288
|
+
) : IconComp ? (
|
|
289
|
+
<Icon icon={IconComp} size="2xl" />
|
|
290
|
+
) : null}
|
|
291
|
+
</StyledPictureButton>
|
|
292
|
+
<StyledPointerTriangle
|
|
293
|
+
pointerColor={pictureButton.colour.pointer.default}
|
|
294
|
+
pointerVisible={isSelected && hasInsight}
|
|
295
|
+
/>
|
|
296
|
+
</StyledItemStack>
|
|
297
|
+
)
|
|
298
|
+
})}
|
|
299
|
+
</StyledRow>
|
|
300
|
+
{hasInsight && selectedItem && (
|
|
301
|
+
<MessageCard.Insight
|
|
302
|
+
variant="standalone"
|
|
303
|
+
title={selectedItem.insightTitle}
|
|
304
|
+
>
|
|
305
|
+
{selectedItem.insightDescription}
|
|
306
|
+
</MessageCard.Insight>
|
|
307
|
+
)}
|
|
308
|
+
</StyledRoot>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
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"
|