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.
- package/package.json +1 -1
- package/template/AGENTS.md +36 -7
- package/template/README.md +14 -0
- package/template/bun.lock +316 -48
- package/template/gitignore +3 -0
- package/template/packages/ui/package.json +10 -6
- package/template/packages/ui/src/components/Button/button.tsx +59 -72
- package/template/packages/ui/src/uniwind-env.d.ts +1 -0
- package/template/storybook/native/.rnstorybook/preview.tsx +2 -2
- package/template/storybook/native/global.css +5 -0
- package/template/storybook/native/metro.config.js +14 -4
- package/template/storybook/native/package.json +3 -1
- package/template/storybook/native/uniwind-env.d.ts +3 -0
- package/template/storybook/web/.storybook/main.ts +16 -0
- package/template/storybook/web/.storybook/preview.tsx +1 -0
- package/template/storybook/web/.storybook/uniwind-env.d.ts +3 -0
- package/template/storybook/web/global.css +5 -0
- package/template/storybook/web/package.json +3 -0
- package/template/packages/ui/src/theme/tokens.ts +0 -40
package/template/gitignore
CHANGED
|
@@ -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": "
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 =
|
|
65
|
-
export type ButtonSize =
|
|
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 ??
|
|
96
|
+
accessibilityLabel={accessibilityLabel ?? text}
|
|
115
97
|
accessibilityState={accessibilityState}
|
|
116
98
|
disabled={inactive}
|
|
117
99
|
onPress={onPress}
|
|
118
100
|
testID={testID}
|
|
119
|
-
|
|
101
|
+
className={container({ variant, size, className })}
|
|
102
|
+
style={style}
|
|
120
103
|
>
|
|
121
104
|
{loading ? (
|
|
122
|
-
<ActivityIndicator
|
|
105
|
+
<ActivityIndicator
|
|
106
|
+
size="small"
|
|
107
|
+
colorClassName={variant === 'primary' ? 'text-white' : 'text-zinc-900'}
|
|
108
|
+
testID="button-spinner"
|
|
109
|
+
/>
|
|
123
110
|
) : (
|
|
124
|
-
<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
|
-
|
|
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
|
),
|
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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",
|
|
@@ -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;
|
|
@@ -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;
|