@scripso-homepad/ui 0.3.2 → 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 +31 -12
- package/dist/index.cjs +136 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +138 -70
- 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 +130 -42
- package/src/index.ts +5 -1
- package/src/utils/useApplyWebClassName.ts +62 -0
- package/src/utils/rnStyleToWebStyle.ts +0 -46
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
|
|
|
@@ -115,18 +128,24 @@ export function Example() {
|
|
|
115
128
|
|
|
116
129
|
#### Tailwind classes (React web)
|
|
117
130
|
|
|
118
|
-
|
|
131
|
+
`className` is applied to the same `TouchableOpacity` / `Text` DOM nodes as the default styles — layout and padding stay consistent with native:
|
|
119
132
|
|
|
120
133
|
```tsx
|
|
121
134
|
<Button
|
|
122
135
|
title="Save"
|
|
123
136
|
onPress={handleSave}
|
|
124
|
-
className="
|
|
137
|
+
className="!bg-violet-600 shadow-lg"
|
|
125
138
|
textClassName="text-sm font-bold uppercase"
|
|
126
139
|
/>
|
|
127
140
|
```
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
Use `!` (important) on Tailwind utilities when overriding default colors, e.g. `!bg-red-500`, because react-native-web applies inline styles from `StyleSheet`.
|
|
143
|
+
|
|
144
|
+
Ensure Tailwind scans the package if needed:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
content: ["./src/**/*.{js,ts,jsx,tsx}", "./node_modules/@scripso-homepad/ui/**/*.{js,ts,jsx,tsx}"],
|
|
148
|
+
```
|
|
130
149
|
|
|
131
150
|
## Development
|
|
132
151
|
|
package/dist/index.cjs
CHANGED
|
@@ -5,108 +5,176 @@ var reactNative = require('react-native');
|
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
7
7
|
// src/components/Button.tsx
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
css.paddingLeft = value;
|
|
21
|
-
css.paddingRight = value;
|
|
22
|
-
break;
|
|
23
|
-
case "marginVertical":
|
|
24
|
-
css.marginTop = value;
|
|
25
|
-
css.marginBottom = value;
|
|
26
|
-
break;
|
|
27
|
-
case "marginHorizontal":
|
|
28
|
-
css.marginLeft = value;
|
|
29
|
-
css.marginRight = value;
|
|
30
|
-
break;
|
|
31
|
-
default:
|
|
32
|
-
css[key] = value;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (css.alignItems || css.justifyContent || css.flexDirection) {
|
|
36
|
-
css.display = css.display ?? "flex";
|
|
8
|
+
function hasClassList(node) {
|
|
9
|
+
return typeof node === "object" && node !== null && "classList" in node && typeof node.classList?.add === "function";
|
|
10
|
+
}
|
|
11
|
+
function resolveWebElement(ref) {
|
|
12
|
+
const node = ref.current;
|
|
13
|
+
if (!node) return null;
|
|
14
|
+
if (hasClassList(node)) return node;
|
|
15
|
+
const host = node;
|
|
16
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
17
|
+
if (typeof host.getScrollableNode === "function") {
|
|
18
|
+
const scrollNode = host.getScrollableNode();
|
|
19
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
37
20
|
}
|
|
38
|
-
return
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function useApplyWebClassName(ref, className) {
|
|
24
|
+
react.useLayoutEffect(() => {
|
|
25
|
+
if (reactNative.Platform.OS !== "web" || !className?.trim()) return;
|
|
26
|
+
const element = resolveWebElement(ref);
|
|
27
|
+
if (!element) return;
|
|
28
|
+
const classes = className.trim().split(/\s+/);
|
|
29
|
+
element.classList.add(...classes);
|
|
30
|
+
return () => {
|
|
31
|
+
element.classList.remove(...classes);
|
|
32
|
+
};
|
|
33
|
+
}, [ref, className]);
|
|
39
34
|
}
|
|
40
35
|
function Button({
|
|
41
36
|
title,
|
|
42
37
|
onPress,
|
|
43
38
|
disabled = false,
|
|
39
|
+
variant = "primary",
|
|
40
|
+
size = "medium",
|
|
44
41
|
style,
|
|
45
42
|
textStyle,
|
|
46
43
|
className,
|
|
47
44
|
textClassName
|
|
48
45
|
}) {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
className: textClassName,
|
|
69
|
-
style: rnStyleToWebStyle(labelStyle)
|
|
70
|
-
},
|
|
71
|
-
title
|
|
72
|
-
)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
46
|
+
const containerRef = react.useRef(null);
|
|
47
|
+
const textRef = react.useRef(null);
|
|
48
|
+
useApplyWebClassName(containerRef, className);
|
|
49
|
+
useApplyWebClassName(textRef, textClassName);
|
|
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
|
+
];
|
|
75
64
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
76
65
|
reactNative.TouchableOpacity,
|
|
77
66
|
{
|
|
67
|
+
ref: containerRef,
|
|
78
68
|
style: containerStyle,
|
|
79
69
|
onPress,
|
|
80
70
|
disabled,
|
|
81
71
|
activeOpacity: 0.7,
|
|
82
72
|
accessibilityRole: "button",
|
|
83
73
|
accessibilityState: { disabled },
|
|
84
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: labelStyle, children: title })
|
|
74
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { ref: textRef, style: labelStyle, children: title })
|
|
85
75
|
}
|
|
86
76
|
);
|
|
87
77
|
}
|
|
88
78
|
var styles = reactNative.StyleSheet.create({
|
|
89
|
-
|
|
90
|
-
backgroundColor: "#2563eb",
|
|
91
|
-
paddingVertical: 12,
|
|
92
|
-
paddingHorizontal: 24,
|
|
93
|
-
borderRadius: 8,
|
|
79
|
+
base: {
|
|
94
80
|
alignItems: "center",
|
|
95
81
|
justifyContent: "center",
|
|
96
|
-
|
|
82
|
+
borderRadius: 8,
|
|
97
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
|
|
98
107
|
},
|
|
99
|
-
|
|
108
|
+
medium: {
|
|
109
|
+
paddingVertical: 12,
|
|
110
|
+
paddingHorizontal: 24,
|
|
111
|
+
minWidth: 120
|
|
112
|
+
},
|
|
113
|
+
large: {
|
|
114
|
+
paddingVertical: 16,
|
|
115
|
+
paddingHorizontal: 32,
|
|
116
|
+
minWidth: 160
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
var disabledVariantStyles = reactNative.StyleSheet.create({
|
|
120
|
+
primary: {
|
|
100
121
|
backgroundColor: "#93c5fd",
|
|
101
122
|
opacity: 0.7
|
|
102
123
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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: {
|
|
106
138
|
fontWeight: "600"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
var textVariantStyles = reactNative.StyleSheet.create({
|
|
142
|
+
primary: {
|
|
143
|
+
color: "#ffffff"
|
|
144
|
+
},
|
|
145
|
+
secondary: {
|
|
146
|
+
color: "#ffffff"
|
|
107
147
|
},
|
|
108
|
-
|
|
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: {
|
|
109
168
|
color: "#e5e7eb"
|
|
169
|
+
},
|
|
170
|
+
secondary: {
|
|
171
|
+
color: "#f3f4f6"
|
|
172
|
+
},
|
|
173
|
+
outline: {
|
|
174
|
+
color: "#93c5fd"
|
|
175
|
+
},
|
|
176
|
+
ghost: {
|
|
177
|
+
color: "#93c5fd"
|
|
110
178
|
}
|
|
111
179
|
});
|
|
112
180
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/rnStyleToWebStyle.ts","../src/components/Button.tsx"],"names":["StyleSheet","Platform","createElement","jsx","TouchableOpacity","Text"],"mappings":"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","file":"index.cjs","sourcesContent":["import { StyleSheet, type StyleProp, type TextStyle, type ViewStyle } from \"react-native\";\nimport type { CSSProperties } from \"react\";\n\ntype RNStyle = StyleProp<ViewStyle> | StyleProp<TextStyle>;\n\n/**\n * Converts React Native StyleSheet values to CSS properties for DOM elements.\n * Required when rendering native HTML on web (browsers ignore paddingVertical, etc.).\n */\nexport function rnStyleToWebStyle(style: RNStyle): CSSProperties {\n const flat = StyleSheet.flatten(style);\n if (!flat) return {};\n\n const css: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(flat)) {\n if (value === undefined || value === null) continue;\n\n switch (key) {\n case \"paddingVertical\":\n css.paddingTop = value;\n css.paddingBottom = value;\n break;\n case \"paddingHorizontal\":\n css.paddingLeft = value;\n css.paddingRight = value;\n break;\n case \"marginVertical\":\n css.marginTop = value;\n css.marginBottom = value;\n break;\n case \"marginHorizontal\":\n css.marginLeft = value;\n css.marginRight = value;\n break;\n default:\n css[key] = value;\n }\n }\n\n if (css.alignItems || css.justifyContent || css.flexDirection) {\n css.display = css.display ?? \"flex\";\n }\n\n return css as CSSProperties;\n}\n","import React, { createElement } from \"react\";\nimport {\n Platform,\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { rnStyleToWebStyle } from \"../utils/rnStyleToWebStyle\";\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 only — uses DOM `<button>` for Tailwind).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web only).\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 containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n const useWebDom =\n Platform.OS === \"web\" && (className != null || textClassName != null);\n\n if (useWebDom) {\n return createElement(\n \"button\",\n {\n type: \"button\",\n className,\n style: rnStyleToWebStyle(containerStyle),\n disabled,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n onPress(event as unknown as GestureResponderEvent);\n },\n \"aria-disabled\": disabled,\n },\n createElement(\n \"span\",\n {\n className: textClassName,\n style: rnStyleToWebStyle(labelStyle),\n },\n title,\n ),\n );\n }\n\n return (\n <TouchableOpacity\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text style={labelStyle}>{title}</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,25 +1,31 @@
|
|
|
1
|
-
import
|
|
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). */
|
|
11
17
|
textStyle?: StyleProp<TextStyle>;
|
|
12
18
|
/**
|
|
13
|
-
* CSS class names for the container (web
|
|
19
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
14
20
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
15
21
|
*/
|
|
16
22
|
className?: string;
|
|
17
23
|
/**
|
|
18
|
-
* CSS class names for the label (web
|
|
24
|
+
* CSS class names for the label (web).
|
|
19
25
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
20
26
|
*/
|
|
21
27
|
textClassName?: string;
|
|
22
28
|
}
|
|
23
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps):
|
|
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,25 +1,31 @@
|
|
|
1
|
-
import
|
|
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). */
|
|
11
17
|
textStyle?: StyleProp<TextStyle>;
|
|
12
18
|
/**
|
|
13
|
-
* CSS class names for the container (web
|
|
19
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
14
20
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
15
21
|
*/
|
|
16
22
|
className?: string;
|
|
17
23
|
/**
|
|
18
|
-
* CSS class names for the label (web
|
|
24
|
+
* CSS class names for the label (web).
|
|
19
25
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
20
26
|
*/
|
|
21
27
|
textClassName?: string;
|
|
22
28
|
}
|
|
23
|
-
declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps):
|
|
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
|
@@ -1,110 +1,178 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StyleSheet,
|
|
1
|
+
import { useRef, useLayoutEffect } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, Text, Platform } from 'react-native';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
// src/components/Button.tsx
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
css.paddingLeft = value;
|
|
19
|
-
css.paddingRight = value;
|
|
20
|
-
break;
|
|
21
|
-
case "marginVertical":
|
|
22
|
-
css.marginTop = value;
|
|
23
|
-
css.marginBottom = value;
|
|
24
|
-
break;
|
|
25
|
-
case "marginHorizontal":
|
|
26
|
-
css.marginLeft = value;
|
|
27
|
-
css.marginRight = value;
|
|
28
|
-
break;
|
|
29
|
-
default:
|
|
30
|
-
css[key] = value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (css.alignItems || css.justifyContent || css.flexDirection) {
|
|
34
|
-
css.display = css.display ?? "flex";
|
|
6
|
+
function hasClassList(node) {
|
|
7
|
+
return typeof node === "object" && node !== null && "classList" in node && typeof node.classList?.add === "function";
|
|
8
|
+
}
|
|
9
|
+
function resolveWebElement(ref) {
|
|
10
|
+
const node = ref.current;
|
|
11
|
+
if (!node) return null;
|
|
12
|
+
if (hasClassList(node)) return node;
|
|
13
|
+
const host = node;
|
|
14
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
15
|
+
if (typeof host.getScrollableNode === "function") {
|
|
16
|
+
const scrollNode = host.getScrollableNode();
|
|
17
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
35
18
|
}
|
|
36
|
-
return
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function useApplyWebClassName(ref, className) {
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
if (Platform.OS !== "web" || !className?.trim()) return;
|
|
24
|
+
const element = resolveWebElement(ref);
|
|
25
|
+
if (!element) return;
|
|
26
|
+
const classes = className.trim().split(/\s+/);
|
|
27
|
+
element.classList.add(...classes);
|
|
28
|
+
return () => {
|
|
29
|
+
element.classList.remove(...classes);
|
|
30
|
+
};
|
|
31
|
+
}, [ref, className]);
|
|
37
32
|
}
|
|
38
33
|
function Button({
|
|
39
34
|
title,
|
|
40
35
|
onPress,
|
|
41
36
|
disabled = false,
|
|
37
|
+
variant = "primary",
|
|
38
|
+
size = "medium",
|
|
42
39
|
style,
|
|
43
40
|
textStyle,
|
|
44
41
|
className,
|
|
45
42
|
textClassName
|
|
46
43
|
}) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
className: textClassName,
|
|
67
|
-
style: rnStyleToWebStyle(labelStyle)
|
|
68
|
-
},
|
|
69
|
-
title
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
}
|
|
44
|
+
const containerRef = useRef(null);
|
|
45
|
+
const textRef = useRef(null);
|
|
46
|
+
useApplyWebClassName(containerRef, className);
|
|
47
|
+
useApplyWebClassName(textRef, textClassName);
|
|
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
|
+
];
|
|
73
62
|
return /* @__PURE__ */ jsx(
|
|
74
63
|
TouchableOpacity,
|
|
75
64
|
{
|
|
65
|
+
ref: containerRef,
|
|
76
66
|
style: containerStyle,
|
|
77
67
|
onPress,
|
|
78
68
|
disabled,
|
|
79
69
|
activeOpacity: 0.7,
|
|
80
70
|
accessibilityRole: "button",
|
|
81
71
|
accessibilityState: { disabled },
|
|
82
|
-
children: /* @__PURE__ */ jsx(Text, { style: labelStyle, children: title })
|
|
72
|
+
children: /* @__PURE__ */ jsx(Text, { ref: textRef, style: labelStyle, children: title })
|
|
83
73
|
}
|
|
84
74
|
);
|
|
85
75
|
}
|
|
86
76
|
var styles = StyleSheet.create({
|
|
87
|
-
|
|
88
|
-
backgroundColor: "#2563eb",
|
|
89
|
-
paddingVertical: 12,
|
|
90
|
-
paddingHorizontal: 24,
|
|
91
|
-
borderRadius: 8,
|
|
77
|
+
base: {
|
|
92
78
|
alignItems: "center",
|
|
93
79
|
justifyContent: "center",
|
|
94
|
-
|
|
80
|
+
borderRadius: 8,
|
|
95
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
|
|
96
105
|
},
|
|
97
|
-
|
|
106
|
+
medium: {
|
|
107
|
+
paddingVertical: 12,
|
|
108
|
+
paddingHorizontal: 24,
|
|
109
|
+
minWidth: 120
|
|
110
|
+
},
|
|
111
|
+
large: {
|
|
112
|
+
paddingVertical: 16,
|
|
113
|
+
paddingHorizontal: 32,
|
|
114
|
+
minWidth: 160
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
var disabledVariantStyles = StyleSheet.create({
|
|
118
|
+
primary: {
|
|
98
119
|
backgroundColor: "#93c5fd",
|
|
99
120
|
opacity: 0.7
|
|
100
121
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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: {
|
|
104
136
|
fontWeight: "600"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
var textVariantStyles = StyleSheet.create({
|
|
140
|
+
primary: {
|
|
141
|
+
color: "#ffffff"
|
|
142
|
+
},
|
|
143
|
+
secondary: {
|
|
144
|
+
color: "#ffffff"
|
|
105
145
|
},
|
|
106
|
-
|
|
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: {
|
|
107
166
|
color: "#e5e7eb"
|
|
167
|
+
},
|
|
168
|
+
secondary: {
|
|
169
|
+
color: "#f3f4f6"
|
|
170
|
+
},
|
|
171
|
+
outline: {
|
|
172
|
+
color: "#93c5fd"
|
|
173
|
+
},
|
|
174
|
+
ghost: {
|
|
175
|
+
color: "#93c5fd"
|
|
108
176
|
}
|
|
109
177
|
});
|
|
110
178
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/rnStyleToWebStyle.ts","../src/components/Button.tsx"],"names":["StyleSheet"],"mappings":";;;;;AASO,SAAS,kBAAkB,KAAA,EAA+B;AAC/D,EAAA,MAAM,IAAA,GAAO,UAAA,CAAW,OAAA,CAAQ,KAAK,CAAA;AACrC,EAAA,IAAI,CAAC,IAAA,EAAM,OAAO,EAAC;AAEnB,EAAA,MAAM,MAA+B,EAAC;AAEtC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AAC/C,IAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,KAAU,IAAA,EAAM;AAE3C,IAAA,QAAQ,GAAA;AAAK,MACX,KAAK,iBAAA;AACH,QAAA,GAAA,CAAI,UAAA,GAAa,KAAA;AACjB,QAAA,GAAA,CAAI,aAAA,GAAgB,KAAA;AACpB,QAAA;AAAA,MACF,KAAK,mBAAA;AACH,QAAA,GAAA,CAAI,WAAA,GAAc,KAAA;AAClB,QAAA,GAAA,CAAI,YAAA,GAAe,KAAA;AACnB,QAAA;AAAA,MACF,KAAK,gBAAA;AACH,QAAA,GAAA,CAAI,SAAA,GAAY,KAAA;AAChB,QAAA,GAAA,CAAI,YAAA,GAAe,KAAA;AACnB,QAAA;AAAA,MACF,KAAK,kBAAA;AACH,QAAA,GAAA,CAAI,UAAA,GAAa,KAAA;AACjB,QAAA,GAAA,CAAI,WAAA,GAAc,KAAA;AAClB,QAAA;AAAA,MACF;AACE,QAAA,GAAA,CAAI,GAAG,CAAA,GAAI,KAAA;AAAA;AACf,EACF;AAEA,EAAA,IAAI,GAAA,CAAI,UAAA,IAAc,GAAA,CAAI,cAAA,IAAkB,IAAI,aAAA,EAAe;AAC7D,IAAA,GAAA,CAAI,OAAA,GAAU,IAAI,OAAA,IAAW,MAAA;AAAA,EAC/B;AAEA,EAAA,OAAO,GAAA;AACT;ACZO,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,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,MAAM,YACJ,QAAA,CAAS,EAAA,KAAO,KAAA,KAAU,SAAA,IAAa,QAAQ,aAAA,IAAiB,IAAA,CAAA;AAElE,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAO,aAAA;AAAA,MACL,QAAA;AAAA,MACA;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,SAAA;AAAA,QACA,KAAA,EAAO,kBAAkB,cAAc,CAAA;AAAA,QACvC,QAAA;AAAA,QACA,OAAA,EAAS,CAAC,KAAA,KAA+C;AACvD,UAAA,OAAA,CAAQ,KAAyC,CAAA;AAAA,QACnD,CAAA;AAAA,QACA,eAAA,EAAiB;AAAA,OACnB;AAAA,MACA,aAAA;AAAA,QACE,MAAA;AAAA,QACA;AAAA,UACE,SAAA,EAAW,aAAA;AAAA,UACX,KAAA,EAAO,kBAAkB,UAAU;AAAA,SACrC;AAAA,QACA;AAAA;AACF,KACF;AAAA,EACF;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,gBAAA;AAAA,IAAA;AAAA,MACC,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,QAAA,kBAAA,GAAA,CAAC,IAAA,EAAA,EAAK,KAAA,EAAO,UAAA,EAAa,QAAA,EAAA,KAAA,EAAM;AAAA;AAAA,GAClC;AAEJ;AAEA,IAAM,MAAA,GAASA,WAAW,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.js","sourcesContent":["import { StyleSheet, type StyleProp, type TextStyle, type ViewStyle } from \"react-native\";\nimport type { CSSProperties } from \"react\";\n\ntype RNStyle = StyleProp<ViewStyle> | StyleProp<TextStyle>;\n\n/**\n * Converts React Native StyleSheet values to CSS properties for DOM elements.\n * Required when rendering native HTML on web (browsers ignore paddingVertical, etc.).\n */\nexport function rnStyleToWebStyle(style: RNStyle): CSSProperties {\n const flat = StyleSheet.flatten(style);\n if (!flat) return {};\n\n const css: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(flat)) {\n if (value === undefined || value === null) continue;\n\n switch (key) {\n case \"paddingVertical\":\n css.paddingTop = value;\n css.paddingBottom = value;\n break;\n case \"paddingHorizontal\":\n css.paddingLeft = value;\n css.paddingRight = value;\n break;\n case \"marginVertical\":\n css.marginTop = value;\n css.marginBottom = value;\n break;\n case \"marginHorizontal\":\n css.marginLeft = value;\n css.marginRight = value;\n break;\n default:\n css[key] = value;\n }\n }\n\n if (css.alignItems || css.justifyContent || css.flexDirection) {\n css.display = css.display ?? \"flex\";\n }\n\n return css as CSSProperties;\n}\n","import React, { createElement } from \"react\";\nimport {\n Platform,\n StyleSheet,\n Text,\n TouchableOpacity,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from \"react-native\";\nimport { rnStyleToWebStyle } from \"../utils/rnStyleToWebStyle\";\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 only — uses DOM `<button>` for Tailwind).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web only).\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 containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n const useWebDom =\n Platform.OS === \"web\" && (className != null || textClassName != null);\n\n if (useWebDom) {\n return createElement(\n \"button\",\n {\n type: \"button\",\n className,\n style: rnStyleToWebStyle(containerStyle),\n disabled,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n onPress(event as unknown as GestureResponderEvent);\n },\n \"aria-disabled\": disabled,\n },\n createElement(\n \"span\",\n {\n className: textClassName,\n style: rnStyleToWebStyle(labelStyle),\n },\n title,\n ),\n );\n }\n\n return (\n <TouchableOpacity\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text style={labelStyle}>{title}</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
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useRef, type ComponentRef } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Platform,
|
|
4
3
|
StyleSheet,
|
|
5
4
|
Text,
|
|
6
5
|
TouchableOpacity,
|
|
@@ -9,23 +8,30 @@ import {
|
|
|
9
8
|
type TextStyle,
|
|
10
9
|
type ViewStyle,
|
|
11
10
|
} from "react-native";
|
|
12
|
-
import {
|
|
11
|
+
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
12
|
+
|
|
13
|
+
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
|
14
|
+
export type ButtonSize = "small" | "medium" | "large";
|
|
13
15
|
|
|
14
16
|
export interface ButtonProps {
|
|
15
17
|
title: string;
|
|
16
18
|
onPress: (event: GestureResponderEvent) => void;
|
|
17
19
|
disabled?: boolean;
|
|
20
|
+
/** Visual style preset. */
|
|
21
|
+
variant?: ButtonVariant;
|
|
22
|
+
/** Size preset. */
|
|
23
|
+
size?: ButtonSize;
|
|
18
24
|
/** Additional container styles (works on web and native). */
|
|
19
25
|
style?: StyleProp<ViewStyle>;
|
|
20
26
|
/** Additional label styles (works on web and native). */
|
|
21
27
|
textStyle?: StyleProp<TextStyle>;
|
|
22
28
|
/**
|
|
23
|
-
* CSS class names for the container (web
|
|
29
|
+
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
24
30
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
25
31
|
*/
|
|
26
32
|
className?: string;
|
|
27
33
|
/**
|
|
28
|
-
* CSS class names for the label (web
|
|
34
|
+
* CSS class names for the label (web).
|
|
29
35
|
* On native: ignored unless using NativeWind with cssInterop.
|
|
30
36
|
*/
|
|
31
37
|
textClassName?: string;
|
|
@@ -35,43 +41,38 @@ export function Button({
|
|
|
35
41
|
title,
|
|
36
42
|
onPress,
|
|
37
43
|
disabled = false,
|
|
44
|
+
variant = "primary",
|
|
45
|
+
size = "medium",
|
|
38
46
|
style,
|
|
39
47
|
textStyle,
|
|
40
48
|
className,
|
|
41
49
|
textClassName,
|
|
42
50
|
}: ButtonProps) {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
51
|
+
const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
|
|
52
|
+
const textRef = useRef<ComponentRef<typeof Text>>(null);
|
|
53
|
+
|
|
54
|
+
useApplyWebClassName(containerRef, className);
|
|
55
|
+
useApplyWebClassName(textRef, textClassName);
|
|
45
56
|
|
|
46
|
-
const
|
|
47
|
-
|
|
57
|
+
const containerStyle = [
|
|
58
|
+
styles.base,
|
|
59
|
+
variantStyles[variant],
|
|
60
|
+
sizeStyles[size],
|
|
61
|
+
disabled && disabledVariantStyles[variant],
|
|
62
|
+
style,
|
|
63
|
+
];
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
disabled,
|
|
57
|
-
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
58
|
-
onPress(event as unknown as GestureResponderEvent);
|
|
59
|
-
},
|
|
60
|
-
"aria-disabled": disabled,
|
|
61
|
-
},
|
|
62
|
-
createElement(
|
|
63
|
-
"span",
|
|
64
|
-
{
|
|
65
|
-
className: textClassName,
|
|
66
|
-
style: rnStyleToWebStyle(labelStyle),
|
|
67
|
-
},
|
|
68
|
-
title,
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
65
|
+
const labelStyle = [
|
|
66
|
+
textBaseStyles.base,
|
|
67
|
+
textVariantStyles[variant],
|
|
68
|
+
textSizeStyles[size],
|
|
69
|
+
disabled && textDisabledStyles[variant],
|
|
70
|
+
textStyle,
|
|
71
|
+
];
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
74
|
<TouchableOpacity
|
|
75
|
+
ref={containerRef}
|
|
75
76
|
style={containerStyle}
|
|
76
77
|
onPress={onPress}
|
|
77
78
|
disabled={disabled}
|
|
@@ -79,32 +80,119 @@ export function Button({
|
|
|
79
80
|
accessibilityRole="button"
|
|
80
81
|
accessibilityState={{ disabled }}
|
|
81
82
|
>
|
|
82
|
-
<Text style={labelStyle}>
|
|
83
|
+
<Text ref={textRef} style={labelStyle}>
|
|
84
|
+
{title}
|
|
85
|
+
</Text>
|
|
83
86
|
</TouchableOpacity>
|
|
84
87
|
);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const styles = StyleSheet.create({
|
|
88
|
-
|
|
91
|
+
base: {
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
borderWidth: 0,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const variantStyles = StyleSheet.create({
|
|
100
|
+
primary: {
|
|
89
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: {
|
|
90
123
|
paddingVertical: 12,
|
|
91
124
|
paddingHorizontal: 24,
|
|
92
|
-
borderRadius: 8,
|
|
93
|
-
alignItems: "center",
|
|
94
|
-
justifyContent: "center",
|
|
95
125
|
minWidth: 120,
|
|
96
|
-
borderWidth: 0,
|
|
97
126
|
},
|
|
98
|
-
|
|
127
|
+
large: {
|
|
128
|
+
paddingVertical: 16,
|
|
129
|
+
paddingHorizontal: 32,
|
|
130
|
+
minWidth: 160,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const disabledVariantStyles = StyleSheet.create({
|
|
135
|
+
primary: {
|
|
99
136
|
backgroundColor: "#93c5fd",
|
|
100
137
|
opacity: 0.7,
|
|
101
138
|
},
|
|
102
|
-
|
|
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: {
|
|
103
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: {
|
|
104
178
|
fontSize: 16,
|
|
105
|
-
fontWeight: "600",
|
|
106
179
|
},
|
|
107
|
-
|
|
180
|
+
large: {
|
|
181
|
+
fontSize: 18,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const textDisabledStyles = StyleSheet.create({
|
|
186
|
+
primary: {
|
|
108
187
|
color: "#e5e7eb",
|
|
109
188
|
},
|
|
189
|
+
secondary: {
|
|
190
|
+
color: "#f3f4f6",
|
|
191
|
+
},
|
|
192
|
+
outline: {
|
|
193
|
+
color: "#93c5fd",
|
|
194
|
+
},
|
|
195
|
+
ghost: {
|
|
196
|
+
color: "#93c5fd",
|
|
197
|
+
},
|
|
110
198
|
});
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
interface ClassListElement {
|
|
5
|
+
classList: {
|
|
6
|
+
add: (...classes: string[]) => void;
|
|
7
|
+
remove: (...classes: string[]) => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hasClassList(node: unknown): node is ClassListElement {
|
|
12
|
+
return (
|
|
13
|
+
typeof node === "object" &&
|
|
14
|
+
node !== null &&
|
|
15
|
+
"classList" in node &&
|
|
16
|
+
typeof (node as ClassListElement).classList?.add === "function"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {
|
|
21
|
+
const node = ref.current;
|
|
22
|
+
if (!node) return null;
|
|
23
|
+
|
|
24
|
+
if (hasClassList(node)) return node;
|
|
25
|
+
|
|
26
|
+
const host = node as {
|
|
27
|
+
_touchableNode?: unknown;
|
|
28
|
+
getScrollableNode?: () => unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (hasClassList(host._touchableNode)) return host._touchableNode;
|
|
32
|
+
|
|
33
|
+
if (typeof host.getScrollableNode === "function") {
|
|
34
|
+
const scrollNode = host.getScrollableNode();
|
|
35
|
+
if (hasClassList(scrollNode)) return scrollNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Applies CSS class names to the underlying DOM node on web.
|
|
43
|
+
* Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.
|
|
44
|
+
*/
|
|
45
|
+
export function useApplyWebClassName(
|
|
46
|
+
ref: React.RefObject<unknown>,
|
|
47
|
+
className?: string,
|
|
48
|
+
): void {
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (Platform.OS !== "web" || !className?.trim()) return;
|
|
51
|
+
|
|
52
|
+
const element = resolveWebElement(ref);
|
|
53
|
+
if (!element) return;
|
|
54
|
+
|
|
55
|
+
const classes = className.trim().split(/\s+/);
|
|
56
|
+
element.classList.add(...classes);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
element.classList.remove(...classes);
|
|
60
|
+
};
|
|
61
|
+
}, [ref, className]);
|
|
62
|
+
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
|
|
2
|
-
import type { CSSProperties } from "react";
|
|
3
|
-
|
|
4
|
-
type RNStyle = StyleProp<ViewStyle> | StyleProp<TextStyle>;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Converts React Native StyleSheet values to CSS properties for DOM elements.
|
|
8
|
-
* Required when rendering native HTML on web (browsers ignore paddingVertical, etc.).
|
|
9
|
-
*/
|
|
10
|
-
export function rnStyleToWebStyle(style: RNStyle): CSSProperties {
|
|
11
|
-
const flat = StyleSheet.flatten(style);
|
|
12
|
-
if (!flat) return {};
|
|
13
|
-
|
|
14
|
-
const css: Record<string, unknown> = {};
|
|
15
|
-
|
|
16
|
-
for (const [key, value] of Object.entries(flat)) {
|
|
17
|
-
if (value === undefined || value === null) continue;
|
|
18
|
-
|
|
19
|
-
switch (key) {
|
|
20
|
-
case "paddingVertical":
|
|
21
|
-
css.paddingTop = value;
|
|
22
|
-
css.paddingBottom = value;
|
|
23
|
-
break;
|
|
24
|
-
case "paddingHorizontal":
|
|
25
|
-
css.paddingLeft = value;
|
|
26
|
-
css.paddingRight = value;
|
|
27
|
-
break;
|
|
28
|
-
case "marginVertical":
|
|
29
|
-
css.marginTop = value;
|
|
30
|
-
css.marginBottom = value;
|
|
31
|
-
break;
|
|
32
|
-
case "marginHorizontal":
|
|
33
|
-
css.marginLeft = value;
|
|
34
|
-
css.marginRight = value;
|
|
35
|
-
break;
|
|
36
|
-
default:
|
|
37
|
-
css[key] = value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (css.alignItems || css.justifyContent || css.flexDirection) {
|
|
42
|
-
css.display = css.display ?? "flex";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return css as CSSProperties;
|
|
46
|
-
}
|