@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.
Files changed (187) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +30 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12413 -1459
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1111 -13
  7. package/dist/index.d.ts +1111 -13
  8. package/dist/index.js +12365 -1457
  9. package/dist/index.js.map +1 -1
  10. package/package.json +29 -11
  11. package/src/__mocks__/asset-stub.ts +1 -0
  12. package/src/__mocks__/emotion-native.tsx +18 -0
  13. package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
  14. package/src/__mocks__/react-native-reanimated.tsx +79 -0
  15. package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
  16. package/src/__mocks__/react-native-svg.tsx +27 -0
  17. package/src/__mocks__/react-native-worklets.tsx +11 -0
  18. package/src/__mocks__/react-native.tsx +338 -0
  19. package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
  20. package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
  21. package/src/__mocks__/rn-primitives/select.tsx +116 -0
  22. package/src/__mocks__/rn-primitives/slider.tsx +40 -0
  23. package/src/__mocks__/rn-primitives/slot.tsx +30 -0
  24. package/src/__mocks__/rn-primitives/switch.tsx +24 -0
  25. package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
  26. package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
  27. package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
  28. package/src/components/atoms/Avatar/Avatar.tsx +68 -22
  29. package/src/components/atoms/Avatar/index.ts +1 -6
  30. package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
  31. package/src/components/atoms/Badge/Badge.test.tsx +90 -0
  32. package/src/components/atoms/Button/Button.test.tsx +123 -0
  33. package/src/components/atoms/Button/Button.tsx +1 -1
  34. package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
  35. package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
  36. package/src/components/atoms/CarouselControls/index.ts +2 -0
  37. package/src/components/atoms/Hint/Hint.test.tsx +36 -0
  38. package/src/components/atoms/Icon/Icon.test.tsx +98 -0
  39. package/src/components/atoms/Icon/Icon.tsx +5 -1
  40. package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
  41. package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
  42. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  43. package/src/components/atoms/Illustration/Illustration.tsx +3 -3
  44. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  45. package/src/components/atoms/Input/Input.test.tsx +306 -0
  46. package/src/components/atoms/Input/Input.tsx +9 -1
  47. package/src/components/atoms/Input/InputField.tsx +226 -74
  48. package/src/components/atoms/Link/Link.test.tsx +89 -0
  49. package/src/components/atoms/Link/Link.tsx +7 -6
  50. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  51. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  52. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  53. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  54. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  55. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  56. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  57. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  58. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  61. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  62. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  63. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  64. package/src/components/atoms/Logo/assets/index.ts +11 -0
  65. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  66. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  67. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  68. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  69. package/src/components/atoms/NumberInput/index.ts +4 -0
  70. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  71. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  72. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  73. package/src/components/atoms/Switch/Switch.tsx +28 -17
  74. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  75. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  76. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  77. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  78. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  79. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  80. package/src/components/atoms/TextArea/index.ts +6 -0
  81. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  82. package/src/components/atoms/index.ts +3 -0
  83. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  84. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  85. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  86. package/src/components/molecules/Accordion/index.ts +6 -0
  87. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  88. package/src/components/molecules/Animated/Animated.tsx +283 -0
  89. package/src/components/molecules/Animated/index.ts +10 -0
  90. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
  91. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  92. package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
  93. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
  94. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  95. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  96. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  97. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  98. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  99. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  100. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  101. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  102. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  103. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  104. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  105. package/src/components/molecules/CopyField/index.ts +4 -0
  106. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  107. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  108. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  109. package/src/components/molecules/DatePicker/index.ts +2 -0
  110. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  111. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  112. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  113. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  114. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  115. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  116. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  117. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  118. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  119. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  120. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  121. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  122. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  123. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  124. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  125. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  126. package/src/components/molecules/Drawer/index.ts +12 -0
  127. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  128. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  129. package/src/components/molecules/FilterTab/index.ts +2 -0
  130. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  131. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  132. package/src/components/molecules/MessageCard/index.ts +10 -0
  133. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  134. package/src/components/molecules/Notification/Notification.tsx +426 -0
  135. package/src/components/molecules/Notification/index.ts +2 -0
  136. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  137. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  138. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  139. package/src/components/molecules/NumberField/index.ts +2 -0
  140. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  141. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  142. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  143. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
  144. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  145. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
  146. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  147. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  148. package/src/components/molecules/PasswordField/index.ts +10 -0
  149. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
  150. package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
  151. package/src/components/molecules/PictureSelector/index.ts +5 -0
  152. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  153. package/src/components/molecules/Progress/Progress.tsx +184 -0
  154. package/src/components/molecules/Progress/index.ts +2 -0
  155. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  156. package/src/components/molecules/Radio/Radio.tsx +1 -2
  157. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  158. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  159. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  160. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  161. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  162. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  163. package/src/components/molecules/SearchField/index.ts +4 -0
  164. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  165. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  166. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  167. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  168. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  169. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  170. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  171. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  172. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  173. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  174. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  175. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  176. package/src/components/molecules/SelectField/index.ts +10 -0
  177. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  178. package/src/components/molecules/Slider/Slider.tsx +293 -180
  179. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  180. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  181. package/src/components/molecules/Tooltip/index.ts +2 -0
  182. package/src/components/molecules/index.ts +15 -0
  183. package/src/test-utils.tsx +20 -0
  184. package/tsconfig.json +1 -1
  185. package/tsup.config.ts +16 -2
  186. package/vitest.config.ts +114 -0
  187. package/vitest.setup.ts +16 -0
@@ -0,0 +1,326 @@
1
+ import React, { useState } from "react"
2
+ import { View, Platform, type ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import * as TooltipPrimitive from "@rn-primitives/tooltip"
6
+ import { Typography } from "../../atoms"
7
+ import { Animated } from "../Animated"
8
+
9
+ const parseTokenValue = (value: string): number => parseFloat(value)
10
+
11
+ type TooltipAlignment = "left" | "middle" | "right"
12
+ type TooltipPosition = "top" | "bottom"
13
+
14
+ type TooltipOwnProps = {
15
+ text: string
16
+ alignment?: TooltipAlignment
17
+ position?: TooltipPosition
18
+ /**
19
+ * Trigger element. When provided the component manages its own open/close
20
+ * state (press-to-toggle, animated). Omit for static rendering (docs stories).
21
+ *
22
+ * Requires a <PortalHost /> at the application root.
23
+ */
24
+ children?: React.ReactNode
25
+ }
26
+
27
+ export type TooltipProps = TooltipOwnProps &
28
+ Omit<ViewProps, keyof TooltipOwnProps>
29
+
30
+ const NIB_OFFSET: Record<TooltipAlignment, number> = {
31
+ left: 24,
32
+ middle: 146,
33
+ right: 268
34
+ }
35
+
36
+ // ─── Styled primitives ────────────────────────────────────────────────────────
37
+
38
+ const Container = styled(View)<{ $width: number }>(({ $width }) => ({
39
+ width: $width
40
+ }))
41
+
42
+ const StyledBody = styled(View)<{
43
+ bgColor: string
44
+ borderColor: string
45
+ borderW: number
46
+ borderR: number
47
+ topPad: number
48
+ bottomPad: number
49
+ horizontalPad: number
50
+ }>(
51
+ ({
52
+ bgColor,
53
+ borderColor,
54
+ borderW,
55
+ borderR,
56
+ topPad,
57
+ bottomPad,
58
+ horizontalPad
59
+ }) => ({
60
+ backgroundColor: bgColor,
61
+ borderWidth: borderW,
62
+ borderColor,
63
+ borderRadius: borderR,
64
+ paddingTop: topPad,
65
+ paddingBottom: bottomPad,
66
+ paddingLeft: horizontalPad,
67
+ paddingRight: horizontalPad
68
+ })
69
+ )
70
+
71
+ // ─── Nib triangle ─────────────────────────────────────────────────────────────
72
+
73
+ type NibTriangleProps = {
74
+ position: TooltipPosition
75
+ nibX: number
76
+ fillColor: string
77
+ borderColor: string
78
+ nibHalfWidth: number
79
+ nibHeight: number
80
+ }
81
+
82
+ function NibTriangle({
83
+ position,
84
+ nibX,
85
+ fillColor,
86
+ borderColor,
87
+ nibHalfWidth,
88
+ nibHeight
89
+ }: NibTriangleProps) {
90
+ const pointingUp = position === "top"
91
+ const nibOuterHalfWidth = nibHalfWidth + 1
92
+ const nibOuterHeight = nibHeight + 1
93
+
94
+ const outerStyle = pointingUp
95
+ ? {
96
+ borderLeftWidth: nibOuterHalfWidth,
97
+ borderRightWidth: nibOuterHalfWidth,
98
+ borderBottomWidth: nibOuterHeight,
99
+ borderLeftColor: "transparent" as const,
100
+ borderRightColor: "transparent" as const,
101
+ borderBottomColor: borderColor
102
+ }
103
+ : {
104
+ borderLeftWidth: nibOuterHalfWidth,
105
+ borderRightWidth: nibOuterHalfWidth,
106
+ borderTopWidth: nibOuterHeight,
107
+ borderLeftColor: "transparent" as const,
108
+ borderRightColor: "transparent" as const,
109
+ borderTopColor: borderColor
110
+ }
111
+
112
+ const innerStyle = pointingUp
113
+ ? {
114
+ borderLeftWidth: nibHalfWidth,
115
+ borderRightWidth: nibHalfWidth,
116
+ borderBottomWidth: nibHeight,
117
+ borderLeftColor: "transparent" as const,
118
+ borderRightColor: "transparent" as const,
119
+ borderBottomColor: fillColor
120
+ }
121
+ : {
122
+ borderLeftWidth: nibHalfWidth,
123
+ borderRightWidth: nibHalfWidth,
124
+ borderTopWidth: nibHeight,
125
+ borderLeftColor: "transparent" as const,
126
+ borderRightColor: "transparent" as const,
127
+ borderTopColor: fillColor
128
+ }
129
+
130
+ return (
131
+ <View
132
+ style={{
133
+ position: "absolute",
134
+ left: nibX - 1,
135
+ width: nibOuterHalfWidth * 2,
136
+ height: nibOuterHeight,
137
+ ...(pointingUp ? { bottom: 0 } : { top: 0 })
138
+ }}
139
+ >
140
+ <View
141
+ style={{
142
+ position: "absolute",
143
+ left: 0,
144
+ top: 0,
145
+ width: 0,
146
+ height: 0,
147
+ ...outerStyle
148
+ }}
149
+ />
150
+ <View
151
+ style={{
152
+ position: "absolute",
153
+ left: 1,
154
+ top: pointingUp ? 1 : 0,
155
+ width: 0,
156
+ height: 0,
157
+ ...innerStyle
158
+ }}
159
+ />
160
+ </View>
161
+ )
162
+ }
163
+
164
+ // ─── Component ────────────────────────────────────────────────────────────────
165
+
166
+ export const Tooltip = React.forwardRef<View, TooltipProps>(
167
+ (
168
+ { text, alignment = "middle", position = "bottom", children, ...props },
169
+ ref
170
+ ) => {
171
+ const theme = useTheme()
172
+ const {
173
+ colour,
174
+ spacing,
175
+ borderRadius,
176
+ borderWidth,
177
+ typography,
178
+ nib: nibToken,
179
+ width
180
+ } = theme.tokens.components.tooltip
181
+
182
+ const [open, setOpen] = useState(false)
183
+
184
+ const tooltipWidth = parseTokenValue(width)
185
+ const nibWidth = parseTokenValue(nibToken.width)
186
+ const nibHeight = parseTokenValue(nibToken.height)
187
+ const nibHalfWidth = nibWidth / 2
188
+ const nibOuterHeight = nibHeight + 1
189
+
190
+ const nibX = NIB_OFFSET[alignment]
191
+ const nibCenterX = nibX + nibHalfWidth
192
+ // Shift bubble so nib centre aligns with trigger centre
193
+ const alignOffset = tooltipWidth / 2 - nibCenterX
194
+ // position="bottom" means nib is below the body → bubble is above trigger
195
+ const side = position === "bottom" ? "top" : ("bottom" as const)
196
+
197
+ const bgColor = colour.background.default
198
+ const borderColor = colour.border.default
199
+ const borderW = parseTokenValue(borderWidth.default)
200
+ const borderR = parseTokenValue(borderRadius.default)
201
+ const topPad = parseTokenValue(spacing.topPadding)
202
+ const bottomPad = parseTokenValue(spacing.bottomPadding)
203
+ const horizontalPad = parseTokenValue(spacing.horizontalPading)
204
+
205
+ const nibRow = (
206
+ <View style={{ height: nibOuterHeight, position: "relative", zIndex: 2 }}>
207
+ <NibTriangle
208
+ position={position}
209
+ nibX={nibX}
210
+ fillColor={bgColor}
211
+ borderColor={borderColor}
212
+ nibHalfWidth={nibHalfWidth}
213
+ nibHeight={nibHeight}
214
+ />
215
+ </View>
216
+ )
217
+
218
+ const bubble = (
219
+ <Container
220
+ $width={tooltipWidth}
221
+ ref={ref}
222
+ accessible={true}
223
+ accessibilityRole="none"
224
+ accessibilityLabel={text}
225
+ accessibilityLiveRegion="polite"
226
+ {...props}
227
+ >
228
+ {position === "top" && nibRow}
229
+ <StyledBody
230
+ bgColor={bgColor}
231
+ borderColor={borderColor}
232
+ borderW={borderW}
233
+ borderR={borderR}
234
+ topPad={topPad}
235
+ bottomPad={bottomPad}
236
+ horizontalPad={horizontalPad}
237
+ style={{
238
+ marginTop: position === "top" ? -1 : 0,
239
+ marginBottom: position === "bottom" ? -1 : 0,
240
+ zIndex: 1,
241
+ ...Platform.select({
242
+ web: { boxShadow: "0px 4px 8px rgba(82,42,16,0.08)" },
243
+ ios: {
244
+ shadowColor: "#522a10",
245
+ shadowOffset: { width: 0, height: 4 },
246
+ shadowOpacity: 0.08,
247
+ shadowRadius: 8
248
+ },
249
+ android: { elevation: 4 }
250
+ })
251
+ }}
252
+ >
253
+ <Typography token={typography.default} color={colour.text.default}>
254
+ {text}
255
+ </Typography>
256
+ </StyledBody>
257
+ {position === "bottom" && nibRow}
258
+ </Container>
259
+ )
260
+
261
+ // Static mode — no trigger, bubble always visible (documentation stories)
262
+ if (children === undefined) {
263
+ return bubble
264
+ }
265
+
266
+ // Web (Storybook Native): inject onPress into the trigger child and
267
+ // position the bubble absolutely so it doesn't affect the trigger's layout.
268
+ // Nested Pressables steal the touch responder, so we merge onPress via
269
+ // cloneElement instead of wrapping. Absolute positioning prevents the
270
+ // bubble from expanding the trigger's container.
271
+ if (Platform.OS === "web") {
272
+ type PressableChild = { onPress?: (() => void) | undefined }
273
+ const child = React.isValidElement<PressableChild>(children)
274
+ ? children
275
+ : null
276
+ const trigger = child
277
+ ? React.cloneElement(child, {
278
+ onPress: () => {
279
+ child.props.onPress?.()
280
+ setOpen((o) => !o)
281
+ }
282
+ })
283
+ : children
284
+
285
+ return (
286
+ <View>
287
+ {trigger}
288
+ {open && (
289
+ <View
290
+ style={{
291
+ position: "absolute",
292
+ left: "50%",
293
+ zIndex: 9999,
294
+ ...(side === "top" ? { bottom: "100%" } : { top: "100%" }),
295
+ transform: [{ translateX: -(tooltipWidth / 2) + alignOffset }]
296
+ }}
297
+ >
298
+ <Animated variant="scale">{bubble}</Animated>
299
+ </View>
300
+ )}
301
+ </View>
302
+ )
303
+ }
304
+
305
+ // Native (iOS/Android): primitive handles press-to-toggle and positioning
306
+ const content = (
307
+ <TooltipPrimitive.Content
308
+ side={side}
309
+ sideOffset={4}
310
+ alignOffset={alignOffset}
311
+ avoidCollisions
312
+ >
313
+ <Animated variant="scale">{bubble}</Animated>
314
+ </TooltipPrimitive.Content>
315
+ )
316
+
317
+ return (
318
+ <TooltipPrimitive.Root>
319
+ <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
320
+ <TooltipPrimitive.Portal>{content}</TooltipPrimitive.Portal>
321
+ </TooltipPrimitive.Root>
322
+ )
323
+ }
324
+ )
325
+
326
+ Tooltip.displayName = "Tooltip"
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from "./Tooltip"
2
+ export type { TooltipProps } from "./Tooltip"
@@ -1,6 +1,21 @@
1
+ export * from "./Accordion"
2
+ export * from "./Animated"
3
+ export * from "./Drawer"
1
4
  export * from "./ButtonDock"
2
5
  export * from "./ButtonGroup"
3
6
  export * from "./Checkbox"
7
+ export * from "./CopyField"
8
+ export * from "./FilterTab"
9
+ export * from "./NumberField"
10
+ export * from "./PasswordField"
11
+ export * from "./Progress"
4
12
  export * from "./Radio"
13
+ export * from "./SearchField"
5
14
  export * from "./SegmentedControl"
15
+ export * from "./SelectField"
6
16
  export * from "./Slider"
17
+ export * from "./Notification"
18
+ export * from "./Tooltip"
19
+ export * from "./MessageCard"
20
+ export * from "./DatePicker"
21
+ export * from "./PictureSelector"
@@ -0,0 +1,20 @@
1
+ import React from "react"
2
+ import { render, type RenderOptions } from "@testing-library/react"
3
+ import { ThemeProvider } from "@emotion/react"
4
+ import { createPawprintTheme } from "./theme/utils"
5
+
6
+ const theme = createPawprintTheme()
7
+
8
+ export function renderWithTheme(
9
+ ui: React.ReactElement,
10
+ options?: Omit<RenderOptions, "wrapper">
11
+ ) {
12
+ return render(ui, {
13
+ wrapper: ({ children }) => (
14
+ <ThemeProvider theme={theme}>{children}</ThemeProvider>
15
+ ),
16
+ ...options
17
+ })
18
+ }
19
+
20
+ export { theme }
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "extends": "@repo/typescript-config/react-library.json",
3
- "include": ["src"],
3
+ "include": ["."],
4
4
  "exclude": ["node_modules", "dist"]
5
5
  }
package/tsup.config.ts CHANGED
@@ -6,6 +6,20 @@ export default defineConfig({
6
6
  dts: true,
7
7
  sourcemap: true,
8
8
  clean: true,
9
- external: ["react", "react-native"],
10
- treeshake: true
9
+ external: [
10
+ "react",
11
+ "react-native",
12
+ "react-native-gesture-handler",
13
+ "react-native-reanimated",
14
+ "react-native-svg",
15
+ "react-native-worklets",
16
+ /^@rn-primitives\/.*/
17
+ ],
18
+ treeshake: true,
19
+ esbuildOptions(options) {
20
+ options.loader = {
21
+ ...options.loader,
22
+ ".js": "jsx"
23
+ }
24
+ }
11
25
  })
@@ -0,0 +1,114 @@
1
+ import { defineConfig, type Plugin } from "vitest/config"
2
+ import { resolve } from "path"
3
+
4
+ /** Vite plugin that stubs binary asset imports (.woff2, .ttf, etc.) */
5
+ const stubAssets = (): Plugin => ({
6
+ name: "stub-font-assets",
7
+ resolveId(id) {
8
+ if (/\.woff2$/.test(id)) return `\0font-stub:${id}`
9
+ return undefined
10
+ },
11
+ load(id) {
12
+ if (id.startsWith("\0font-stub:")) return "export default 'font-stub'"
13
+ return undefined
14
+ }
15
+ })
16
+
17
+ export default defineConfig({
18
+ plugins: [stubAssets()],
19
+ test: {
20
+ environment: "jsdom",
21
+ setupFiles: ["./vitest.setup.ts"],
22
+ globals: true
23
+ },
24
+ resolve: {
25
+ alias: [
26
+ {
27
+ find: /^react-native$/,
28
+ replacement: resolve(__dirname, "src/__mocks__/react-native.tsx")
29
+ },
30
+ {
31
+ find: /^@emotion\/native$/,
32
+ replacement: resolve(__dirname, "src/__mocks__/emotion-native.tsx")
33
+ },
34
+ {
35
+ find: /^@rn-primitives\/avatar$/,
36
+ replacement: resolve(
37
+ __dirname,
38
+ "src/__mocks__/rn-primitives/avatar.tsx"
39
+ )
40
+ },
41
+ {
42
+ find: /^@rn-primitives\/checkbox$/,
43
+ replacement: resolve(
44
+ __dirname,
45
+ "src/__mocks__/rn-primitives/checkbox.tsx"
46
+ )
47
+ },
48
+ {
49
+ find: /^@rn-primitives\/switch$/,
50
+ replacement: resolve(
51
+ __dirname,
52
+ "src/__mocks__/rn-primitives/switch.tsx"
53
+ )
54
+ },
55
+ {
56
+ find: /^@rn-primitives\/slider$/,
57
+ replacement: resolve(
58
+ __dirname,
59
+ "src/__mocks__/rn-primitives/slider.tsx"
60
+ )
61
+ },
62
+ {
63
+ find: /^@rn-primitives\/select$/,
64
+ replacement: resolve(
65
+ __dirname,
66
+ "src/__mocks__/rn-primitives/select.tsx"
67
+ )
68
+ },
69
+ {
70
+ find: /^@rn-primitives\/slot$/,
71
+ replacement: resolve(__dirname, "src/__mocks__/rn-primitives/slot.tsx")
72
+ },
73
+ {
74
+ find: /^@rn-primitives\/toggle$/,
75
+ replacement: resolve(
76
+ __dirname,
77
+ "src/__mocks__/rn-primitives/toggle.tsx"
78
+ )
79
+ },
80
+ {
81
+ find: /^react-native-gesture-handler$/,
82
+ replacement: resolve(
83
+ __dirname,
84
+ "src/__mocks__/react-native-gesture-handler.tsx"
85
+ )
86
+ },
87
+ {
88
+ find: /^react-native-reanimated$/,
89
+ replacement: resolve(
90
+ __dirname,
91
+ "src/__mocks__/react-native-reanimated.tsx"
92
+ )
93
+ },
94
+ {
95
+ find: /^react-native-worklets$/,
96
+ replacement: resolve(
97
+ __dirname,
98
+ "src/__mocks__/react-native-worklets.tsx"
99
+ )
100
+ },
101
+ {
102
+ find: /^react-native-safe-area-context$/,
103
+ replacement: resolve(
104
+ __dirname,
105
+ "src/__mocks__/react-native-safe-area-context.tsx"
106
+ )
107
+ },
108
+ {
109
+ find: /^react-native-svg$/,
110
+ replacement: resolve(__dirname, "src/__mocks__/react-native-svg.tsx")
111
+ }
112
+ ]
113
+ }
114
+ })
@@ -0,0 +1,16 @@
1
+ import "@testing-library/jest-dom/vitest"
2
+
3
+ // Suppress React DOM warnings for unknown props forwarded by styled mock
4
+ const originalConsoleError = console.error
5
+ console.error = (...args: unknown[]) => {
6
+ const msg = typeof args[0] === "string" ? args[0] : ""
7
+ if (
8
+ msg.includes("React does not recognize") ||
9
+ msg.includes("Unknown event handler property") ||
10
+ msg.includes("is not a valid DOM attribute") ||
11
+ msg.includes("Invalid DOM property")
12
+ ) {
13
+ return
14
+ }
15
+ originalConsoleError(...args)
16
+ }