@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 CHANGED
@@ -92,15 +92,28 @@ export function Example() {
92
92
 
93
93
  ### `Button`
94
94
 
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
- | `style` | `StyleProp<ViewStyle>` | No | | Extra container styles (web + native) |
101
- | `textStyle` | `StyleProp<TextStyle>` | No | | Extra label styles (web + native) |
102
- | `className` | `string` | No | — | CSS/Tailwind classes for container (web / NativeWind) |
103
- | `textClassName` | `string` | No | — | CSS/Tailwind classes for label (web / NativeWind) |
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
- On web, `className` is applied to real DOM elements (`<button>` / `<span>`), so Tailwind utilities work with Vite or Next.js:
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="rounded-full bg-violet-600 px-8 shadow-lg"
137
+ className="!bg-violet-600 shadow-lg"
125
138
  textClassName="text-sm font-bold uppercase"
126
139
  />
127
140
  ```
128
141
 
129
- > **Note:** `react-native-web` does not forward `className` to the DOM by default. This package renders native HTML elements on web so Tailwind classes appear in the DOM. On React Native, use `style` or [NativeWind](https://www.nativewind.dev/) with `cssInterop`.
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 rnStyleToWebStyle(style) {
9
- const flat = reactNative.StyleSheet.flatten(style);
10
- if (!flat) return {};
11
- const css = {};
12
- for (const [key, value] of Object.entries(flat)) {
13
- if (value === void 0 || value === null) continue;
14
- switch (key) {
15
- case "paddingVertical":
16
- css.paddingTop = value;
17
- css.paddingBottom = value;
18
- break;
19
- case "paddingHorizontal":
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 css;
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 containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
50
- const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
51
- const useWebDom = reactNative.Platform.OS === "web" && (className != null || textClassName != null);
52
- if (useWebDom) {
53
- return react.createElement(
54
- "button",
55
- {
56
- type: "button",
57
- className,
58
- style: rnStyleToWebStyle(containerStyle),
59
- disabled,
60
- onClick: (event) => {
61
- onPress(event);
62
- },
63
- "aria-disabled": disabled
64
- },
65
- react.createElement(
66
- "span",
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
- button: {
90
- backgroundColor: "#2563eb",
91
- paddingVertical: 12,
92
- paddingHorizontal: 24,
93
- borderRadius: 8,
79
+ base: {
94
80
  alignItems: "center",
95
81
  justifyContent: "center",
96
- minWidth: 120,
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
- buttonDisabled: {
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
- text: {
104
- color: "#ffffff",
105
- fontSize: 16,
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
- textDisabled: {
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
 
@@ -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 React from 'react';
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 only uses DOM `<button>` for Tailwind).
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 only).
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): 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,25 +1,31 @@
1
- import React from 'react';
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 only uses DOM `<button>` for Tailwind).
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 only).
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): 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
@@ -1,110 +1,178 @@
1
- import { createElement } from 'react';
2
- import { StyleSheet, Platform, TouchableOpacity, Text } from 'react-native';
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 rnStyleToWebStyle(style) {
7
- const flat = StyleSheet.flatten(style);
8
- if (!flat) return {};
9
- const css = {};
10
- for (const [key, value] of Object.entries(flat)) {
11
- if (value === void 0 || value === null) continue;
12
- switch (key) {
13
- case "paddingVertical":
14
- css.paddingTop = value;
15
- css.paddingBottom = value;
16
- break;
17
- case "paddingHorizontal":
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 css;
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 containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
48
- const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
49
- const useWebDom = Platform.OS === "web" && (className != null || textClassName != null);
50
- if (useWebDom) {
51
- return createElement(
52
- "button",
53
- {
54
- type: "button",
55
- className,
56
- style: rnStyleToWebStyle(containerStyle),
57
- disabled,
58
- onClick: (event) => {
59
- onPress(event);
60
- },
61
- "aria-disabled": disabled
62
- },
63
- createElement(
64
- "span",
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
- button: {
88
- backgroundColor: "#2563eb",
89
- paddingVertical: 12,
90
- paddingHorizontal: 24,
91
- borderRadius: 8,
77
+ base: {
92
78
  alignItems: "center",
93
79
  justifyContent: "center",
94
- minWidth: 120,
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
- buttonDisabled: {
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
- text: {
102
- color: "#ffffff",
103
- fontSize: 16,
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
- textDisabled: {
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":"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","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,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -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 LongLabel: Story = {
31
- args: {
32
- title: "Continue to next step",
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 CustomStyle: Story = {
37
- args: {
38
- title: "Custom styles",
39
- style: {
40
- backgroundColor: "#dc2626",
41
- borderRadius: 999,
42
- paddingHorizontal: 32,
43
- },
44
- textStyle: {
45
- fontSize: 14,
46
- letterSpacing: 1,
47
- textTransform: "uppercase",
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 classes",
55
- className: "rounded-full bg-violet-600 px-8 shadow-lg",
56
- textClassName: "text-sm font-bold uppercase tracking-wide",
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 React, { createElement } from "react";
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 { rnStyleToWebStyle } from "../utils/rnStyleToWebStyle";
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 only uses DOM `<button>` for Tailwind).
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 only).
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 containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
44
- const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
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 useWebDom =
47
- Platform.OS === "web" && (className != null || textClassName != null);
57
+ const containerStyle = [
58
+ styles.base,
59
+ variantStyles[variant],
60
+ sizeStyles[size],
61
+ disabled && disabledVariantStyles[variant],
62
+ style,
63
+ ];
48
64
 
49
- if (useWebDom) {
50
- return createElement(
51
- "button",
52
- {
53
- type: "button",
54
- className,
55
- style: rnStyleToWebStyle(containerStyle),
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}>{title}</Text>
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
- button: {
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
- buttonDisabled: {
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
- text: {
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
- textDisabled: {
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
@@ -1,2 +1,6 @@
1
1
  export { Button } from "./components/Button";
2
- export type { ButtonProps } from "./components/Button";
2
+ export type {
3
+ ButtonProps,
4
+ ButtonSize,
5
+ ButtonVariant,
6
+ } from "./components/Button";
@@ -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
- }