@scripso-homepad/ui 0.3.3 → 0.3.4
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/README.md +22 -9
- package/dist/index.cjs +105 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +105 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button.stories.tsx +88 -33
- package/src/components/Button.tsx +118 -11
- package/src/index.ts +5 -1
package/README.md
CHANGED
|
@@ -92,15 +92,28 @@ export function Example() {
|
|
|
92
92
|
|
|
93
93
|
### `Button`
|
|
94
94
|
|
|
95
|
-
| Prop | Type
|
|
96
|
-
| --------------- |
|
|
97
|
-
| `title` | `string`
|
|
98
|
-
| `onPress` | `() => void`
|
|
99
|
-
| `disabled` | `boolean`
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
95
|
+
| Prop | Type | Required | Default | Description |
|
|
96
|
+
| --------------- | ------------------------------------------------- | -------- | ----------- | -------------------------------------------------------- |
|
|
97
|
+
| `title` | `string` | Yes | — | Button label text |
|
|
98
|
+
| `onPress` | `() => void` | Yes | — | Press handler |
|
|
99
|
+
| `disabled` | `boolean` | No | `false` | Disables interaction |
|
|
100
|
+
| `variant` | `"primary" \| "secondary" \| "outline" \| "ghost"` | No | `"primary"` | Visual style preset |
|
|
101
|
+
| `size` | `"small" \| "medium" \| "large"` | No | `"medium"` | Size preset |
|
|
102
|
+
| `style` | `StyleProp<ViewStyle>` | No | — | Extra container styles (web + native) |
|
|
103
|
+
| `textStyle` | `StyleProp<TextStyle>` | No | — | Extra label styles (web + native) |
|
|
104
|
+
| `className` | `string` | No | — | CSS/Tailwind classes for container (web / NativeWind) |
|
|
105
|
+
| `textClassName` | `string` | No | — | CSS/Tailwind classes for label (web / NativeWind) |
|
|
106
|
+
|
|
107
|
+
#### Variants and sizes
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<Button title="Save" onPress={handleSave} variant="primary" size="medium" />
|
|
111
|
+
<Button title="Cancel" onPress={handleCancel} variant="outline" size="small" />
|
|
112
|
+
<Button title="Delete" onPress={handleDelete} variant="secondary" size="large" />
|
|
113
|
+
<Button title="More" onPress={handleMore} variant="ghost" />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Preview all combinations in Storybook: `npm run storybook` → **AllVariants**, **AllSizes**, **VariantMatrix**.
|
|
104
117
|
|
|
105
118
|
#### Custom styles (React Native `style`)
|
|
106
119
|
|
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,8 @@ function Button({
|
|
|
36
36
|
title,
|
|
37
37
|
onPress,
|
|
38
38
|
disabled = false,
|
|
39
|
+
variant = "primary",
|
|
40
|
+
size = "medium",
|
|
39
41
|
style,
|
|
40
42
|
textStyle,
|
|
41
43
|
className,
|
|
@@ -45,8 +47,20 @@ function Button({
|
|
|
45
47
|
const textRef = react.useRef(null);
|
|
46
48
|
useApplyWebClassName(containerRef, className);
|
|
47
49
|
useApplyWebClassName(textRef, textClassName);
|
|
48
|
-
const containerStyle = [
|
|
49
|
-
|
|
50
|
+
const containerStyle = [
|
|
51
|
+
styles.base,
|
|
52
|
+
variantStyles[variant],
|
|
53
|
+
sizeStyles[size],
|
|
54
|
+
disabled && disabledVariantStyles[variant],
|
|
55
|
+
style
|
|
56
|
+
];
|
|
57
|
+
const labelStyle = [
|
|
58
|
+
textBaseStyles.base,
|
|
59
|
+
textVariantStyles[variant],
|
|
60
|
+
textSizeStyles[size],
|
|
61
|
+
disabled && textDisabledStyles[variant],
|
|
62
|
+
textStyle
|
|
63
|
+
];
|
|
50
64
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
51
65
|
reactNative.TouchableOpacity,
|
|
52
66
|
{
|
|
@@ -62,27 +76,105 @@ function Button({
|
|
|
62
76
|
);
|
|
63
77
|
}
|
|
64
78
|
var styles = reactNative.StyleSheet.create({
|
|
65
|
-
|
|
66
|
-
backgroundColor: "#2563eb",
|
|
67
|
-
paddingVertical: 12,
|
|
68
|
-
paddingHorizontal: 24,
|
|
69
|
-
borderRadius: 8,
|
|
79
|
+
base: {
|
|
70
80
|
alignItems: "center",
|
|
71
81
|
justifyContent: "center",
|
|
72
|
-
|
|
82
|
+
borderRadius: 8,
|
|
73
83
|
borderWidth: 0
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
var variantStyles = reactNative.StyleSheet.create({
|
|
87
|
+
primary: {
|
|
88
|
+
backgroundColor: "#2563eb"
|
|
89
|
+
},
|
|
90
|
+
secondary: {
|
|
91
|
+
backgroundColor: "#4b5563"
|
|
92
|
+
},
|
|
93
|
+
outline: {
|
|
94
|
+
backgroundColor: "transparent",
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: "#2563eb"
|
|
97
|
+
},
|
|
98
|
+
ghost: {
|
|
99
|
+
backgroundColor: "transparent"
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
var sizeStyles = reactNative.StyleSheet.create({
|
|
103
|
+
small: {
|
|
104
|
+
paddingVertical: 8,
|
|
105
|
+
paddingHorizontal: 16,
|
|
106
|
+
minWidth: 96
|
|
107
|
+
},
|
|
108
|
+
medium: {
|
|
109
|
+
paddingVertical: 12,
|
|
110
|
+
paddingHorizontal: 24,
|
|
111
|
+
minWidth: 120
|
|
74
112
|
},
|
|
75
|
-
|
|
113
|
+
large: {
|
|
114
|
+
paddingVertical: 16,
|
|
115
|
+
paddingHorizontal: 32,
|
|
116
|
+
minWidth: 160
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
var disabledVariantStyles = reactNative.StyleSheet.create({
|
|
120
|
+
primary: {
|
|
76
121
|
backgroundColor: "#93c5fd",
|
|
77
122
|
opacity: 0.7
|
|
78
123
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
124
|
+
secondary: {
|
|
125
|
+
backgroundColor: "#9ca3af",
|
|
126
|
+
opacity: 0.7
|
|
127
|
+
},
|
|
128
|
+
outline: {
|
|
129
|
+
borderColor: "#93c5fd",
|
|
130
|
+
opacity: 0.7
|
|
131
|
+
},
|
|
132
|
+
ghost: {
|
|
133
|
+
opacity: 0.5
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
var textBaseStyles = reactNative.StyleSheet.create({
|
|
137
|
+
base: {
|
|
82
138
|
fontWeight: "600"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
var textVariantStyles = reactNative.StyleSheet.create({
|
|
142
|
+
primary: {
|
|
143
|
+
color: "#ffffff"
|
|
144
|
+
},
|
|
145
|
+
secondary: {
|
|
146
|
+
color: "#ffffff"
|
|
83
147
|
},
|
|
84
|
-
|
|
148
|
+
outline: {
|
|
149
|
+
color: "#2563eb"
|
|
150
|
+
},
|
|
151
|
+
ghost: {
|
|
152
|
+
color: "#2563eb"
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
var textSizeStyles = reactNative.StyleSheet.create({
|
|
156
|
+
small: {
|
|
157
|
+
fontSize: 14
|
|
158
|
+
},
|
|
159
|
+
medium: {
|
|
160
|
+
fontSize: 16
|
|
161
|
+
},
|
|
162
|
+
large: {
|
|
163
|
+
fontSize: 18
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
var textDisabledStyles = reactNative.StyleSheet.create({
|
|
167
|
+
primary: {
|
|
85
168
|
color: "#e5e7eb"
|
|
169
|
+
},
|
|
170
|
+
secondary: {
|
|
171
|
+
color: "#f3f4f6"
|
|
172
|
+
},
|
|
173
|
+
outline: {
|
|
174
|
+
color: "#93c5fd"
|
|
175
|
+
},
|
|
176
|
+
ghost: {
|
|
177
|
+
color: "#93c5fd"
|
|
86
178
|
}
|
|
87
179
|
});
|
|
88
180
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":["useLayoutEffect","Platform","useRef","jsx","TouchableOpacity","Text","StyleSheet"],"mappings":";;;;;;;AAUA,SAAS,aAAa,IAAA,EAAyC;AAC7D,EAAA,OACE,OAAO,IAAA,KAAS,QAAA,IAChB,IAAA,KAAS,IAAA,IACT,eAAe,IAAA,IACf,OAAQ,IAAA,CAA0B,SAAA,EAAW,GAAA,KAAQ,UAAA;AAEzD;AAEA,SAAS,kBAAkB,GAAA,EAAwD;AACjF,EAAA,MAAM,OAAO,GAAA,CAAI,OAAA;AACjB,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,IAAI,YAAA,CAAa,IAAI,CAAA,EAAG,OAAO,IAAA;AAE/B,EAAA,MAAM,IAAA,GAAO,IAAA;AAKb,EAAA,IAAI,YAAA,CAAa,IAAA,CAAK,cAAc,CAAA,SAAU,IAAA,CAAK,cAAA;AAEnD,EAAA,IAAI,OAAO,IAAA,CAAK,iBAAA,KAAsB,UAAA,EAAY;AAChD,IAAA,MAAM,UAAA,GAAa,KAAK,iBAAA,EAAkB;AAC1C,IAAA,IAAI,YAAA,CAAa,UAAU,CAAA,EAAG,OAAO,UAAA;AAAA,EACvC;AAEA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,oBAAA,CACd,KACA,SAAA,EACM;AACN,EAAAA,qBAAA,CAAgB,MAAM;AACpB,IAAA,IAAIC,qBAAS,EAAA,KAAO,KAAA,IAAS,CAAC,SAAA,EAAW,MAAK,EAAG;AAEjD,IAAA,MAAM,OAAA,GAAU,kBAAkB,GAAG,CAAA;AACrC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,MAAM,OAAA,GAAU,SAAA,CAAU,IAAA,EAAK,CAAE,MAAM,KAAK,CAAA;AAC5C,IAAA,OAAA,CAAQ,SAAA,CAAU,GAAA,CAAI,GAAG,OAAO,CAAA;AAEhC,IAAA,OAAO,MAAM;AACX,MAAA,OAAA,CAAQ,SAAA,CAAU,MAAA,CAAO,GAAG,OAAO,CAAA;AAAA,IACrC,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,SAAS,CAAC,CAAA;AACrB;AC7BO,SAAS,MAAA,CAAO;AAAA,EACrB,KAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA,GAAW,KAAA;AAAA,EACX,KAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAAgB;AACd,EAAA,MAAM,YAAA,GAAeC,aAA8C,IAAI,CAAA;AACvE,EAAA,MAAM,OAAA,GAAUA,aAAkC,IAAI,CAAA;AAEtD,EAAA,oBAAA,CAAqB,cAAc,SAAS,CAAA;AAC5C,EAAA,oBAAA,CAAqB,SAAS,aAAa,CAAA;AAE3C,EAAA,MAAM,iBAAiB,CAAC,MAAA,CAAO,QAAQ,QAAA,IAAY,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC/E,EAAA,MAAM,aAAa,CAAC,MAAA,CAAO,MAAM,QAAA,IAAY,MAAA,CAAO,cAAc,SAAS,CAAA;AAE3E,EAAA,uBACEC,cAAA;AAAA,IAACC,4BAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,YAAA;AAAA,MACL,KAAA,EAAO,cAAA;AAAA,MACP,OAAA;AAAA,MACA,QAAA;AAAA,MACA,aAAA,EAAe,GAAA;AAAA,MACf,iBAAA,EAAkB,QAAA;AAAA,MAClB,kBAAA,EAAoB,EAAE,QAAA,EAAS;AAAA,MAE/B,yCAACC,gBAAA,EAAA,EAAK,GAAA,EAAK,OAAA,EAAS,KAAA,EAAO,YACxB,QAAA,EAAA,KAAA,EACH;AAAA;AAAA,GACF;AAEJ;AAEA,IAAM,MAAA,GAASC,uBAAW,MAAA,CAAO;AAAA,EAC/B,MAAA,EAAQ;AAAA,IACN,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,EAAA;AAAA,IACjB,iBAAA,EAAmB,EAAA;AAAA,IACnB,YAAA,EAAc,CAAA;AAAA,IACd,UAAA,EAAY,QAAA;AAAA,IACZ,cAAA,EAAgB,QAAA;AAAA,IAChB,QAAA,EAAU,GAAA;AAAA,IACV,WAAA,EAAa;AAAA,GACf;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,eAAA,EAAiB,SAAA;AAAA,IACjB,OAAA,EAAS;AAAA,GACX;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,KAAA,EAAO,SAAA;AAAA,IACP,QAAA,EAAU,EAAA;AAAA,IACV,UAAA,EAAY;AAAA,GACd;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,KAAA,EAAO;AAAA;AAEX,CAAC,CAAA","file":"index.cjs","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":["useLayoutEffect","Platform","useRef","jsx","TouchableOpacity","Text","StyleSheet"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\";\nexport type ButtonSize = \"small\" | \"medium\" | \"large\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Visual style preset. */\n variant?: ButtonVariant;\n /** Size preset. */\n size?: ButtonSize;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n variant = \"primary\",\n size = \"medium\",\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [\n styles.base,\n variantStyles[variant],\n sizeStyles[size],\n disabled && disabledVariantStyles[variant],\n style,\n ];\n\n const labelStyle = [\n textBaseStyles.base,\n textVariantStyles[variant],\n textSizeStyles[size],\n disabled && textDisabledStyles[variant],\n textStyle,\n ];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n base: {\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: 8,\n borderWidth: 0,\n },\n});\n\nconst variantStyles = StyleSheet.create({\n primary: {\n backgroundColor: \"#2563eb\",\n },\n secondary: {\n backgroundColor: \"#4b5563\",\n },\n outline: {\n backgroundColor: \"transparent\",\n borderWidth: 1,\n borderColor: \"#2563eb\",\n },\n ghost: {\n backgroundColor: \"transparent\",\n },\n});\n\nconst sizeStyles = StyleSheet.create({\n small: {\n paddingVertical: 8,\n paddingHorizontal: 16,\n minWidth: 96,\n },\n medium: {\n paddingVertical: 12,\n paddingHorizontal: 24,\n minWidth: 120,\n },\n large: {\n paddingVertical: 16,\n paddingHorizontal: 32,\n minWidth: 160,\n },\n});\n\nconst disabledVariantStyles = StyleSheet.create({\n primary: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n secondary: {\n backgroundColor: \"#9ca3af\",\n opacity: 0.7,\n },\n outline: {\n borderColor: \"#93c5fd\",\n opacity: 0.7,\n },\n ghost: {\n opacity: 0.5,\n },\n});\n\nconst textBaseStyles = StyleSheet.create({\n base: {\n fontWeight: \"600\",\n },\n});\n\nconst textVariantStyles = StyleSheet.create({\n primary: {\n color: \"#ffffff\",\n },\n secondary: {\n color: \"#ffffff\",\n },\n outline: {\n color: \"#2563eb\",\n },\n ghost: {\n color: \"#2563eb\",\n },\n});\n\nconst textSizeStyles = StyleSheet.create({\n small: {\n fontSize: 14,\n },\n medium: {\n fontSize: 16,\n },\n large: {\n fontSize: 18,\n },\n});\n\nconst textDisabledStyles = StyleSheet.create({\n primary: {\n color: \"#e5e7eb\",\n },\n secondary: {\n color: \"#f3f4f6\",\n },\n outline: {\n color: \"#93c5fd\",\n },\n ghost: {\n color: \"#93c5fd\",\n },\n});\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
+
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
|
5
|
+
type ButtonSize = "small" | "medium" | "large";
|
|
4
6
|
interface ButtonProps {
|
|
5
7
|
title: string;
|
|
6
8
|
onPress: (event: GestureResponderEvent) => void;
|
|
7
9
|
disabled?: boolean;
|
|
10
|
+
/** Visual style preset. */
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
/** Size preset. */
|
|
13
|
+
size?: ButtonSize;
|
|
8
14
|
/** Additional container styles (works on web and native). */
|
|
9
15
|
style?: StyleProp<ViewStyle>;
|
|
10
16
|
/** Additional label styles (works on web and native). */
|
|
@@ -20,6 +26,6 @@ interface ButtonProps {
|
|
|
20
26
|
*/
|
|
21
27
|
textClassName?: string;
|
|
22
28
|
}
|
|
23
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
29
|
+
declare function Button({ title, onPress, disabled, variant, size, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
24
30
|
|
|
25
|
-
export { Button, type ButtonProps };
|
|
31
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
+
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
|
5
|
+
type ButtonSize = "small" | "medium" | "large";
|
|
4
6
|
interface ButtonProps {
|
|
5
7
|
title: string;
|
|
6
8
|
onPress: (event: GestureResponderEvent) => void;
|
|
7
9
|
disabled?: boolean;
|
|
10
|
+
/** Visual style preset. */
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
/** Size preset. */
|
|
13
|
+
size?: ButtonSize;
|
|
8
14
|
/** Additional container styles (works on web and native). */
|
|
9
15
|
style?: StyleProp<ViewStyle>;
|
|
10
16
|
/** Additional label styles (works on web and native). */
|
|
@@ -20,6 +26,6 @@ interface ButtonProps {
|
|
|
20
26
|
*/
|
|
21
27
|
textClassName?: string;
|
|
22
28
|
}
|
|
23
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
29
|
+
declare function Button({ title, onPress, disabled, variant, size, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
|
|
24
30
|
|
|
25
|
-
export { Button, type ButtonProps };
|
|
31
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,8 @@ function Button({
|
|
|
34
34
|
title,
|
|
35
35
|
onPress,
|
|
36
36
|
disabled = false,
|
|
37
|
+
variant = "primary",
|
|
38
|
+
size = "medium",
|
|
37
39
|
style,
|
|
38
40
|
textStyle,
|
|
39
41
|
className,
|
|
@@ -43,8 +45,20 @@ function Button({
|
|
|
43
45
|
const textRef = useRef(null);
|
|
44
46
|
useApplyWebClassName(containerRef, className);
|
|
45
47
|
useApplyWebClassName(textRef, textClassName);
|
|
46
|
-
const containerStyle = [
|
|
47
|
-
|
|
48
|
+
const containerStyle = [
|
|
49
|
+
styles.base,
|
|
50
|
+
variantStyles[variant],
|
|
51
|
+
sizeStyles[size],
|
|
52
|
+
disabled && disabledVariantStyles[variant],
|
|
53
|
+
style
|
|
54
|
+
];
|
|
55
|
+
const labelStyle = [
|
|
56
|
+
textBaseStyles.base,
|
|
57
|
+
textVariantStyles[variant],
|
|
58
|
+
textSizeStyles[size],
|
|
59
|
+
disabled && textDisabledStyles[variant],
|
|
60
|
+
textStyle
|
|
61
|
+
];
|
|
48
62
|
return /* @__PURE__ */ jsx(
|
|
49
63
|
TouchableOpacity,
|
|
50
64
|
{
|
|
@@ -60,27 +74,105 @@ function Button({
|
|
|
60
74
|
);
|
|
61
75
|
}
|
|
62
76
|
var styles = StyleSheet.create({
|
|
63
|
-
|
|
64
|
-
backgroundColor: "#2563eb",
|
|
65
|
-
paddingVertical: 12,
|
|
66
|
-
paddingHorizontal: 24,
|
|
67
|
-
borderRadius: 8,
|
|
77
|
+
base: {
|
|
68
78
|
alignItems: "center",
|
|
69
79
|
justifyContent: "center",
|
|
70
|
-
|
|
80
|
+
borderRadius: 8,
|
|
71
81
|
borderWidth: 0
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
var variantStyles = StyleSheet.create({
|
|
85
|
+
primary: {
|
|
86
|
+
backgroundColor: "#2563eb"
|
|
87
|
+
},
|
|
88
|
+
secondary: {
|
|
89
|
+
backgroundColor: "#4b5563"
|
|
90
|
+
},
|
|
91
|
+
outline: {
|
|
92
|
+
backgroundColor: "transparent",
|
|
93
|
+
borderWidth: 1,
|
|
94
|
+
borderColor: "#2563eb"
|
|
95
|
+
},
|
|
96
|
+
ghost: {
|
|
97
|
+
backgroundColor: "transparent"
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
var sizeStyles = StyleSheet.create({
|
|
101
|
+
small: {
|
|
102
|
+
paddingVertical: 8,
|
|
103
|
+
paddingHorizontal: 16,
|
|
104
|
+
minWidth: 96
|
|
105
|
+
},
|
|
106
|
+
medium: {
|
|
107
|
+
paddingVertical: 12,
|
|
108
|
+
paddingHorizontal: 24,
|
|
109
|
+
minWidth: 120
|
|
72
110
|
},
|
|
73
|
-
|
|
111
|
+
large: {
|
|
112
|
+
paddingVertical: 16,
|
|
113
|
+
paddingHorizontal: 32,
|
|
114
|
+
minWidth: 160
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
var disabledVariantStyles = StyleSheet.create({
|
|
118
|
+
primary: {
|
|
74
119
|
backgroundColor: "#93c5fd",
|
|
75
120
|
opacity: 0.7
|
|
76
121
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
secondary: {
|
|
123
|
+
backgroundColor: "#9ca3af",
|
|
124
|
+
opacity: 0.7
|
|
125
|
+
},
|
|
126
|
+
outline: {
|
|
127
|
+
borderColor: "#93c5fd",
|
|
128
|
+
opacity: 0.7
|
|
129
|
+
},
|
|
130
|
+
ghost: {
|
|
131
|
+
opacity: 0.5
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
var textBaseStyles = StyleSheet.create({
|
|
135
|
+
base: {
|
|
80
136
|
fontWeight: "600"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
var textVariantStyles = StyleSheet.create({
|
|
140
|
+
primary: {
|
|
141
|
+
color: "#ffffff"
|
|
142
|
+
},
|
|
143
|
+
secondary: {
|
|
144
|
+
color: "#ffffff"
|
|
81
145
|
},
|
|
82
|
-
|
|
146
|
+
outline: {
|
|
147
|
+
color: "#2563eb"
|
|
148
|
+
},
|
|
149
|
+
ghost: {
|
|
150
|
+
color: "#2563eb"
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
var textSizeStyles = StyleSheet.create({
|
|
154
|
+
small: {
|
|
155
|
+
fontSize: 14
|
|
156
|
+
},
|
|
157
|
+
medium: {
|
|
158
|
+
fontSize: 16
|
|
159
|
+
},
|
|
160
|
+
large: {
|
|
161
|
+
fontSize: 18
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
var textDisabledStyles = StyleSheet.create({
|
|
165
|
+
primary: {
|
|
83
166
|
color: "#e5e7eb"
|
|
167
|
+
},
|
|
168
|
+
secondary: {
|
|
169
|
+
color: "#f3f4f6"
|
|
170
|
+
},
|
|
171
|
+
outline: {
|
|
172
|
+
color: "#93c5fd"
|
|
173
|
+
},
|
|
174
|
+
ghost: {
|
|
175
|
+
color: "#93c5fd"
|
|
84
176
|
}
|
|
85
177
|
});
|
|
86
178
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/useApplyWebClassName.ts","../src/components/Button.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { useLayoutEffect } from \"react\";\nimport { Platform } from \"react-native\";\n\ninterface ClassListElement {\n classList: {\n add: (...classes: string[]) => void;\n remove: (...classes: string[]) => void;\n };\n}\n\nfunction hasClassList(node: unknown): node is ClassListElement {\n return (\n typeof node === \"object\" &&\n node !== null &&\n \"classList\" in node &&\n typeof (node as ClassListElement).classList?.add === \"function\"\n );\n}\n\nfunction resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {\n const node = ref.current;\n if (!node) return null;\n\n if (hasClassList(node)) return node;\n\n const host = node as {\n _touchableNode?: unknown;\n getScrollableNode?: () => unknown;\n };\n\n if (hasClassList(host._touchableNode)) return host._touchableNode;\n\n if (typeof host.getScrollableNode === \"function\") {\n const scrollNode = host.getScrollableNode();\n if (hasClassList(scrollNode)) return scrollNode;\n }\n\n return null;\n}\n\n/**\n * Applies CSS class names to the underlying DOM node on web.\n * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.\n */\nexport function useApplyWebClassName(\n ref: React.RefObject<unknown>,\n className?: string,\n): void {\n useLayoutEffect(() => {\n if (Platform.OS !== \"web\" || !className?.trim()) return;\n\n const element = resolveWebElement(ref);\n if (!element) return;\n\n const classes = className.trim().split(/\\s+/);\n element.classList.add(...classes);\n\n return () => {\n element.classList.remove(...classes);\n };\n }, [ref, className]);\n}\n","import { useRef, type ComponentRef } from \"react\";\nimport {\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { useApplyWebClassName } from \"../utils/useApplyWebClassName\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\";\nexport type ButtonSize = \"small\" | \"medium\" | \"large\";\n\nexport interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Visual style preset. */\n variant?: ButtonVariant;\n /** Size preset. */\n size?: ButtonSize;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n variant = \"primary\",\n size = \"medium\",\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [\n styles.base,\n variantStyles[variant],\n sizeStyles[size],\n disabled && disabledVariantStyles[variant],\n style,\n ];\n\n const labelStyle = [\n textBaseStyles.base,\n textVariantStyles[variant],\n textSizeStyles[size],\n disabled && textDisabledStyles[variant],\n textStyle,\n ];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n base: {\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: 8,\n borderWidth: 0,\n },\n});\n\nconst variantStyles = StyleSheet.create({\n primary: {\n backgroundColor: \"#2563eb\",\n },\n secondary: {\n backgroundColor: \"#4b5563\",\n },\n outline: {\n backgroundColor: \"transparent\",\n borderWidth: 1,\n borderColor: \"#2563eb\",\n },\n ghost: {\n backgroundColor: \"transparent\",\n },\n});\n\nconst sizeStyles = StyleSheet.create({\n small: {\n paddingVertical: 8,\n paddingHorizontal: 16,\n minWidth: 96,\n },\n medium: {\n paddingVertical: 12,\n paddingHorizontal: 24,\n minWidth: 120,\n },\n large: {\n paddingVertical: 16,\n paddingHorizontal: 32,\n minWidth: 160,\n },\n});\n\nconst disabledVariantStyles = StyleSheet.create({\n primary: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n secondary: {\n backgroundColor: \"#9ca3af\",\n opacity: 0.7,\n },\n outline: {\n borderColor: \"#93c5fd\",\n opacity: 0.7,\n },\n ghost: {\n opacity: 0.5,\n },\n});\n\nconst textBaseStyles = StyleSheet.create({\n base: {\n fontWeight: \"600\",\n },\n});\n\nconst textVariantStyles = StyleSheet.create({\n primary: {\n color: \"#ffffff\",\n },\n secondary: {\n color: \"#ffffff\",\n },\n outline: {\n color: \"#2563eb\",\n },\n ghost: {\n color: \"#2563eb\",\n },\n});\n\nconst textSizeStyles = StyleSheet.create({\n small: {\n fontSize: 14,\n },\n medium: {\n fontSize: 16,\n },\n large: {\n fontSize: 18,\n },\n});\n\nconst textDisabledStyles = StyleSheet.create({\n primary: {\n color: \"#e5e7eb\",\n },\n secondary: {\n color: \"#f3f4f6\",\n },\n outline: {\n color: \"#93c5fd\",\n },\n ghost: {\n color: \"#93c5fd\",\n },\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { fn } from "@storybook/test";
|
|
3
|
+
import { View, StyleSheet } from "react-native";
|
|
3
4
|
import { Button } from "./Button";
|
|
4
5
|
|
|
5
6
|
const meta = {
|
|
@@ -9,9 +10,19 @@ const meta = {
|
|
|
9
10
|
title: "Press me",
|
|
10
11
|
onPress: fn(),
|
|
11
12
|
disabled: false,
|
|
13
|
+
variant: "primary",
|
|
14
|
+
size: "medium",
|
|
12
15
|
},
|
|
13
16
|
argTypes: {
|
|
14
17
|
onPress: { action: "pressed" },
|
|
18
|
+
variant: {
|
|
19
|
+
control: "select",
|
|
20
|
+
options: ["primary", "secondary", "outline", "ghost"],
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
control: "select",
|
|
24
|
+
options: ["small", "medium", "large"],
|
|
25
|
+
},
|
|
15
26
|
},
|
|
16
27
|
} satisfies Meta<typeof Button>;
|
|
17
28
|
|
|
@@ -20,47 +31,91 @@ type Story = StoryObj<typeof meta>;
|
|
|
20
31
|
|
|
21
32
|
export const Default: Story = {};
|
|
22
33
|
|
|
34
|
+
export const Primary: Story = {
|
|
35
|
+
args: { variant: "primary", title: "Primary" },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Secondary: Story = {
|
|
39
|
+
args: { variant: "secondary", title: "Secondary" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Outline: Story = {
|
|
43
|
+
args: { variant: "outline", title: "Outline" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Ghost: Story = {
|
|
47
|
+
args: { variant: "ghost", title: "Ghost" },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Small: Story = {
|
|
51
|
+
args: { size: "small", title: "Small" },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Large: Story = {
|
|
55
|
+
args: { size: "large", title: "Large" },
|
|
56
|
+
};
|
|
57
|
+
|
|
23
58
|
export const Disabled: Story = {
|
|
24
|
-
args: {
|
|
25
|
-
disabled: true,
|
|
26
|
-
title: "Disabled",
|
|
27
|
-
},
|
|
59
|
+
args: { disabled: true, title: "Disabled" },
|
|
28
60
|
};
|
|
29
61
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
export const AllVariants: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<View style={storyStyles.column}>
|
|
65
|
+
<Button title="Primary" variant="primary" onPress={fn()} />
|
|
66
|
+
<Button title="Secondary" variant="secondary" onPress={fn()} />
|
|
67
|
+
<Button title="Outline" variant="outline" onPress={fn()} />
|
|
68
|
+
<Button title="Ghost" variant="ghost" onPress={fn()} />
|
|
69
|
+
</View>
|
|
70
|
+
),
|
|
34
71
|
};
|
|
35
72
|
|
|
36
|
-
export const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
73
|
+
export const AllSizes: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<View style={storyStyles.column}>
|
|
76
|
+
<Button title="Small" size="small" onPress={fn()} />
|
|
77
|
+
<Button title="Medium" size="medium" onPress={fn()} />
|
|
78
|
+
<Button title="Large" size="large" onPress={fn()} />
|
|
79
|
+
</View>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const VariantMatrix: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<View style={storyStyles.grid}>
|
|
86
|
+
{(["primary", "secondary", "outline", "ghost"] as const).map((variant) => (
|
|
87
|
+
<View key={variant} style={storyStyles.column}>
|
|
88
|
+
{(["small", "medium", "large"] as const).map((size) => (
|
|
89
|
+
<Button
|
|
90
|
+
key={`${variant}-${size}`}
|
|
91
|
+
title={`${variant} ${size}`}
|
|
92
|
+
variant={variant}
|
|
93
|
+
size={size}
|
|
94
|
+
onPress={fn()}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</View>
|
|
98
|
+
))}
|
|
99
|
+
</View>
|
|
100
|
+
),
|
|
50
101
|
};
|
|
51
102
|
|
|
52
103
|
export const WithClassName: Story = {
|
|
53
104
|
args: {
|
|
54
|
-
title: "Tailwind
|
|
55
|
-
className: "
|
|
56
|
-
textClassName: "
|
|
57
|
-
},
|
|
58
|
-
parameters: {
|
|
59
|
-
docs: {
|
|
60
|
-
description: {
|
|
61
|
-
story:
|
|
62
|
-
"Pass Tailwind utility classes via className. Requires Tailwind in your web app (or NativeWind on native).",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
105
|
+
title: "Custom Tailwind",
|
|
106
|
+
className: "!bg-violet-600 shadow-lg",
|
|
107
|
+
textClassName: "uppercase tracking-wide",
|
|
65
108
|
},
|
|
66
109
|
};
|
|
110
|
+
|
|
111
|
+
const storyStyles = StyleSheet.create({
|
|
112
|
+
column: {
|
|
113
|
+
gap: 12,
|
|
114
|
+
alignItems: "flex-start",
|
|
115
|
+
},
|
|
116
|
+
grid: {
|
|
117
|
+
flexDirection: "row",
|
|
118
|
+
flexWrap: "wrap",
|
|
119
|
+
gap: 24,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -10,10 +10,17 @@ import {
|
|
|
10
10
|
} from "react-native";
|
|
11
11
|
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
12
12
|
|
|
13
|
+
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
|
14
|
+
export type ButtonSize = "small" | "medium" | "large";
|
|
15
|
+
|
|
13
16
|
export interface ButtonProps {
|
|
14
17
|
title: string;
|
|
15
18
|
onPress: (event: GestureResponderEvent) => void;
|
|
16
19
|
disabled?: boolean;
|
|
20
|
+
/** Visual style preset. */
|
|
21
|
+
variant?: ButtonVariant;
|
|
22
|
+
/** Size preset. */
|
|
23
|
+
size?: ButtonSize;
|
|
17
24
|
/** Additional container styles (works on web and native). */
|
|
18
25
|
style?: StyleProp<ViewStyle>;
|
|
19
26
|
/** Additional label styles (works on web and native). */
|
|
@@ -34,6 +41,8 @@ export function Button({
|
|
|
34
41
|
title,
|
|
35
42
|
onPress,
|
|
36
43
|
disabled = false,
|
|
44
|
+
variant = "primary",
|
|
45
|
+
size = "medium",
|
|
37
46
|
style,
|
|
38
47
|
textStyle,
|
|
39
48
|
className,
|
|
@@ -45,8 +54,21 @@ export function Button({
|
|
|
45
54
|
useApplyWebClassName(containerRef, className);
|
|
46
55
|
useApplyWebClassName(textRef, textClassName);
|
|
47
56
|
|
|
48
|
-
const containerStyle = [
|
|
49
|
-
|
|
57
|
+
const containerStyle = [
|
|
58
|
+
styles.base,
|
|
59
|
+
variantStyles[variant],
|
|
60
|
+
sizeStyles[size],
|
|
61
|
+
disabled && disabledVariantStyles[variant],
|
|
62
|
+
style,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const labelStyle = [
|
|
66
|
+
textBaseStyles.base,
|
|
67
|
+
textVariantStyles[variant],
|
|
68
|
+
textSizeStyles[size],
|
|
69
|
+
disabled && textDisabledStyles[variant],
|
|
70
|
+
textStyle,
|
|
71
|
+
];
|
|
50
72
|
|
|
51
73
|
return (
|
|
52
74
|
<TouchableOpacity
|
|
@@ -66,26 +88,111 @@ export function Button({
|
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
const styles = StyleSheet.create({
|
|
69
|
-
|
|
91
|
+
base: {
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
borderWidth: 0,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const variantStyles = StyleSheet.create({
|
|
100
|
+
primary: {
|
|
70
101
|
backgroundColor: "#2563eb",
|
|
102
|
+
},
|
|
103
|
+
secondary: {
|
|
104
|
+
backgroundColor: "#4b5563",
|
|
105
|
+
},
|
|
106
|
+
outline: {
|
|
107
|
+
backgroundColor: "transparent",
|
|
108
|
+
borderWidth: 1,
|
|
109
|
+
borderColor: "#2563eb",
|
|
110
|
+
},
|
|
111
|
+
ghost: {
|
|
112
|
+
backgroundColor: "transparent",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const sizeStyles = StyleSheet.create({
|
|
117
|
+
small: {
|
|
118
|
+
paddingVertical: 8,
|
|
119
|
+
paddingHorizontal: 16,
|
|
120
|
+
minWidth: 96,
|
|
121
|
+
},
|
|
122
|
+
medium: {
|
|
71
123
|
paddingVertical: 12,
|
|
72
124
|
paddingHorizontal: 24,
|
|
73
|
-
borderRadius: 8,
|
|
74
|
-
alignItems: "center",
|
|
75
|
-
justifyContent: "center",
|
|
76
125
|
minWidth: 120,
|
|
77
|
-
borderWidth: 0,
|
|
78
126
|
},
|
|
79
|
-
|
|
127
|
+
large: {
|
|
128
|
+
paddingVertical: 16,
|
|
129
|
+
paddingHorizontal: 32,
|
|
130
|
+
minWidth: 160,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const disabledVariantStyles = StyleSheet.create({
|
|
135
|
+
primary: {
|
|
80
136
|
backgroundColor: "#93c5fd",
|
|
81
137
|
opacity: 0.7,
|
|
82
138
|
},
|
|
83
|
-
|
|
139
|
+
secondary: {
|
|
140
|
+
backgroundColor: "#9ca3af",
|
|
141
|
+
opacity: 0.7,
|
|
142
|
+
},
|
|
143
|
+
outline: {
|
|
144
|
+
borderColor: "#93c5fd",
|
|
145
|
+
opacity: 0.7,
|
|
146
|
+
},
|
|
147
|
+
ghost: {
|
|
148
|
+
opacity: 0.5,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const textBaseStyles = StyleSheet.create({
|
|
153
|
+
base: {
|
|
154
|
+
fontWeight: "600",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const textVariantStyles = StyleSheet.create({
|
|
159
|
+
primary: {
|
|
84
160
|
color: "#ffffff",
|
|
161
|
+
},
|
|
162
|
+
secondary: {
|
|
163
|
+
color: "#ffffff",
|
|
164
|
+
},
|
|
165
|
+
outline: {
|
|
166
|
+
color: "#2563eb",
|
|
167
|
+
},
|
|
168
|
+
ghost: {
|
|
169
|
+
color: "#2563eb",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const textSizeStyles = StyleSheet.create({
|
|
174
|
+
small: {
|
|
175
|
+
fontSize: 14,
|
|
176
|
+
},
|
|
177
|
+
medium: {
|
|
85
178
|
fontSize: 16,
|
|
86
|
-
fontWeight: "600",
|
|
87
179
|
},
|
|
88
|
-
|
|
180
|
+
large: {
|
|
181
|
+
fontSize: 18,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const textDisabledStyles = StyleSheet.create({
|
|
186
|
+
primary: {
|
|
89
187
|
color: "#e5e7eb",
|
|
90
188
|
},
|
|
189
|
+
secondary: {
|
|
190
|
+
color: "#f3f4f6",
|
|
191
|
+
},
|
|
192
|
+
outline: {
|
|
193
|
+
color: "#93c5fd",
|
|
194
|
+
},
|
|
195
|
+
ghost: {
|
|
196
|
+
color: "#93c5fd",
|
|
197
|
+
},
|
|
91
198
|
});
|