@butternutbox/pawprint-native 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -0,0 +1,71 @@
1
+ import React from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { Divider } from "./Divider"
4
+ import { Typography } from "../Typography"
5
+ import type { DividerProps } from "./Divider"
6
+
7
+ export default {
8
+ title: "Atoms/Divider",
9
+ component: Divider,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "A horizontal rule used to separate content, group related elements, and establish a clear visual hierarchy. Native counterpart to the web Divider."
15
+ }
16
+ }
17
+ },
18
+ argTypes: {
19
+ size: {
20
+ control: { type: "select" },
21
+ options: ["small", "medium", "large"],
22
+ description:
23
+ "Thickness of the divider (small = 1px, medium = 2px, large = 4px)"
24
+ },
25
+ colour: {
26
+ control: { type: "select" },
27
+ options: ["light", "dark"],
28
+ description: "Colour variant of the divider"
29
+ }
30
+ }
31
+ }
32
+
33
+ const sizes = ["small", "medium", "large"] as const
34
+ const colours = ["light", "dark"] as const
35
+
36
+ export const Default = (args: DividerProps) => (
37
+ <View style={styles.container}>
38
+ <Divider {...args} />
39
+ </View>
40
+ )
41
+ Default.args = {
42
+ size: "small",
43
+ colour: "light"
44
+ }
45
+
46
+ export const AllVariants = () => (
47
+ <View style={styles.column}>
48
+ {colours.map((colour) => (
49
+ <View key={colour} style={styles.section}>
50
+ <Typography variant="heading" size="sm">
51
+ {colour}
52
+ </Typography>
53
+ {sizes.map((size) => (
54
+ <View key={size} style={styles.row}>
55
+ <Typography variant="body" size="sm">
56
+ {size}
57
+ </Typography>
58
+ <Divider size={size} colour={colour} />
59
+ </View>
60
+ ))}
61
+ </View>
62
+ ))}
63
+ </View>
64
+ )
65
+
66
+ const styles = StyleSheet.create({
67
+ container: { width: 300 },
68
+ column: { width: 300, gap: 24 },
69
+ section: { gap: 12 },
70
+ row: { gap: 4 }
71
+ })
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import { describe, it, expect } from "vitest"
3
+ import { renderWithTheme } from "../../../test-utils"
4
+ import { Divider } from "./Divider"
5
+
6
+ describe("Divider", () => {
7
+ it("renders with default size and colour", () => {
8
+ const { container } = renderWithTheme(<Divider />)
9
+ expect(container.firstChild).toBeInTheDocument()
10
+ })
11
+
12
+ it("renders size and colour variants", () => {
13
+ const { container } = renderWithTheme(
14
+ <Divider size="large" colour="dark" />
15
+ )
16
+ expect(container.firstChild).toBeInTheDocument()
17
+ })
18
+
19
+ it("forwards ref to the root element", () => {
20
+ const ref = React.createRef<any>()
21
+ renderWithTheme(<Divider ref={ref} />)
22
+ expect(ref.current).toBeTruthy()
23
+ expect(ref.current).toBeInTheDocument()
24
+ })
25
+ })
@@ -0,0 +1,61 @@
1
+ import React from "react"
2
+ import { View } from "react-native"
3
+ import styled from "@emotion/native"
4
+
5
+ type DividerSize = "small" | "medium" | "large"
6
+ type DividerColour = "light" | "dark"
7
+
8
+ type DividerOwnProps = {
9
+ size?: DividerSize
10
+ colour?: DividerColour
11
+ }
12
+
13
+ type DividerProps = DividerOwnProps &
14
+ Omit<React.ComponentProps<typeof View>, keyof DividerOwnProps>
15
+
16
+ const parseTokenValue = (value: string): number => parseFloat(value)
17
+
18
+ const StyledDivider = styled(View)<{
19
+ dividerSize: DividerSize
20
+ dividerColour: DividerColour
21
+ }>(({ theme, dividerSize, dividerColour }) => {
22
+ const { colour, sizing } = theme.tokens.components.dividers
23
+
24
+ return {
25
+ width: "100%",
26
+ height: parseTokenValue(sizing[dividerSize].height),
27
+ backgroundColor: colour[dividerColour]
28
+ }
29
+ })
30
+
31
+ /**
32
+ * A horizontal rule used to separate content, group related elements, and
33
+ * establish a clear visual hierarchy. Native counterpart to the web `Divider`.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { Divider } from "@butternutbox/pawprint-native"
38
+ *
39
+ * <Divider />
40
+ * <Divider size="medium" colour="dark" />
41
+ * ```
42
+ *
43
+ * @param {"small" | "medium" | "large"} [size="small"] - Thickness variant
44
+ * (small = 1px, medium = 2px, large = 4px).
45
+ * @param {"light" | "dark"} [colour="light"] - Colour variant.
46
+ */
47
+ const Divider = React.forwardRef<View, DividerProps>(
48
+ ({ size = "small", colour = "light", ...rest }, ref) => (
49
+ <StyledDivider
50
+ ref={ref}
51
+ dividerSize={size}
52
+ dividerColour={colour}
53
+ {...rest}
54
+ />
55
+ )
56
+ )
57
+
58
+ Divider.displayName = "Divider"
59
+
60
+ export { Divider }
61
+ export type { DividerProps, DividerSize, DividerColour }
@@ -0,0 +1,2 @@
1
+ export { Divider } from "./Divider"
2
+ export type { DividerProps, DividerSize, DividerColour } from "./Divider"
@@ -3,6 +3,7 @@ export * from "./CarouselControls"
3
3
  export * from "./Typography"
4
4
  export * from "./Badge"
5
5
  export * from "./Button"
6
+ export * from "./Divider"
6
7
  export * from "./Hint"
7
8
  export * from "./Icon"
8
9
  export * from "./IconButton"
@@ -19,6 +19,12 @@ type CheckboxOwnProps = {
19
19
  disabled?: boolean
20
20
  name?: string
21
21
  value?: string
22
+ /**
23
+ * Tile only: hug the content width instead of filling the container, so tiles
24
+ * can sit side by side and wrap. Set automatically by a horizontal
25
+ * `CheckboxGroup`; rarely needs setting by hand.
26
+ */
27
+ fitContent?: boolean
22
28
  }
23
29
 
24
30
  export type CheckboxProps = CheckboxOwnProps &
@@ -33,6 +39,7 @@ const StyledRootPressable = styled(Pressable)<{
33
39
  rootPaddingVertical?: number
34
40
  rootPaddingHorizontal?: number
35
41
  rootMaxWidth?: number
42
+ rootFitContent?: boolean
36
43
  rootBgColor?: string
37
44
  rootBorderWidth?: number
38
45
  rootBorderColor?: string
@@ -45,6 +52,7 @@ const StyledRootPressable = styled(Pressable)<{
45
52
  rootPaddingVertical,
46
53
  rootPaddingHorizontal,
47
54
  rootMaxWidth,
55
+ rootFitContent,
48
56
  rootBgColor,
49
57
  rootBorderWidth,
50
58
  rootBorderColor,
@@ -60,8 +68,12 @@ const StyledRootPressable = styled(Pressable)<{
60
68
  ...(rootPaddingHorizontal !== undefined
61
69
  ? { paddingHorizontal: rootPaddingHorizontal }
62
70
  : {}),
71
+ // `fitContent` tiles hug their content (for wrapping rows); otherwise the
72
+ // tile fills its container width.
63
73
  ...(rootMaxWidth !== undefined
64
- ? { maxWidth: rootMaxWidth, width: "100%" }
74
+ ? rootFitContent
75
+ ? { maxWidth: rootMaxWidth, alignSelf: "flex-start" }
76
+ : { maxWidth: rootMaxWidth, width: "100%" }
65
77
  : {}),
66
78
  ...(rootBgColor ? { backgroundColor: rootBgColor } : {}),
67
79
  ...(rootBorderWidth !== undefined
@@ -101,10 +113,11 @@ const StyledControl = styled(View)<{
101
113
 
102
114
  const StyledContent = styled(View)<{
103
115
  contentGap: number
104
- }>(({ contentGap }) => ({
116
+ contentFlex: number
117
+ }>(({ contentGap, contentFlex }) => ({
105
118
  flexDirection: "column",
106
119
  gap: contentGap,
107
- flex: 1,
120
+ flex: contentFlex,
108
121
  minWidth: 0
109
122
  }))
110
123
 
@@ -144,6 +157,7 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
144
157
  defaultChecked = false,
145
158
  onCheckedChange,
146
159
  disabled = false,
160
+ fitContent = false,
147
161
  style,
148
162
  ...rest
149
163
  },
@@ -201,6 +215,7 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
201
215
  ? parseTokenValue(checkbox.checkboxTile.size.maxWidth)
202
216
  : undefined
203
217
  }
218
+ rootFitContent={isTile && fitContent}
204
219
  rootBgColor={
205
220
  isTile
206
221
  ? isChecked
@@ -246,6 +261,7 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
246
261
 
247
262
  <StyledContent
248
263
  contentGap={parseTokenValue(checkbox.spacing.content.gap)}
264
+ contentFlex={isTile && fitContent ? 0 : 1}
249
265
  >
250
266
  {label && (
251
267
  <Typography
@@ -2,6 +2,7 @@ import React from "react"
2
2
  import { View, ViewProps } from "react-native"
3
3
  import styled from "@emotion/native"
4
4
  import { useTheme } from "@emotion/react"
5
+ import type { CheckboxProps } from "./Checkbox"
5
6
 
6
7
  type CheckboxGroupOwnProps = {
7
8
  children: React.ReactNode
@@ -16,20 +17,26 @@ const parseTokenValue = (value: string): number => parseFloat(value)
16
17
  const StyledGroup = styled(View)<{
17
18
  groupDirection: "row" | "column"
18
19
  groupGap: number
19
- }>(({ groupDirection, groupGap }) => ({
20
+ groupWrap: boolean
21
+ }>(({ groupDirection, groupGap, groupWrap }) => ({
20
22
  flexDirection: groupDirection,
21
- gap: groupGap
23
+ gap: groupGap,
24
+ flexWrap: groupWrap ? "wrap" : "nowrap"
22
25
  }))
23
26
 
24
27
  /**
25
28
  * CheckboxGroup manages layout for a collection of Checkboxes.
26
29
  *
30
+ * A `horizontal` group lays its tiles out in a wrapping row and makes them hug
31
+ * their content (via each Checkbox's `fitContent`) so they sit side by side;
32
+ * a `vertical` group stacks full-width tiles.
33
+ *
27
34
  * @param {"horizontal" | "vertical"} [orientation="vertical"] - Layout direction.
28
35
  *
29
36
  * @example
30
- * <CheckboxGroup>
31
- * <Checkbox label="Chicken" />
32
- * <Checkbox label="Beef" />
37
+ * <CheckboxGroup orientation="horizontal">
38
+ * <Checkbox variant="tile" label="Chicken" />
39
+ * <Checkbox variant="tile" label="Beef" />
33
40
  * </CheckboxGroup>
34
41
  */
35
42
  export const CheckboxGroup = React.forwardRef<View, CheckboxGroupProps>(
@@ -37,14 +44,31 @@ export const CheckboxGroup = React.forwardRef<View, CheckboxGroupProps>(
37
44
  const theme = useTheme()
38
45
  const { checkbox } = theme.tokens.components
39
46
 
47
+ const isHorizontal = orientation === "horizontal"
48
+
49
+ // Horizontal tiles hug their content so they can wrap; let an explicit
50
+ // per-Checkbox `fitContent` still win.
51
+ const renderChildren = (): React.ReactNode =>
52
+ isHorizontal
53
+ ? React.Children.map(children, (child) => {
54
+ if (!React.isValidElement(child)) return child
55
+ const childProps = child.props as CheckboxProps
56
+ return React.cloneElement(
57
+ child as React.ReactElement<CheckboxProps>,
58
+ { fitContent: childProps.fitContent ?? true }
59
+ )
60
+ })
61
+ : children
62
+
40
63
  return (
41
64
  <StyledGroup
42
65
  ref={ref}
43
- groupDirection={orientation === "horizontal" ? "row" : "column"}
66
+ groupDirection={isHorizontal ? "row" : "column"}
44
67
  groupGap={parseTokenValue(checkbox.spacing.gap)}
68
+ groupWrap={isHorizontal}
45
69
  {...rest}
46
70
  >
47
- {children}
71
+ {renderChildren()}
48
72
  </StyledGroup>
49
73
  )
50
74
  }