@scripso-homepad/ui 0.3.1 → 0.3.3

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
@@ -115,18 +115,24 @@ export function Example() {
115
115
 
116
116
  #### Tailwind classes (React web)
117
117
 
118
- On web, `className` is applied to real DOM elements (`<button>` / `<span>`), so Tailwind utilities work with Vite or Next.js:
118
+ `className` is applied to the same `TouchableOpacity` / `Text` DOM nodes as the default styles layout and padding stay consistent with native:
119
119
 
120
120
  ```tsx
121
121
  <Button
122
122
  title="Save"
123
123
  onPress={handleSave}
124
- className="rounded-full bg-violet-600 px-8 shadow-lg"
124
+ className="!bg-violet-600 shadow-lg"
125
125
  textClassName="text-sm font-bold uppercase"
126
126
  />
127
127
  ```
128
128
 
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`.
129
+ Use `!` (important) on Tailwind utilities when overriding default colors, e.g. `!bg-red-500`, because react-native-web applies inline styles from `StyleSheet`.
130
+
131
+ Ensure Tailwind scans the package if needed:
132
+
133
+ ```js
134
+ content: ["./src/**/*.{js,ts,jsx,tsx}", "./node_modules/@scripso-homepad/ui/**/*.{js,ts,jsx,tsx}"],
135
+ ```
130
136
 
131
137
  ## Development
132
138
 
package/dist/index.cjs CHANGED
@@ -5,6 +5,33 @@ var reactNative = require('react-native');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/components/Button.tsx
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;
20
+ }
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]);
34
+ }
8
35
  function Button({
9
36
  title,
10
37
  onPress,
@@ -14,41 +41,23 @@ function Button({
14
41
  className,
15
42
  textClassName
16
43
  }) {
44
+ const containerRef = react.useRef(null);
45
+ const textRef = react.useRef(null);
46
+ useApplyWebClassName(containerRef, className);
47
+ useApplyWebClassName(textRef, textClassName);
17
48
  const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
18
49
  const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
19
- if (reactNative.Platform.OS === "web") {
20
- return react.createElement(
21
- "button",
22
- {
23
- type: "button",
24
- className,
25
- style: reactNative.StyleSheet.flatten(containerStyle),
26
- disabled,
27
- onClick: (event) => {
28
- onPress(event);
29
- },
30
- "aria-disabled": disabled
31
- },
32
- react.createElement(
33
- "span",
34
- {
35
- className: textClassName,
36
- style: reactNative.StyleSheet.flatten(labelStyle)
37
- },
38
- title
39
- )
40
- );
41
- }
42
50
  return /* @__PURE__ */ jsxRuntime.jsx(
43
51
  reactNative.TouchableOpacity,
44
52
  {
53
+ ref: containerRef,
45
54
  style: containerStyle,
46
55
  onPress,
47
56
  disabled,
48
57
  activeOpacity: 0.7,
49
58
  accessibilityRole: "button",
50
59
  accessibilityState: { disabled },
51
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: labelStyle, children: title })
60
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { ref: textRef, style: labelStyle, children: title })
52
61
  }
53
62
  );
54
63
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/Button.tsx"],"names":["Platform","createElement","StyleSheet","jsx","TouchableOpacity","Text"],"mappings":";;;;;;;AAkCO,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,IAAIA,oBAAA,CAAS,OAAO,KAAA,EAAO;AACzB,IAAA,OAAOC,mBAAA;AAAA,MACL,QAAA;AAAA,MACA;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,SAAA;AAAA,QACA,KAAA,EAAOC,sBAAA,CAAW,OAAA,CAAQ,cAAc,CAAA;AAAA,QACxC,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,MACAD,mBAAA;AAAA,QACE,MAAA;AAAA,QACA;AAAA,UACE,SAAA,EAAW,aAAA;AAAA,UACX,KAAA,EAAOC,sBAAA,CAAW,OAAA,CAAQ,UAAU;AAAA,SACtC;AAAA,QACA;AAAA;AACF,KACF;AAAA,EACF;AAEA,EAAA,uBACEC,cAAA;AAAA,IAACC,4BAAA;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,kBAAAD,cAAA,CAACE,gBAAA,EAAA,EAAK,KAAA,EAAO,UAAA,EAAa,QAAA,EAAA,KAAA,EAAM;AAAA;AAAA,GAClC;AAEJ;AAEA,IAAM,MAAA,GAASH,uBAAW,MAAA,CAAO;AAAA,EAC/B,MAAA,EAAQ;AAAA,IACN,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,EAAA;AAAA,IACjB,iBAAA,EAAmB,EAAA;AAAA,IACnB,YAAA,EAAc,CAAA;AAAA,IACd,UAAA,EAAY,QAAA;AAAA,IACZ,cAAA,EAAgB,QAAA;AAAA,IAChB,QAAA,EAAU,GAAA;AAAA,IACV,WAAA,EAAa;AAAA,GACf;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,eAAA,EAAiB,SAAA;AAAA,IACjB,OAAA,EAAS;AAAA,GACX;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,KAAA,EAAO,SAAA;AAAA,IACP,QAAA,EAAU,EAAA;AAAA,IACV,UAAA,EAAY;AAAA,GACd;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,KAAA,EAAO;AAAA;AAEX,CAAC,CAAA","file":"index.cjs","sourcesContent":["import 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\";\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.\n * On web: applied to the underlying `<button>` element (Tailwind works).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label.\n * On web: applied to the underlying `<span>` element (Tailwind works).\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 if (Platform.OS === \"web\") {\n return createElement(\n \"button\",\n {\n type: \"button\",\n className,\n style: StyleSheet.flatten(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: StyleSheet.flatten(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 interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import * as react from 'react';
2
2
  import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
3
3
 
4
4
  interface ButtonProps {
@@ -10,18 +10,16 @@ interface ButtonProps {
10
10
  /** Additional label styles (works on web and native). */
11
11
  textStyle?: StyleProp<TextStyle>;
12
12
  /**
13
- * CSS class names for the container.
14
- * On web: applied to the underlying `<button>` element (Tailwind works).
13
+ * CSS class names for the container (web: applied to the same DOM node as default styles).
15
14
  * On native: ignored unless using NativeWind with cssInterop.
16
15
  */
17
16
  className?: string;
18
17
  /**
19
- * CSS class names for the label.
20
- * On web: applied to the underlying `<span>` element (Tailwind works).
18
+ * CSS class names for the label (web).
21
19
  * On native: ignored unless using NativeWind with cssInterop.
22
20
  */
23
21
  textClassName?: string;
24
22
  }
25
- declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): React.JSX.Element;
23
+ declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
26
24
 
27
25
  export { Button, type ButtonProps };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import * as react from 'react';
2
2
  import { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
3
3
 
4
4
  interface ButtonProps {
@@ -10,18 +10,16 @@ interface ButtonProps {
10
10
  /** Additional label styles (works on web and native). */
11
11
  textStyle?: StyleProp<TextStyle>;
12
12
  /**
13
- * CSS class names for the container.
14
- * On web: applied to the underlying `<button>` element (Tailwind works).
13
+ * CSS class names for the container (web: applied to the same DOM node as default styles).
15
14
  * On native: ignored unless using NativeWind with cssInterop.
16
15
  */
17
16
  className?: string;
18
17
  /**
19
- * CSS class names for the label.
20
- * On web: applied to the underlying `<span>` element (Tailwind works).
18
+ * CSS class names for the label (web).
21
19
  * On native: ignored unless using NativeWind with cssInterop.
22
20
  */
23
21
  textClassName?: string;
24
22
  }
25
- declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): React.JSX.Element;
23
+ declare function Button({ title, onPress, disabled, style, textStyle, className, textClassName, }: ButtonProps): react.JSX.Element;
26
24
 
27
25
  export { Button, type ButtonProps };
package/dist/index.js CHANGED
@@ -1,8 +1,35 @@
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 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;
18
+ }
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]);
32
+ }
6
33
  function Button({
7
34
  title,
8
35
  onPress,
@@ -12,41 +39,23 @@ function Button({
12
39
  className,
13
40
  textClassName
14
41
  }) {
42
+ const containerRef = useRef(null);
43
+ const textRef = useRef(null);
44
+ useApplyWebClassName(containerRef, className);
45
+ useApplyWebClassName(textRef, textClassName);
15
46
  const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
16
47
  const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
17
- if (Platform.OS === "web") {
18
- return createElement(
19
- "button",
20
- {
21
- type: "button",
22
- className,
23
- style: StyleSheet.flatten(containerStyle),
24
- disabled,
25
- onClick: (event) => {
26
- onPress(event);
27
- },
28
- "aria-disabled": disabled
29
- },
30
- createElement(
31
- "span",
32
- {
33
- className: textClassName,
34
- style: StyleSheet.flatten(labelStyle)
35
- },
36
- title
37
- )
38
- );
39
- }
40
48
  return /* @__PURE__ */ jsx(
41
49
  TouchableOpacity,
42
50
  {
51
+ ref: containerRef,
43
52
  style: containerStyle,
44
53
  onPress,
45
54
  disabled,
46
55
  activeOpacity: 0.7,
47
56
  accessibilityRole: "button",
48
57
  accessibilityState: { disabled },
49
- children: /* @__PURE__ */ jsx(Text, { style: labelStyle, children: title })
58
+ children: /* @__PURE__ */ jsx(Text, { ref: textRef, style: labelStyle, children: title })
50
59
  }
51
60
  );
52
61
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/Button.tsx"],"names":[],"mappings":";;;;;AAkCO,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,IAAI,QAAA,CAAS,OAAO,KAAA,EAAO;AACzB,IAAA,OAAO,aAAA;AAAA,MACL,QAAA;AAAA,MACA;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,SAAA;AAAA,QACA,KAAA,EAAO,UAAA,CAAW,OAAA,CAAQ,cAAc,CAAA;AAAA,QACxC,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,UAAA,CAAW,OAAA,CAAQ,UAAU;AAAA,SACtC;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,GAAS,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 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\";\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.\n * On web: applied to the underlying `<button>` element (Tailwind works).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label.\n * On web: applied to the underlying `<span>` element (Tailwind works).\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 if (Platform.OS === \"web\") {\n return createElement(\n \"button\",\n {\n type: \"button\",\n className,\n style: StyleSheet.flatten(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: StyleSheet.flatten(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 interface ButtonProps {\n title: string;\n onPress: (event: GestureResponderEvent) => void;\n disabled?: boolean;\n /** Additional container styles (works on web and native). */\n style?: StyleProp<ViewStyle>;\n /** Additional label styles (works on web and native). */\n textStyle?: StyleProp<TextStyle>;\n /**\n * CSS class names for the container (web: applied to the same DOM node as default styles).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n className?: string;\n /**\n * CSS class names for the label (web).\n * On native: ignored unless using NativeWind with cssInterop.\n */\n textClassName?: string;\n}\n\nexport function Button({\n title,\n onPress,\n disabled = false,\n style,\n textStyle,\n className,\n textClassName,\n}: ButtonProps) {\n const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);\n const textRef = useRef<ComponentRef<typeof Text>>(null);\n\n useApplyWebClassName(containerRef, className);\n useApplyWebClassName(textRef, textClassName);\n\n const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];\n const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];\n\n return (\n <TouchableOpacity\n ref={containerRef}\n style={containerStyle}\n onPress={onPress}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled }}\n >\n <Text ref={textRef} style={labelStyle}>\n {title}\n </Text>\n </TouchableOpacity>\n );\n}\n\nconst styles = StyleSheet.create({\n button: {\n backgroundColor: \"#2563eb\",\n paddingVertical: 12,\n paddingHorizontal: 24,\n borderRadius: 8,\n alignItems: \"center\",\n justifyContent: \"center\",\n minWidth: 120,\n borderWidth: 0,\n },\n buttonDisabled: {\n backgroundColor: \"#93c5fd\",\n opacity: 0.7,\n },\n text: {\n color: \"#ffffff\",\n fontSize: 16,\n fontWeight: \"600\",\n },\n textDisabled: {\n color: \"#e5e7eb\",\n },\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -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,6 +8,7 @@ import {
9
8
  type TextStyle,
10
9
  type ViewStyle,
11
10
  } from "react-native";
11
+ import { useApplyWebClassName } from "../utils/useApplyWebClassName";
12
12
 
13
13
  export interface ButtonProps {
14
14
  title: string;
@@ -19,14 +19,12 @@ export interface ButtonProps {
19
19
  /** Additional label styles (works on web and native). */
20
20
  textStyle?: StyleProp<TextStyle>;
21
21
  /**
22
- * CSS class names for the container.
23
- * On web: applied to the underlying `<button>` element (Tailwind works).
22
+ * CSS class names for the container (web: applied to the same DOM node as default styles).
24
23
  * On native: ignored unless using NativeWind with cssInterop.
25
24
  */
26
25
  className?: string;
27
26
  /**
28
- * CSS class names for the label.
29
- * On web: applied to the underlying `<span>` element (Tailwind works).
27
+ * CSS class names for the label (web).
30
28
  * On native: ignored unless using NativeWind with cssInterop.
31
29
  */
32
30
  textClassName?: string;
@@ -41,35 +39,18 @@ export function Button({
41
39
  className,
42
40
  textClassName,
43
41
  }: ButtonProps) {
42
+ const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
43
+ const textRef = useRef<ComponentRef<typeof Text>>(null);
44
+
45
+ useApplyWebClassName(containerRef, className);
46
+ useApplyWebClassName(textRef, textClassName);
47
+
44
48
  const containerStyle = [styles.button, disabled && styles.buttonDisabled, style];
45
49
  const labelStyle = [styles.text, disabled && styles.textDisabled, textStyle];
46
50
 
47
- if (Platform.OS === "web") {
48
- return createElement(
49
- "button",
50
- {
51
- type: "button",
52
- className,
53
- style: StyleSheet.flatten(containerStyle),
54
- disabled,
55
- onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
56
- onPress(event as unknown as GestureResponderEvent);
57
- },
58
- "aria-disabled": disabled,
59
- },
60
- createElement(
61
- "span",
62
- {
63
- className: textClassName,
64
- style: StyleSheet.flatten(labelStyle),
65
- },
66
- title,
67
- ),
68
- );
69
- }
70
-
71
51
  return (
72
52
  <TouchableOpacity
53
+ ref={containerRef}
73
54
  style={containerStyle}
74
55
  onPress={onPress}
75
56
  disabled={disabled}
@@ -77,7 +58,9 @@ export function Button({
77
58
  accessibilityRole="button"
78
59
  accessibilityState={{ disabled }}
79
60
  >
80
- <Text style={labelStyle}>{title}</Text>
61
+ <Text ref={textRef} style={labelStyle}>
62
+ {title}
63
+ </Text>
81
64
  </TouchableOpacity>
82
65
  );
83
66
  }
@@ -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
+ }