@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.
Files changed (182) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +16 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12370 -1455
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1110 -11
  7. package/dist/index.d.ts +1110 -11
  8. package/dist/index.js +12324 -1455
  9. package/dist/index.js.map +1 -1
  10. package/package.json +28 -9
  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.test.tsx +55 -0
  42. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  43. package/src/components/atoms/Input/Input.test.tsx +306 -0
  44. package/src/components/atoms/Input/Input.tsx +9 -1
  45. package/src/components/atoms/Input/InputField.tsx +226 -74
  46. package/src/components/atoms/Link/Link.test.tsx +89 -0
  47. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  48. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  49. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  50. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  51. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  52. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  53. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  54. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  55. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  56. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  57. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  58. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  61. package/src/components/atoms/Logo/assets/index.ts +11 -0
  62. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  63. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  64. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  65. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  66. package/src/components/atoms/NumberInput/index.ts +4 -0
  67. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  68. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  69. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  70. package/src/components/atoms/Switch/Switch.tsx +16 -13
  71. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  72. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  73. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  74. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  75. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  76. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  77. package/src/components/atoms/TextArea/index.ts +6 -0
  78. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  79. package/src/components/atoms/index.ts +3 -0
  80. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  81. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  82. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  83. package/src/components/molecules/Accordion/index.ts +6 -0
  84. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  85. package/src/components/molecules/Animated/Animated.tsx +283 -0
  86. package/src/components/molecules/Animated/index.ts +10 -0
  87. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  88. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
  89. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  90. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  91. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  92. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  93. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  94. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  95. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  96. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  97. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  98. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  99. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  100. package/src/components/molecules/CopyField/index.ts +4 -0
  101. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  102. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  103. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  104. package/src/components/molecules/DatePicker/index.ts +2 -0
  105. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  106. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  107. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  108. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  109. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  110. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  111. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  112. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  113. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  114. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  115. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  116. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  117. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  118. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  119. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  120. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  121. package/src/components/molecules/Drawer/index.ts +12 -0
  122. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  123. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  124. package/src/components/molecules/FilterTab/index.ts +2 -0
  125. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  126. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  127. package/src/components/molecules/MessageCard/index.ts +10 -0
  128. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  129. package/src/components/molecules/Notification/Notification.tsx +426 -0
  130. package/src/components/molecules/Notification/index.ts +2 -0
  131. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  132. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  133. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  134. package/src/components/molecules/NumberField/index.ts +2 -0
  135. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  136. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  137. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  138. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
  139. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  140. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
  141. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  142. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  143. package/src/components/molecules/PasswordField/index.ts +10 -0
  144. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
  145. package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
  146. package/src/components/molecules/PictureSelector/index.ts +5 -0
  147. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  148. package/src/components/molecules/Progress/Progress.tsx +184 -0
  149. package/src/components/molecules/Progress/index.ts +2 -0
  150. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  151. package/src/components/molecules/Radio/Radio.tsx +1 -2
  152. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  153. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  154. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  155. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  156. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  157. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  158. package/src/components/molecules/SearchField/index.ts +4 -0
  159. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  160. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  161. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  162. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  163. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  164. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  165. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  166. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  167. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  168. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  169. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  170. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  171. package/src/components/molecules/SelectField/index.ts +10 -0
  172. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  173. package/src/components/molecules/Slider/Slider.tsx +293 -180
  174. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  175. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  176. package/src/components/molecules/Tooltip/index.ts +2 -0
  177. package/src/components/molecules/index.ts +15 -0
  178. package/src/test-utils.tsx +20 -0
  179. package/tsconfig.json +1 -1
  180. package/tsup.config.ts +16 -2
  181. package/vitest.config.ts +114 -0
  182. package/vitest.setup.ts +16 -0
@@ -38,23 +38,32 @@ const Spinner = React.forwardRef<View, SpinnerProps>(
38
38
 
39
39
  const { size: sizeTokens, colour } = theme.tokens.components.spinner
40
40
  const borderWidth = parseTokenValue(
41
- theme.tokens.semantics.dimensions.borderWidth.md
41
+ theme.tokens.semantics.dimensions.borderWidth.lg
42
42
  )
43
43
  const baseColor = colour.background.base[variant]
44
44
  const progressColor = colour.background.progress[variant]
45
45
  const dimension = parseTokenValue(sizeTokens[size])
46
46
 
47
47
  useEffect(() => {
48
- const animation = Animated.loop(
48
+ let stopped = false
49
+ const animate = () => {
50
+ spinAnim.setValue(0)
49
51
  Animated.timing(spinAnim, {
50
52
  toValue: 1,
51
53
  duration: 600,
52
54
  easing: Easing.linear,
53
55
  useNativeDriver: true
56
+ }).start(({ finished }) => {
57
+ if (finished && !stopped) {
58
+ animate()
59
+ }
54
60
  })
55
- )
56
- animation.start()
57
- return () => animation.stop()
61
+ }
62
+ animate()
63
+ return () => {
64
+ stopped = true
65
+ spinAnim.stopAnimation()
66
+ }
58
67
  }, [spinAnim])
59
68
 
60
69
  const spin = spinAnim.interpolate({
@@ -0,0 +1,92 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { describe, it, expect, vi } from "vitest"
5
+ import { renderWithTheme } from "../../../test-utils"
6
+ import { Switch } from "./Switch"
7
+
8
+ describe("Switch", () => {
9
+ describe("when component is rendering", () => {
10
+ it("renders label", () => {
11
+ renderWithTheme(<Switch label="Notifications" />)
12
+ expect(screen.getByText("Notifications")).toBeInTheDocument()
13
+ })
14
+
15
+ it("renders label and subText", () => {
16
+ renderWithTheme(
17
+ <Switch label="SMS Notifications" subText="Receive SMS updates" />
18
+ )
19
+ expect(screen.getByText("SMS Notifications")).toBeInTheDocument()
20
+ expect(screen.getByText("Receive SMS updates")).toBeInTheDocument()
21
+ })
22
+
23
+ it("renders without label or subText", () => {
24
+ const { container } = renderWithTheme(<Switch />)
25
+ expect(container.firstChild).toBeTruthy()
26
+ })
27
+ })
28
+
29
+ describe("when component is uncontrolled", () => {
30
+ it("toggles checked state when clicked", async () => {
31
+ const user = userEvent.setup()
32
+ renderWithTheme(<Switch label="Toggle" />)
33
+
34
+ const switchEl = screen.getByRole("switch")
35
+ expect(switchEl).toHaveAttribute("aria-checked", "false")
36
+
37
+ await user.click(switchEl)
38
+ expect(switchEl).toHaveAttribute("aria-checked", "true")
39
+ })
40
+
41
+ it("does not toggle when disabled", async () => {
42
+ const user = userEvent.setup()
43
+ renderWithTheme(<Switch label="Toggle" disabled />)
44
+
45
+ const switchEl = screen.getByRole("switch")
46
+ expect(switchEl).toBeDisabled()
47
+
48
+ await user.click(switchEl)
49
+ expect(switchEl).toHaveAttribute("aria-checked", "false")
50
+ })
51
+
52
+ it("can be initially checked via defaultChecked", () => {
53
+ renderWithTheme(<Switch label="Toggle" defaultChecked />)
54
+
55
+ const switchEl = screen.getByRole("switch")
56
+ expect(switchEl).toHaveAttribute("aria-checked", "true")
57
+ })
58
+ })
59
+
60
+ describe("when component is controlled", () => {
61
+ it("calls onCheckedChange when clicked", async () => {
62
+ const user = userEvent.setup()
63
+ const onCheckedChange = vi.fn()
64
+ renderWithTheme(
65
+ <Switch
66
+ label="Toggle"
67
+ checked={false}
68
+ onCheckedChange={onCheckedChange}
69
+ />
70
+ )
71
+
72
+ await user.click(screen.getByRole("switch"))
73
+ expect(onCheckedChange).toHaveBeenCalledWith(true)
74
+ })
75
+
76
+ it("reflects controlled checked state", () => {
77
+ renderWithTheme(
78
+ <Switch label="Toggle" checked={true} onCheckedChange={() => {}} />
79
+ )
80
+
81
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "true")
82
+ })
83
+ })
84
+
85
+ describe("when using with ref", () => {
86
+ it("forwards ref", () => {
87
+ const ref = React.createRef<any>()
88
+ renderWithTheme(<Switch ref={ref} label="Ref test" />)
89
+ expect(ref.current).toBeTruthy()
90
+ })
91
+ })
92
+ })
@@ -28,7 +28,7 @@ const StyledContainer = styled(View)<{
28
28
  opacity: switchOpacity
29
29
  }))
30
30
 
31
- const StyledControl = styled(SwitchPrimitive.Root)<{
31
+ const StyledControlTrack = styled(View)<{
32
32
  switchChecked: boolean
33
33
  controlWidth: number
34
34
  controlHeight: number
@@ -129,7 +129,7 @@ export const Switch = React.forwardRef<View, SwitchProps>(
129
129
 
130
130
  const thumbTranslateX = animValue.interpolate({
131
131
  inputRange: [0, 1],
132
- outputRange: [inset, inset + translateX]
132
+ outputRange: [inset, inset + translateX + 3]
133
133
  })
134
134
 
135
135
  const handleCheckedChange = (checked: boolean) => {
@@ -146,19 +146,22 @@ export const Switch = React.forwardRef<View, SwitchProps>(
146
146
  switchOpacity={disabled ? parseFloat(opacity.disabled) : 1}
147
147
  {...rest}
148
148
  >
149
- <StyledControl
149
+ <SwitchPrimitive.Root
150
150
  checked={isChecked}
151
151
  onCheckedChange={handleCheckedChange}
152
152
  disabled={disabled}
153
- switchChecked={isChecked}
154
- controlWidth={controlWidth}
155
- controlHeight={controlHeight}
156
- controlBorderWidth={borderWidthValue}
157
- controlBorderColor={colour.control.border.default}
158
- controlBgChecked={colour.control.background.selected}
159
- controlBgDefault={colour.control.background.default}
153
+ asChild
160
154
  >
161
- <SwitchPrimitive.Thumb asChild>
155
+ <StyledControlTrack
156
+ switchChecked={isChecked}
157
+ controlWidth={controlWidth}
158
+ controlHeight={controlHeight}
159
+ controlBorderWidth={borderWidthValue}
160
+ controlBorderColor={colour.control.border.default}
161
+ controlBgChecked={colour.control.background.selected}
162
+ controlBgDefault={colour.control.background.default}
163
+ pointerEvents={disabled ? "none" : "auto"}
164
+ >
162
165
  <StyledThumb
163
166
  thumbSize={thumbSize}
164
167
  thumbBgColor={
@@ -168,8 +171,8 @@ export const Switch = React.forwardRef<View, SwitchProps>(
168
171
  }
169
172
  style={{ transform: [{ translateX: thumbTranslateX }] }}
170
173
  />
171
- </SwitchPrimitive.Thumb>
172
- </StyledControl>
174
+ </StyledControlTrack>
175
+ </SwitchPrimitive.Root>
173
176
 
174
177
  {(label || subText) && (
175
178
  <StyledContent contentGap={parseTokenValue(spacing.content.gap)}>
@@ -0,0 +1,70 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import { describe, it, expect } from "vitest"
4
+ import { renderWithTheme } from "../../../test-utils"
5
+ import { Tag } from "./Tag"
6
+
7
+ const MockIcon = ({ width, height, color }: any) => (
8
+ <svg data-testid="mock-icon" width={width} height={height} fill={color} />
9
+ )
10
+ MockIcon.category = "core" as const
11
+
12
+ describe("Tag", () => {
13
+ describe("when component is rendering", () => {
14
+ it("renders children text", () => {
15
+ renderWithTheme(<Tag>Company news</Tag>)
16
+ expect(screen.getByText("Company news")).toBeInTheDocument()
17
+ })
18
+
19
+ it("renders with default variant and size", () => {
20
+ renderWithTheme(<Tag>Default</Tag>)
21
+ expect(screen.getByText("Default")).toBeInTheDocument()
22
+ })
23
+ })
24
+
25
+ describe("when rendering all variants", () => {
26
+ it.each([
27
+ "primary",
28
+ "secondary",
29
+ "tertiary",
30
+ "promo",
31
+ "success",
32
+ "warning",
33
+ "error"
34
+ ] as const)("renders %s variant", (variant) => {
35
+ renderWithTheme(<Tag variant={variant}>{variant}</Tag>)
36
+ expect(screen.getByText(variant)).toBeInTheDocument()
37
+ })
38
+ })
39
+
40
+ describe("when rendering all sizes", () => {
41
+ it.each(["small", "medium", "large"] as const)(
42
+ "renders %s size",
43
+ (size) => {
44
+ renderWithTheme(<Tag size={size}>{size}</Tag>)
45
+ expect(screen.getByText(size)).toBeInTheDocument()
46
+ }
47
+ )
48
+ })
49
+
50
+ describe("when rendering with icon", () => {
51
+ it("renders with icon", () => {
52
+ renderWithTheme(<Tag icon={MockIcon}>With Icon</Tag>)
53
+ expect(screen.getByTestId("mock-icon")).toBeInTheDocument()
54
+ expect(screen.getByText("With Icon")).toBeInTheDocument()
55
+ })
56
+
57
+ it("renders without icon when not provided", () => {
58
+ renderWithTheme(<Tag>No Icon</Tag>)
59
+ expect(screen.queryByTestId("mock-icon")).not.toBeInTheDocument()
60
+ })
61
+ })
62
+
63
+ describe("when using with ref", () => {
64
+ it("forwards ref", () => {
65
+ const ref = React.createRef<any>()
66
+ renderWithTheme(<Tag ref={ref}>Ref test</Tag>)
67
+ expect(ref.current).toBeTruthy()
68
+ })
69
+ })
70
+ })
@@ -0,0 +1,303 @@
1
+ import React, { useState } from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { TextArea } from "./TextArea"
4
+ import type { TextAreaProps } from "./TextArea"
5
+ import type { InputState } from "../Input/InputField"
6
+ import { Icon } from "../Icon"
7
+ import { Search, Info } from "@butternutbox/pawprint-icons/core"
8
+
9
+ export default {
10
+ title: "Atoms/TextArea",
11
+ component: TextArea,
12
+ parameters: {
13
+ layout: "centered",
14
+ docs: {
15
+ description: {
16
+ component:
17
+ "Multi-line text input component for longer text entry. Supports both simple props API and compound component API."
18
+ }
19
+ }
20
+ },
21
+ argTypes: {
22
+ label: {
23
+ control: "text",
24
+ description: "Label text"
25
+ },
26
+ placeholder: {
27
+ control: "text",
28
+ description: "Placeholder text"
29
+ },
30
+ description: {
31
+ control: "text",
32
+ description: "Help text below textarea"
33
+ },
34
+ error: {
35
+ control: "text",
36
+ description: "Error message"
37
+ },
38
+ state: {
39
+ control: "select",
40
+ options: ["default", "error", "success"],
41
+ description: "Visual state of the textarea"
42
+ },
43
+ optionalText: {
44
+ control: "text",
45
+ description: "Optional indicator next to label"
46
+ },
47
+ maxLength: {
48
+ control: "number",
49
+ description:
50
+ "Maximum character length (shows character counter automatically)"
51
+ },
52
+ rows: {
53
+ control: "number",
54
+ description: "Number of visible text rows"
55
+ },
56
+ editable: {
57
+ control: "boolean",
58
+ description: "Controls whether the textarea is editable"
59
+ }
60
+ }
61
+ }
62
+
63
+ export const Default = (args: TextAreaProps) => (
64
+ <View style={{ width: 320 }}>
65
+ <TextArea {...args} />
66
+ </View>
67
+ )
68
+ Default.args = {
69
+ label: "Label",
70
+ placeholder: "Placeholder",
71
+ description: "Help text"
72
+ }
73
+
74
+ export const WithCharacterCount = () => (
75
+ <View style={styles.column}>
76
+ <TextArea
77
+ label="Description"
78
+ placeholder="Enter your description..."
79
+ description="Maximum 100 characters"
80
+ maxLength={100}
81
+ rows={4}
82
+ />
83
+ <TextArea
84
+ label="With Default Value"
85
+ placeholder="Enter your description..."
86
+ description="Maximum 100 characters"
87
+ defaultValue="Some text here"
88
+ maxLength={100}
89
+ rows={4}
90
+ />
91
+ </View>
92
+ )
93
+
94
+ export const WithIcons = () => (
95
+ <View style={styles.column}>
96
+ <TextArea
97
+ label="Leading Icon"
98
+ placeholder="Placeholder"
99
+ leadingIcon={<Icon icon={Search} size="md" />}
100
+ description="Help text"
101
+ />
102
+ <TextArea
103
+ label="Trailing Icon"
104
+ placeholder="Placeholder"
105
+ trailingIcon={<Icon icon={Search} size="md" />}
106
+ description="Help text"
107
+ />
108
+ <TextArea
109
+ label="Both Icons"
110
+ placeholder="Placeholder"
111
+ leadingIcon={<Icon icon={Search} size="md" />}
112
+ trailingIcon={<Icon icon={Info} size="md" />}
113
+ description="Help text"
114
+ />
115
+ </View>
116
+ )
117
+
118
+ export const States = () => (
119
+ <View style={styles.column}>
120
+ <TextArea
121
+ label="Default State"
122
+ placeholder="Enter text"
123
+ description="Normal textarea state"
124
+ />
125
+ <TextArea
126
+ label="Error State"
127
+ placeholder="Enter text"
128
+ state="error"
129
+ description="Manually set to error state"
130
+ />
131
+ <TextArea
132
+ label="Error with Custom Message"
133
+ placeholder="Enter text"
134
+ state="error"
135
+ description="Manually set to error state"
136
+ error="Custom error message"
137
+ />
138
+ <TextArea
139
+ label="Success State"
140
+ placeholder="Enter text"
141
+ state="success"
142
+ description="Manually set to success state"
143
+ />
144
+ <TextArea
145
+ label="Disabled"
146
+ placeholder="Enter text"
147
+ description="Help text"
148
+ editable={false}
149
+ />
150
+ </View>
151
+ )
152
+
153
+ export const Controlled = () => {
154
+ const [value, setValue] = useState("")
155
+
156
+ return (
157
+ <View style={styles.column}>
158
+ <TextArea
159
+ label="Controlled TextArea"
160
+ value={value}
161
+ onValueChange={setValue}
162
+ placeholder="Type something..."
163
+ description={`You typed: ${value.length} characters`}
164
+ />
165
+ </View>
166
+ )
167
+ }
168
+
169
+ export const CustomStateValidationWithSuccess = () => {
170
+ const [description, setDescription] = useState("")
171
+ const [descriptionState, setDescriptionState] =
172
+ useState<InputState>("default")
173
+ const [errorMessage, setErrorMessage] = useState("")
174
+
175
+ const validateDescription = (value: string) => {
176
+ if (!value) {
177
+ setDescriptionState("default")
178
+ setErrorMessage("")
179
+ return
180
+ }
181
+
182
+ const hasMinLength = value.length >= 10
183
+ const hasMaxLength = value.length <= 200
184
+
185
+ if (!hasMinLength) {
186
+ setDescriptionState("error")
187
+ setErrorMessage("Description must be at least 10 characters")
188
+ } else if (!hasMaxLength) {
189
+ setDescriptionState("error")
190
+ setErrorMessage("Description is too long (max 200 characters)")
191
+ } else {
192
+ setDescriptionState("success")
193
+ setErrorMessage("")
194
+ }
195
+ }
196
+
197
+ const handleChange = (newValue: string) => {
198
+ setDescription(newValue)
199
+ validateDescription(newValue)
200
+ }
201
+
202
+ return (
203
+ <View style={styles.column}>
204
+ <TextArea
205
+ label="Description"
206
+ placeholder="Enter description..."
207
+ value={description}
208
+ onValueChange={handleChange}
209
+ state={descriptionState}
210
+ description="10-200 characters"
211
+ error={errorMessage}
212
+ />
213
+ </View>
214
+ )
215
+ }
216
+
217
+ export const Validation = () => {
218
+ const [requiredValue, setRequiredValue] = useState("")
219
+ const [requiredTouched, setRequiredTouched] = useState(false)
220
+ const requiredError =
221
+ requiredTouched && requiredValue.length === 0
222
+ ? "This field is required"
223
+ : ""
224
+
225
+ const [minLengthValue, setMinLengthValue] = useState("")
226
+ const [minLengthBlurred, setMinLengthBlurred] = useState(false)
227
+ const minLengthError =
228
+ minLengthBlurred && minLengthValue.length > 0 && minLengthValue.length < 20
229
+ ? "Must be at least 20 characters"
230
+ : ""
231
+
232
+ return (
233
+ <View style={styles.column}>
234
+ <TextArea
235
+ label="valueMissing - Required Field"
236
+ placeholder="Enter text"
237
+ description="Validates on change • Leave empty to see error"
238
+ value={requiredValue}
239
+ onValueChange={(v) => {
240
+ setRequiredValue(v)
241
+ setRequiredTouched(true)
242
+ }}
243
+ state={requiredError ? "error" : "default"}
244
+ error={requiredError}
245
+ />
246
+
247
+ <TextArea
248
+ label="tooShort - Min Length"
249
+ placeholder="Min 20 chars"
250
+ description="Validates on blur • Type less than 20 characters"
251
+ value={minLengthValue}
252
+ onValueChange={setMinLengthValue}
253
+ onBlur={() => setMinLengthBlurred(true)}
254
+ state={minLengthError ? "error" : "default"}
255
+ error={minLengthError}
256
+ />
257
+ </View>
258
+ )
259
+ }
260
+
261
+ export const CompoundComponentAPI = () => (
262
+ <View style={styles.column}>
263
+ <TextArea.Root>
264
+ <TextArea.Label>Description</TextArea.Label>
265
+ <TextArea.Field placeholder="Enter text..." />
266
+ <TextArea.Description>Provide details</TextArea.Description>
267
+ </TextArea.Root>
268
+ <TextArea.Root>
269
+ <TextArea.Label state="error">Description</TextArea.Label>
270
+ <TextArea.Field placeholder="Enter text..." state="error" />
271
+ <TextArea.Error>Invalid description</TextArea.Error>
272
+ </TextArea.Root>
273
+ <TextArea.Root>
274
+ <TextArea.Label state="success">Description</TextArea.Label>
275
+ <TextArea.Field placeholder="Enter text..." state="success" />
276
+ <TextArea.Description state="success">
277
+ Description verified
278
+ </TextArea.Description>
279
+ </TextArea.Root>
280
+ <TextArea.Root>
281
+ <TextArea.Label
282
+ optionalText="(optional)"
283
+ maxLength={50}
284
+ currentLength={15}
285
+ >
286
+ Comments
287
+ </TextArea.Label>
288
+ <TextArea.Field
289
+ placeholder="Add comments..."
290
+ defaultValue="Some comments"
291
+ maxLength={50}
292
+ />
293
+ </TextArea.Root>
294
+ </View>
295
+ )
296
+
297
+ const styles = StyleSheet.create({
298
+ column: {
299
+ flexDirection: "column",
300
+ gap: 24,
301
+ width: 320
302
+ }
303
+ })