create-react-native-library-template 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,9 @@ expo-env.d.ts
13
13
  # Storybook React Native (generated)
14
14
  storybook.requires.ts
15
15
 
16
+ # Uniwind (generated type declarations for className autocomplete)
17
+ uniwind-types.d.ts
18
+
16
19
  # Tests
17
20
  coverage/
18
21
 
@@ -20,11 +20,6 @@
20
20
  "types": "./lib/typescript/components/Button/button.d.ts",
21
21
  "default": "./lib/module/components/Button/button.js"
22
22
  },
23
- "./theme": {
24
- "source": "./src/theme/tokens.ts",
25
- "types": "./lib/typescript/theme/tokens.d.ts",
26
- "default": "./lib/module/theme/tokens.js"
27
- },
28
23
  "./package.json": "./package.json"
29
24
  },
30
25
  "files": [
@@ -44,7 +39,7 @@
44
39
  "react-native-builder-bob": {
45
40
  "source": "src",
46
41
  "output": "lib",
47
- "exclude": "**/{*.test,*.stories}.{ts,tsx}",
42
+ "exclude": "**/*.{test,stories,d}.{ts,tsx}",
48
43
  "targets": [
49
44
  [
50
45
  "module",
@@ -72,10 +67,19 @@
72
67
  "react-native-builder-bob": "^0.43.0",
73
68
  "react-native-web": "0.21.2",
74
69
  "storybook": "^10.4.6",
70
+ "tailwindcss": "^4.3.2",
75
71
  "typescript": "^6",
72
+ "uniwind": "^1.10.0",
76
73
  "vitest": "^4.1.10"
77
74
  },
78
75
  "publishConfig": {
79
76
  "access": "public"
77
+ },
78
+ "dependencies": {
79
+ "class-variance-authority": "^0.7.1"
80
+ },
81
+ "peerDependencies": {
82
+ "tailwindcss": "^4.3.2",
83
+ "uniwind": "^1.10.0"
80
84
  }
81
85
  }
@@ -1,68 +1,56 @@
1
- import { useCallback, useMemo } from 'react';
2
- import { ActivityIndicator, Pressable, type StyleProp, StyleSheet, Text, type ViewStyle } from 'react-native';
3
- import { colors, fontSizes, radii, spacing } from '../../theme/tokens';
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import { useMemo } from 'react';
3
+ import { ActivityIndicator, Pressable, type StyleProp, Text, type ViewStyle } from 'react-native';
4
4
 
5
- function getButtonOpacity(inactive: boolean, pressed: boolean) {
6
- if (inactive) return 0.5;
7
- if (pressed) return 0.7;
8
- return 1;
9
- }
10
-
11
- function variantStyles(variant: ButtonVariant, pressed: boolean, inactive: boolean): ViewStyle {
12
- if (inactive && variant !== 'ghost') return { backgroundColor: colors.disabled };
13
- switch (variant) {
14
- case 'primary':
15
- return { backgroundColor: pressed ? colors.primaryPressed : colors.primary };
16
- case 'secondary':
17
- return { backgroundColor: pressed ? colors.secondaryPressed : colors.secondary };
18
- case 'ghost':
19
- return {
20
- backgroundColor: 'transparent',
21
- borderWidth: StyleSheet.hairlineWidth,
22
- borderColor: colors.border,
23
- opacity: getButtonOpacity(inactive, pressed),
24
- };
25
- default:
26
- return {};
27
- }
28
- }
29
-
30
- function labelStyles(variant: ButtonVariant, inactive: boolean) {
31
- if (inactive && variant !== 'ghost') return { color: colors.textDisabled };
32
- return { color: variant === 'primary' ? colors.textInverse : colors.textPrimary };
33
- }
34
-
35
- function spinnerColor(variant: ButtonVariant): typeof colors.textInverse | typeof colors.textPrimary {
36
- return variant === 'primary' ? colors.textInverse : colors.textPrimary;
37
- }
5
+ // Uniwind resolves these Tailwind classes at build time. Pressable exposes the
6
+ // `active:` (pressed) and `disabled:` pseudo-variants; Text has neither, so its
7
+ // disabled color is driven by the `disabled` cva variant below.
8
+ //
9
+ // On web, react-native-web renders Pressable as a focusable `div[role=button]`,
10
+ // which gets the browser's UA outline on *every* focus (including mouse clicks) —
11
+ // unlike a native `<button>`. The `web:`-scoped classes suppress that always-on
12
+ // outline and restore a keyboard-only focus ring; they compile to `@supports`
13
+ // blocks the native bundler skips, so native styling is unaffected.
14
+ const FOCUS_RING =
15
+ 'web:focus:outline-none web:focus-visible:ring-2 web:focus-visible:ring-indigo-500 web:focus-visible:ring-offset-2';
38
16
 
39
- const styles = StyleSheet.create({
40
- base: {
41
- alignItems: 'center',
42
- justifyContent: 'center',
43
- flexDirection: 'row',
44
- borderRadius: radii.md,
45
- alignSelf: 'flex-start',
17
+ const container = cva(`flex-row items-center justify-center self-start rounded-lg ${FOCUS_RING}`, {
18
+ variants: {
19
+ variant: {
20
+ primary: 'bg-indigo-600 active:bg-indigo-700 disabled:bg-zinc-200',
21
+ secondary: 'bg-indigo-100 active:bg-indigo-200 disabled:bg-zinc-200',
22
+ ghost: 'border border-zinc-300 active:opacity-70 disabled:opacity-50',
23
+ },
24
+ size: {
25
+ sm: 'min-h-8 px-3 py-1',
26
+ md: 'min-h-10 px-4 py-2',
27
+ lg: 'min-h-12 px-6 py-3',
28
+ },
46
29
  },
47
- label: {
48
- fontWeight: '600',
49
- },
50
- });
51
-
52
- const sizeStyles = StyleSheet.create({
53
- sm: { paddingVertical: spacing.xs, paddingHorizontal: spacing.md, minHeight: 32 },
54
- md: { paddingVertical: spacing.sm, paddingHorizontal: spacing.lg, minHeight: 40 },
55
- lg: { paddingVertical: spacing.md, paddingHorizontal: spacing.xl, minHeight: 48 },
30
+ defaultVariants: { variant: 'primary', size: 'md' },
56
31
  });
57
32
 
58
- const labelSizeStyles = StyleSheet.create({
59
- sm: { fontSize: fontSizes.sm },
60
- md: { fontSize: fontSizes.md },
61
- lg: { fontSize: fontSizes.lg },
33
+ const label = cva('font-semibold', {
34
+ variants: {
35
+ variant: { primary: '', secondary: '', ghost: '' },
36
+ size: { sm: 'text-sm', md: 'text-base', lg: 'text-lg' },
37
+ disabled: { true: '', false: '' },
38
+ },
39
+ compoundVariants: [
40
+ { variant: 'primary', disabled: false, class: 'text-white' },
41
+ { variant: 'secondary', disabled: false, class: 'text-zinc-900' },
42
+ { variant: 'ghost', disabled: false, class: 'text-zinc-900' },
43
+ // Muted label for disabled primary/secondary; ghost keeps its color and the
44
+ // container dims via `disabled:opacity-50`.
45
+ { variant: 'primary', disabled: true, class: 'text-zinc-400' },
46
+ { variant: 'secondary', disabled: true, class: 'text-zinc-400' },
47
+ { variant: 'ghost', disabled: true, class: 'text-zinc-900' },
48
+ ],
49
+ defaultVariants: { variant: 'primary', size: 'md', disabled: false },
62
50
  });
63
51
 
64
- export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
65
- export type ButtonSize = 'sm' | 'md' | 'lg';
52
+ export type ButtonVariant = NonNullable<VariantProps<typeof container>['variant']>;
53
+ export type ButtonSize = NonNullable<VariantProps<typeof container>['size']>;
66
54
 
67
55
  export type ButtonProps = {
68
56
  /** Text rendered inside the button. */
@@ -77,6 +65,8 @@ export type ButtonProps = {
77
65
  disabled?: boolean;
78
66
  /** Shows a spinner instead of the label and disables interaction. @default false */
79
67
  loading?: boolean;
68
+ /** Extra Tailwind classes merged onto the outer pressable. */
69
+ className?: string;
80
70
  /** Style override for the outer pressable. */
81
71
  style?: StyleProp<ViewStyle>;
82
72
  /** Accessibility label; falls back to `label`. */
@@ -86,42 +76,39 @@ export type ButtonProps = {
86
76
  };
87
77
 
88
78
  export function Button({
89
- label,
79
+ label: text,
90
80
  onPress,
91
81
  variant = 'primary',
92
82
  size = 'md',
93
83
  disabled = false,
94
84
  loading = false,
85
+ className,
95
86
  style,
96
87
  accessibilityLabel,
97
88
  testID,
98
89
  }: ButtonProps) {
99
90
  const inactive = disabled || loading;
100
91
  const accessibilityState = useMemo(() => ({ disabled: inactive, busy: loading }), [inactive, loading]);
101
- const buttonStyles = useCallback(
102
- ({ pressed }: { pressed: boolean }) => [styles.base, sizeStyles[size], variantStyles(variant, pressed, inactive), style],
103
- [size, variant, inactive, style],
104
- );
105
-
106
- const textStyles = useCallback(
107
- () => [styles.label, labelSizeStyles[size], labelStyles(variant, inactive)],
108
- [size, variant, inactive],
109
- );
110
92
 
111
93
  return (
112
94
  <Pressable
113
95
  accessibilityRole="button"
114
- accessibilityLabel={accessibilityLabel ?? label}
96
+ accessibilityLabel={accessibilityLabel ?? text}
115
97
  accessibilityState={accessibilityState}
116
98
  disabled={inactive}
117
99
  onPress={onPress}
118
100
  testID={testID}
119
- style={buttonStyles}
101
+ className={container({ variant, size, className })}
102
+ style={style}
120
103
  >
121
104
  {loading ? (
122
- <ActivityIndicator size="small" color={spinnerColor(variant)} testID="button-spinner" />
105
+ <ActivityIndicator
106
+ size="small"
107
+ colorClassName={variant === 'primary' ? 'text-white' : 'text-zinc-900'}
108
+ testID="button-spinner"
109
+ />
123
110
  ) : (
124
- <Text style={textStyles()}>{label}</Text>
111
+ <Text className={label({ variant, size, disabled: inactive })}>{text}</Text>
125
112
  )}
126
113
  </Pressable>
127
114
  );
@@ -0,0 +1 @@
1
+ /// <reference types="uniwind/types" />
@@ -1,11 +1,11 @@
1
1
  import type { Preview } from '@storybook/react';
2
2
  import { View } from 'react-native';
3
+ import '../global.css';
3
4
 
4
5
  const preview: Preview = {
5
6
  decorators: [
6
7
  (Story) => (
7
- // biome-ignore lint/plugin: exception for storybook preview
8
- <View style={{ flex: 1, alignItems: 'flex-start', padding: 16 }}>
8
+ <View className="flex-1 items-start p-4">
9
9
  <Story />
10
10
  </View>
11
11
  ),
@@ -0,0 +1,5 @@
1
+ @import "tailwindcss";
2
+ @import "uniwind";
3
+
4
+ /* Stories live in the library workspace, so point Tailwind's class scanner there. */
5
+ @source "../../packages/ui/src";
@@ -2,6 +2,7 @@
2
2
  const path = require('node:path');
3
3
  const { withStorybook } = require('@storybook/react-native/metro/withStorybook');
4
4
  const { getDefaultConfig } = require('expo/metro-config');
5
+ const { withUniwindConfig } = require('uniwind/metro');
5
6
 
6
7
  const projectRoot = import.meta.dirname;
7
8
  const workspaceRoot = path.resolve(projectRoot, '../..');
@@ -12,7 +13,16 @@ const config = getDefaultConfig(projectRoot);
12
13
  config.watchFolders = [workspaceRoot];
13
14
  config.resolver.nodeModulesPaths = [path.resolve(projectRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules')];
14
15
 
15
- module.exports = withStorybook(config, {
16
- enabled: true,
17
- configPath: path.resolve(projectRoot, '.rnstorybook'),
18
- });
16
+ // Uniwind rewrites `react-native` imports to className-aware components at bundle
17
+ // time. It must be the outermost Metro wrapper. `cssEntryFile` must stay a plain
18
+ // relative string (not path.resolve). The generated dtsFile is gitignored.
19
+ module.exports = withUniwindConfig(
20
+ withStorybook(config, {
21
+ enabled: true,
22
+ configPath: path.resolve(projectRoot, '.rnstorybook'),
23
+ }),
24
+ {
25
+ cssEntryFile: './global.css',
26
+ dtsFile: './uniwind-types.d.ts',
27
+ },
28
+ );
@@ -30,7 +30,9 @@
30
30
  "react-native-safe-area-context": "5.6.0",
31
31
  "react-native-svg": "15.12.1",
32
32
  "react-native-worklets": "0.5.1",
33
- "storybook": "^10.4.6"
33
+ "storybook": "^10.4.6",
34
+ "tailwindcss": "^4.3.2",
35
+ "uniwind": "^1.10.0"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/react": "~19.1.10",
@@ -0,0 +1,3 @@
1
+ /// <reference types="uniwind/types" />
2
+
3
+ declare module '*.css';
@@ -1,4 +1,6 @@
1
1
  import type { StorybookConfig } from '@storybook/react-native-web-vite';
2
+ import tailwindcss from '@tailwindcss/vite';
3
+ import { uniwind } from 'uniwind/vite';
2
4
 
3
5
  const config: StorybookConfig = {
4
6
  stories: ['../../../packages/ui/src/**/*.stories.@(ts|tsx)'],
@@ -7,6 +9,20 @@ const config: StorybookConfig = {
7
9
  name: '@storybook/react-native-web-vite',
8
10
  options: {},
9
11
  },
12
+ // Uniwind resolves `className` on RN components at build time; it composes with
13
+ // the framework's react-native-web aliasing. `@tailwindcss/vite` processes the
14
+ // `@import 'tailwindcss'` in global.css.
15
+ viteFinal: (viteConfig) => {
16
+ viteConfig.plugins ??= [];
17
+ viteConfig.plugins.push(tailwindcss(), uniwind({ cssEntryFile: './global.css', dtsFile: './uniwind-types.d.ts' }));
18
+ // Uniwind excludes `react-native` from dep optimization, which otherwise stops Vite
19
+ // from pre-bundling react-native-web as a unit — leaving its CJS transitive dep
20
+ // `@react-native/normalize-colors` served without a synthesized `default` export.
21
+ // Force-optimize react-native-web so esbuild/rolldown inlines those CJS deps again.
22
+ viteConfig.optimizeDeps ??= {};
23
+ viteConfig.optimizeDeps.include = [...(viteConfig.optimizeDeps.include ?? []), 'react-native-web'];
24
+ return viteConfig;
25
+ },
10
26
  };
11
27
 
12
28
  export default config;
@@ -1,4 +1,5 @@
1
1
  import type { Preview } from '@storybook/react';
2
+ import '../global.css';
2
3
 
3
4
  const preview: Preview = {
4
5
  parameters: {
@@ -0,0 +1,3 @@
1
+ /// <reference types="uniwind/types" />
2
+
3
+ declare module '*.css';
@@ -0,0 +1,5 @@
1
+ @import "tailwindcss";
2
+ @import "uniwind";
3
+
4
+ /* Stories live in the library workspace, so point Tailwind's class scanner there. */
5
+ @source "../../packages/ui/src";
@@ -21,12 +21,15 @@
21
21
  "@storybook/addon-vitest": "^10.4.6",
22
22
  "@storybook/react": "^10.4.6",
23
23
  "@storybook/react-native-web-vite": "^10.4.6",
24
+ "@tailwindcss/vite": "^4.3.2",
24
25
  "@types/node": "^26",
25
26
  "@types/react": "~19.1.10",
26
27
  "@vitest/browser-playwright": "^4.1.10",
27
28
  "playwright": "^1.61.1",
28
29
  "storybook": "^10.4.6",
30
+ "tailwindcss": "^4.3.2",
29
31
  "typescript": "^6",
32
+ "uniwind": "^1.10.0",
30
33
  "vite": "^8.1.3",
31
34
  "vitest": "^4.1.10"
32
35
  }
@@ -1,40 +0,0 @@
1
- export const colors = {
2
- primary: '#4f46e5',
3
- primaryPressed: '#4338ca',
4
- secondary: '#e0e7ff',
5
- secondaryPressed: '#c7d2fe',
6
- surface: '#ffffff',
7
- border: '#d4d4d8',
8
- textPrimary: '#18181b',
9
- textInverse: '#ffffff',
10
- textMuted: '#71717a',
11
- danger: '#dc2626',
12
- disabled: '#e4e4e7',
13
- textDisabled: '#a1a1aa',
14
- } as const;
15
-
16
- export const spacing = {
17
- xs: 4,
18
- sm: 8,
19
- md: 12,
20
- lg: 16,
21
- xl: 24,
22
- } as const;
23
-
24
- export const radii = {
25
- sm: 4,
26
- md: 8,
27
- lg: 16,
28
- full: 9999,
29
- } as const;
30
-
31
- export const fontSizes = {
32
- sm: 13,
33
- md: 15,
34
- lg: 17,
35
- } as const;
36
-
37
- export type Color = keyof typeof colors;
38
- export type Spacing = keyof typeof spacing;
39
- export type Radius = keyof typeof radii;
40
- export type FontSize = keyof typeof fontSizes;