cnnative-ui 1.0.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/README.md +34 -0
- package/babel.config.js +6 -0
- package/jest.config.js +22 -0
- package/jest.init.js +5 -0
- package/jest.setup.js +173 -0
- package/package.json +87 -0
- package/src/__tests__/a11y/accessibility.test.tsx +33 -0
- package/src/__tests__/components/badge.test.tsx +25 -0
- package/src/__tests__/components/button.test.tsx +53 -0
- package/src/__tests__/components/card.test.tsx +28 -0
- package/src/__tests__/components/input.test.tsx +33 -0
- package/src/__tests__/hooks/use-controllable.test.ts +58 -0
- package/src/__tests__/integration.test.tsx +35 -0
- package/src/__tests__/lib/utils.test.ts +23 -0
- package/src/__tests__/mocks/handlers.ts +19 -0
- package/src/components/accordion/accordion.tsx +143 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +65 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/alert-dialog/alert-dialog.tsx +145 -0
- package/src/components/alert-dialog/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +18 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +93 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +64 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.tsx +75 -0
- package/src/components/breadcrumb/index.ts +1 -0
- package/src/components/button/button.tsx +119 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +40 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +87 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.tsx +92 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/context-menu/context-menu.tsx +121 -0
- package/src/components/context-menu/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +124 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +145 -0
- package/src/components/dropdown-menu/index.ts +1 -0
- package/src/components/form/form.tsx +84 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +115 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +13 -0
- package/src/components/navigation-menu/index.ts +1 -0
- package/src/components/navigation-menu/navigation-menu.tsx +68 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.tsx +70 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.tsx +66 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.tsx +90 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +27 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +154 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +37 -0
- package/src/components/sheet/index.ts +1 -0
- package/src/components/sheet/sheet.tsx +128 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +84 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.tsx +145 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +78 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/table/table.tsx +71 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +124 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +83 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +124 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.tsx +87 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.tsx +87 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +103 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.tsx +57 -0
- package/src/context/index.ts +3 -0
- package/src/context/provider.tsx +35 -0
- package/src/context/theme-context.tsx +81 -0
- package/src/context/toast-context.tsx +63 -0
- package/src/env.d.ts +2 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/use-biometric.ts +27 -0
- package/src/hooks/use-color-scheme.ts +10 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-countdown.ts +33 -0
- package/src/hooks/use-debounce.ts +18 -0
- package/src/hooks/use-disclosure.ts +14 -0
- package/src/hooks/use-haptics.ts +47 -0
- package/src/hooks/use-keyboard.ts +35 -0
- package/src/hooks/use-media-query.ts +27 -0
- package/src/hooks/use-press-animation.ts +45 -0
- package/src/hooks/use-previous.ts +14 -0
- package/src/hooks/use-scroll-header.ts +42 -0
- package/src/hooks/use-spring.ts +18 -0
- package/src/hooks/use-theme.ts +6 -0
- package/src/hooks/use-toast.ts +6 -0
- package/src/index.ts +53 -0
- package/src/lib/create-animated.tsx +25 -0
- package/src/lib/create-component.tsx +56 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/platform.ts +25 -0
- package/src/lib/types.ts +28 -0
- package/src/lib/utils.ts +35 -0
- package/src/lib/variants.ts +7 -0
- package/src/premium/ai/chat-bubble.tsx +58 -0
- package/src/premium/ai/typing-indicator.tsx +59 -0
- package/src/premium/charts/bar-chart.tsx +66 -0
- package/src/premium/charts/progress-ring.tsx +63 -0
- package/src/premium/glass/glass-bottom-sheet.tsx +50 -0
- package/src/premium/glass/glass-card.tsx +51 -0
- package/src/premium/glass/glass-header.tsx +61 -0
- package/src/premium/glass/glass-panel.tsx +32 -0
- package/src/premium/glass/glass-sidebar.tsx +56 -0
- package/src/premium/index.ts +44 -0
- package/src/premium/index2.ts +13 -0
- package/src/premium/index3.ts +1 -0
- package/src/premium/inputs/color-picker.tsx +92 -0
- package/src/premium/inputs/currency-input.tsx +50 -0
- package/src/premium/inputs/otp-input.tsx +92 -0
- package/src/premium/inputs/phone-input.tsx +58 -0
- package/src/premium/inputs/rating.tsx +51 -0
- package/src/premium/layout/carousel.tsx +57 -0
- package/src/premium/layout/floating-dock.tsx +63 -0
- package/src/premium/layout/masonry-grid.tsx +41 -0
- package/src/premium/layout/parallax-scroll.tsx +81 -0
- package/src/premium/magic/animated-number.tsx +104 -0
- package/src/premium/magic/bento-grid.tsx +55 -0
- package/src/premium/magic/border-beam.tsx +68 -0
- package/src/premium/magic/confetti.tsx +88 -0
- package/src/premium/magic/magic-card.tsx +65 -0
- package/src/premium/magic/meteors.tsx +95 -0
- package/src/premium/magic/ripple.tsx +70 -0
- package/src/premium/magic/shimmer.tsx +58 -0
- package/src/premium/magic/shiny-button.tsx +70 -0
- package/src/premium/mobile/biometric-button.tsx +82 -0
- package/src/premium/mobile/bottom-tab-bar.tsx +81 -0
- package/src/premium/mobile/fab.tsx +74 -0
- package/src/premium/mobile/haptic-pressable.tsx +53 -0
- package/src/premium/mobile/notification-badge.tsx +61 -0
- package/src/premium/mobile/pull-to-refresh.tsx +84 -0
- package/src/premium/mobile/scroll-header.tsx +57 -0
- package/src/premium/mobile/swipe-row.tsx +128 -0
- package/src/premium/mobile/swipeable-card-stack.tsx +121 -0
- package/src/premium/motion/blur-fade.tsx +51 -0
- package/src/premium/motion/fade-up.tsx +34 -0
- package/src/premium/motion/marquee.tsx +67 -0
- package/src/premium/motion/pulsating-button.tsx +95 -0
- package/src/premium/motion/slide-in.tsx +38 -0
- package/src/premium/motion/stagger-children.tsx +28 -0
- package/src/premium/motion/typing-text.tsx +55 -0
- package/src/premium/motion/word-pull-up.tsx +34 -0
- package/src/premium/onboarding/step-indicator.tsx +65 -0
- package/src/tokens/colors.ts +83 -0
- package/src/tokens/global.css +83 -0
- package/src/tokens/index.ts +10 -0
- package/src/tokens/layout.ts +121 -0
- package/src/tokens/motion.ts +94 -0
- package/src/tokens/themes/dark.ts +7 -0
- package/src/tokens/themes/default.ts +8 -0
- package/src/tokens/themes/ocean.ts +28 -0
- package/src/tokens/themes/rose.ts +29 -0
- package/src/tokens/typography.ts +127 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# cnnative-ui
|
|
2
|
+
|
|
3
|
+
The core component library source code for the **cnnative** ecosystem. Beautiful, accessible, and high-performance React Native and Expo component primitives styled with NativeWind.
|
|
4
|
+
|
|
5
|
+
This library is designed around the **copy-paste component model** popularized by shadcn/ui. You own the code of your components—it is written directly into your application directories rather than hidden deep inside `node_modules`.
|
|
6
|
+
|
|
7
|
+
## Component Features
|
|
8
|
+
|
|
9
|
+
* **Premium Micro-Animations**: Built-in support for fluid spring transitions using React Native Reanimated and Moti.
|
|
10
|
+
* **Vibrant Glassmorphism**: Stunning pre-configured frosted glass cards, panel layouts, and sheets.
|
|
11
|
+
* **Tailwind Styling**: Out-of-the-box support for NativeWind styling variables and Tailwind utility configurations.
|
|
12
|
+
* **Tactile Haptics**: Native-feeling haptic feedback integrated directly into interactive primitives (buttons, switches, dials).
|
|
13
|
+
|
|
14
|
+
## Peer Dependencies
|
|
15
|
+
|
|
16
|
+
To use components added by the `cnnative` CLI in your project, ensure the following peer dependencies are installed:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Using Expo
|
|
20
|
+
npx expo install react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-svg expo-haptics expo-blur expo-linear-gradient expo-local-authentication
|
|
21
|
+
|
|
22
|
+
# Standard utility libraries
|
|
23
|
+
npm install clsx tailwind-merge class-variance-authority lucide-react-native moti
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How It Works
|
|
27
|
+
|
|
28
|
+
1. Initialize your workspace: `npx cnnative init`
|
|
29
|
+
2. Copy components dynamically: `npx cnnative add button`
|
|
30
|
+
3. Customize the component code directly within your `components/ui/` folder!
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
package/babel.config.js
ADDED
package/jest.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: '@react-native/jest-preset',
|
|
4
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
|
5
|
+
moduleDirectories: ['node_modules', '<rootDir>/node_modules', '<rootDir>/../../node_modules'],
|
|
6
|
+
testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'],
|
|
7
|
+
transformIgnorePatterns: [
|
|
8
|
+
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|lucide-react-native|react-native-reanimated|react-native-gesture-handler|moti)'
|
|
9
|
+
],
|
|
10
|
+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
|
11
|
+
moduleNameMapper: {
|
|
12
|
+
'^@/(.*)$': '<rootDir>/src/$1'
|
|
13
|
+
},
|
|
14
|
+
collectCoverage: true,
|
|
15
|
+
coverageReporters: ['json', 'html', 'text'],
|
|
16
|
+
collectCoverageFrom: [
|
|
17
|
+
'src/**/*.{ts,tsx}',
|
|
18
|
+
'!src/**/*.d.ts',
|
|
19
|
+
'!src/**/index.ts',
|
|
20
|
+
'!src/tokens/**'
|
|
21
|
+
]
|
|
22
|
+
};
|
package/jest.init.js
ADDED
package/jest.setup.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import '@testing-library/jest-native/extend-expect';
|
|
2
|
+
|
|
3
|
+
// Mock react-native-gesture-handler
|
|
4
|
+
jest.mock('react-native-gesture-handler', () => {
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { View } = require('react-native');
|
|
7
|
+
return {
|
|
8
|
+
__esModule: true,
|
|
9
|
+
GestureHandlerRootView: ({ children, style, ...props }) => React.createElement(View, { ...props, style }, children),
|
|
10
|
+
Swipeable: ({ children }) => children,
|
|
11
|
+
DrawerLayout: ({ children }) => children,
|
|
12
|
+
State: {},
|
|
13
|
+
PanGestureHandler: ({ children }) => children,
|
|
14
|
+
TapGestureHandler: ({ children }) => children,
|
|
15
|
+
FlingGestureHandler: ({ children }) => children,
|
|
16
|
+
ForceTouchGestureHandler: ({ children }) => children,
|
|
17
|
+
LongPressGestureHandler: ({ children }) => children,
|
|
18
|
+
PinchGestureHandler: ({ children }) => children,
|
|
19
|
+
RotationGestureHandler: ({ children }) => children,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Mock react-native-reanimated
|
|
24
|
+
jest.mock('react-native-reanimated', () => {
|
|
25
|
+
const react = require('react');
|
|
26
|
+
const { View, Text } = require('react-native');
|
|
27
|
+
return {
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: {
|
|
30
|
+
createAnimatedComponent: (c) => c,
|
|
31
|
+
View: ({ children, style, ...props }) => react.createElement(View, { ...props, style }, children),
|
|
32
|
+
Text: ({ children, style, ...props }) => react.createElement(Text, { ...props, style }, children),
|
|
33
|
+
},
|
|
34
|
+
useAnimatedStyle: (fn) => fn(),
|
|
35
|
+
useSharedValue: (val) => ({ value: val }),
|
|
36
|
+
withRepeat: (val) => val,
|
|
37
|
+
withTiming: (val, config, cb) => {
|
|
38
|
+
if (cb) cb(true);
|
|
39
|
+
return val;
|
|
40
|
+
},
|
|
41
|
+
withSpring: (val, config, cb) => {
|
|
42
|
+
if (cb) cb(true);
|
|
43
|
+
return val;
|
|
44
|
+
},
|
|
45
|
+
withDelay: (delay, animation) => animation,
|
|
46
|
+
Easing: {
|
|
47
|
+
linear: (t) => t,
|
|
48
|
+
ease: (t) => t,
|
|
49
|
+
quad: (t) => t,
|
|
50
|
+
cubic: (t) => t,
|
|
51
|
+
poly: (n) => (t) => Math.pow(t, n),
|
|
52
|
+
sin: (t) => t,
|
|
53
|
+
circle: (t) => t,
|
|
54
|
+
exp: (t) => t,
|
|
55
|
+
elastic: (bounciness) => (t) => t,
|
|
56
|
+
back: (s) => (t) => t,
|
|
57
|
+
bounce: (t) => t,
|
|
58
|
+
bezier: (x1, y1, x2, y2) => (t) => t,
|
|
59
|
+
in: (easing) => easing,
|
|
60
|
+
out: (easing) => easing,
|
|
61
|
+
inOut: (easing) => easing,
|
|
62
|
+
},
|
|
63
|
+
interpolate: (value, inputRange, outputRange, extrapolate) => {
|
|
64
|
+
return outputRange[0];
|
|
65
|
+
},
|
|
66
|
+
Extrapolate: {
|
|
67
|
+
CLAMP: 'clamp',
|
|
68
|
+
IDENTITY: 'identity',
|
|
69
|
+
EXTEND: 'extend',
|
|
70
|
+
},
|
|
71
|
+
interpolateColor: (value, inputRange, outputRange) => {
|
|
72
|
+
return outputRange[0];
|
|
73
|
+
},
|
|
74
|
+
runOnJS: (fn) => fn,
|
|
75
|
+
runOnUI: (fn) => fn,
|
|
76
|
+
useAnimatedRef: () => react.createRef(),
|
|
77
|
+
measure: () => ({ x: 0, y: 0, width: 100, height: 100, pageX: 0, pageY: 0 }),
|
|
78
|
+
createAnimatedComponent: (c) => c,
|
|
79
|
+
useReducedMotion: () => false,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Mock expo-haptics
|
|
84
|
+
jest.mock('expo-haptics', () => ({
|
|
85
|
+
impactAsync: jest.fn(),
|
|
86
|
+
notificationAsync: jest.fn(),
|
|
87
|
+
ImpactFeedbackStyle: {
|
|
88
|
+
Light: 'light',
|
|
89
|
+
Medium: 'medium',
|
|
90
|
+
Heavy: 'heavy'
|
|
91
|
+
},
|
|
92
|
+
NotificationFeedbackType: {
|
|
93
|
+
Success: 'success',
|
|
94
|
+
Warning: 'warning',
|
|
95
|
+
Error: 'error'
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// Mock lucide-react-native
|
|
100
|
+
jest.mock('lucide-react-native', () => {
|
|
101
|
+
const React = require('react');
|
|
102
|
+
const { View } = require('react-native');
|
|
103
|
+
const createMockIcon = (name) => {
|
|
104
|
+
const Component = (props) => React.createElement(View, { ...props, testID: `lucide-${name}` });
|
|
105
|
+
Component.displayName = `MockIcon(${name})`;
|
|
106
|
+
return Component;
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
__esModule: true,
|
|
110
|
+
Check: createMockIcon('Check'),
|
|
111
|
+
RefreshCw: createMockIcon('RefreshCw'),
|
|
112
|
+
Fingerprint: createMockIcon('Fingerprint'),
|
|
113
|
+
ScanFace: createMockIcon('ScanFace'),
|
|
114
|
+
Star: createMockIcon('Star'),
|
|
115
|
+
ChevronDown: createMockIcon('ChevronDown'),
|
|
116
|
+
X: createMockIcon('X'),
|
|
117
|
+
ChevronLeft: createMockIcon('ChevronLeft'),
|
|
118
|
+
ChevronRight: createMockIcon('ChevronRight'),
|
|
119
|
+
MoreHorizontal: createMockIcon('MoreHorizontal'),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Mock expo-blur
|
|
124
|
+
jest.mock('expo-blur', () => {
|
|
125
|
+
const React = require('react');
|
|
126
|
+
const { View } = require('react-native');
|
|
127
|
+
return {
|
|
128
|
+
BlurView: (props) => React.createElement(View, { ...props, testID: 'expo-blur-view' })
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Mock react-native-safe-area-context
|
|
133
|
+
jest.mock('react-native-safe-area-context', () => {
|
|
134
|
+
const React = require('react');
|
|
135
|
+
const { View } = require('react-native');
|
|
136
|
+
return {
|
|
137
|
+
SafeAreaProvider: ({ children }) => React.createElement(View, {}, children),
|
|
138
|
+
SafeAreaView: ({ children }) => React.createElement(View, {}, children),
|
|
139
|
+
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Mock theme-context
|
|
144
|
+
jest.mock('./src/context/theme-context', () => {
|
|
145
|
+
return {
|
|
146
|
+
__esModule: true,
|
|
147
|
+
ThemeProvider: ({ children }) => children,
|
|
148
|
+
useThemeContext: () => ({
|
|
149
|
+
colorScheme: 'light',
|
|
150
|
+
setColorScheme: jest.fn(),
|
|
151
|
+
isDark: false,
|
|
152
|
+
themeName: 'default',
|
|
153
|
+
setThemeName: jest.fn(),
|
|
154
|
+
theme: {
|
|
155
|
+
background: 'hsl(0 0% 100%)',
|
|
156
|
+
foreground: 'hsl(222.2 84% 4.9%)',
|
|
157
|
+
card: 'hsl(0 0% 100%)',
|
|
158
|
+
cardForeground: 'hsl(222.2 84% 4.9%)',
|
|
159
|
+
popover: 'hsl(0 0% 100%)',
|
|
160
|
+
popoverForeground: 'hsl(222.2 84% 4.9%)',
|
|
161
|
+
primary: { DEFAULT: 'hsl(222.2 47.4% 11.2%)', foreground: 'hsl(210 40% 98%)' },
|
|
162
|
+
secondary: { DEFAULT: 'hsl(210 40% 96.1%)', foreground: 'hsl(222.2 47.4% 11.2%)' },
|
|
163
|
+
muted: { DEFAULT: 'hsl(210 40% 96.1%)', foreground: 'hsl(215.4 16.3% 46.9%)' },
|
|
164
|
+
accent: { DEFAULT: 'hsl(210 40% 96.1%)', foreground: 'hsl(222.2 47.4% 11.2%)' },
|
|
165
|
+
destructive: { DEFAULT: 'hsl(0 84.2% 60.2%)', foreground: 'hsl(210 40% 98%)' },
|
|
166
|
+
border: 'hsl(214.3 31.8% 91.4%)',
|
|
167
|
+
input: 'hsl(214.3 31.8% 91.4%)',
|
|
168
|
+
ring: 'hsl(222.2 84% 4.9%)',
|
|
169
|
+
shimmer: '#E2E8F0',
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cnnative-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A shadcn/ui + Radix-inspired React Native component library that surpasses both",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"expo",
|
|
8
|
+
"ui",
|
|
9
|
+
"components",
|
|
10
|
+
"shadcn",
|
|
11
|
+
"nativecn"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"main": "./src/index.ts",
|
|
15
|
+
"types": "./src/index.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.ts",
|
|
18
|
+
"./tokens": "./src/tokens/index.ts",
|
|
19
|
+
"./lib": "./src/lib/index.ts",
|
|
20
|
+
"./hooks": "./src/hooks/index.ts",
|
|
21
|
+
"./components/*": "./src/components/*/index.ts",
|
|
22
|
+
"./premium/*": "./src/premium/*/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc --noEmit",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"test:coverage": "jest --coverage",
|
|
30
|
+
"test:watch": "jest --watch",
|
|
31
|
+
"clean": "rimraf dist coverage"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"expo": ">=52.0.0",
|
|
35
|
+
"nativewind": ">=5.0.0",
|
|
36
|
+
"react": ">=18.0.0",
|
|
37
|
+
"react-native": ">=0.76.0",
|
|
38
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
39
|
+
"react-native-reanimated": ">=3.0.0",
|
|
40
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"expo": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"react-native-gesture-handler": {
|
|
47
|
+
"optional": false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"class-variance-authority": "^0.7.1",
|
|
52
|
+
"clsx": "^2.1.1",
|
|
53
|
+
"lucide-react-native": "^0.469.0",
|
|
54
|
+
"moti": "^0.29.0",
|
|
55
|
+
"tailwind-merge": "^2.6.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@react-native/jest-preset": "^0.85.3",
|
|
59
|
+
"@testing-library/jest-native": "^5.4.3",
|
|
60
|
+
"@testing-library/react-native": "^13.2.0",
|
|
61
|
+
"@testing-library/user-event": "^14.5.2",
|
|
62
|
+
"@types/jest": "^30.0.0",
|
|
63
|
+
"@types/react": "^19.0.8",
|
|
64
|
+
"@types/react-native": "^0.73.0",
|
|
65
|
+
"babel-jest": "^29.7.0",
|
|
66
|
+
"babel-preset-expo": "^56.0.14",
|
|
67
|
+
"expo-blur": "^56.0.3",
|
|
68
|
+
"expo-haptics": "^56.0.3",
|
|
69
|
+
"expo-linear-gradient": "^56.0.4",
|
|
70
|
+
"expo-local-authentication": "^56.0.4",
|
|
71
|
+
"jest": "^29.7.0",
|
|
72
|
+
"jest-expo": "^52.0.0",
|
|
73
|
+
"jest-html-reporters": "^3.1.7",
|
|
74
|
+
"memfs": "^4.14.1",
|
|
75
|
+
"nativewind": "^4.2.4",
|
|
76
|
+
"react": "19.2.6",
|
|
77
|
+
"react-native": "^0.85.3",
|
|
78
|
+
"react-native-gesture-handler": "^3.0.0",
|
|
79
|
+
"react-native-reanimated": "^4.4.0",
|
|
80
|
+
"react-native-safe-area-context": "^5.8.0",
|
|
81
|
+
"react-native-svg": "^15.15.5",
|
|
82
|
+
"react-test-renderer": "19.2.6",
|
|
83
|
+
"rimraf": "^6.0.1",
|
|
84
|
+
"tsd": "^0.31.2",
|
|
85
|
+
"typescript": "^5.7.3"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import { Button } from '../../components/button/button';
|
|
4
|
+
import { Text } from '../../components/typography';
|
|
5
|
+
import { Switch } from '../../components/switch/switch';
|
|
6
|
+
import { Checkbox } from '../../components/checkbox/checkbox';
|
|
7
|
+
|
|
8
|
+
describe('Accessibility Standards', () => {
|
|
9
|
+
it('Button should be accessible as a button role', () => {
|
|
10
|
+
const { getByRole } = render(
|
|
11
|
+
<Button accessibilityRole="button">
|
|
12
|
+
<Text>Accessible Button</Text>
|
|
13
|
+
</Button>
|
|
14
|
+
);
|
|
15
|
+
expect(getByRole('button')).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('Switch should reflect checked accessibility state', () => {
|
|
19
|
+
const { getByRole } = render(
|
|
20
|
+
<Switch checked={true} accessibilityRole="switch" accessibilityState={{ checked: true }} />
|
|
21
|
+
);
|
|
22
|
+
const switchElement = getByRole('switch');
|
|
23
|
+
expect(switchElement.props.accessibilityState.checked).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('Checkbox should reflect checked accessibility state', () => {
|
|
27
|
+
const { getByRole } = render(
|
|
28
|
+
<Checkbox checked={true} accessibilityRole="checkbox" accessibilityState={{ checked: true }} />
|
|
29
|
+
);
|
|
30
|
+
const checkboxElement = getByRole('checkbox');
|
|
31
|
+
expect(checkboxElement.props.accessibilityState.checked).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import { Badge } from '../../components/badge/badge';
|
|
4
|
+
|
|
5
|
+
describe('Badge Component', () => {
|
|
6
|
+
it('renders correctly with default variant', () => {
|
|
7
|
+
const { getByText } = render(<Badge>Default Badge</Badge>);
|
|
8
|
+
expect(getByText('Default Badge')).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders correctly with outline variant', () => {
|
|
12
|
+
const { getByText, getByTestId } = render(
|
|
13
|
+
<Badge variant="outline" testID="badge-outline">
|
|
14
|
+
Outline Badge
|
|
15
|
+
</Badge>
|
|
16
|
+
);
|
|
17
|
+
expect(getByText('Outline Badge')).toBeTruthy();
|
|
18
|
+
expect(getByTestId('badge-outline')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders correctly with destructive variant', () => {
|
|
22
|
+
const { getByText } = render(<Badge variant="destructive">Destructive Badge</Badge>);
|
|
23
|
+
expect(getByText('Destructive Badge')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { Button } from '../../components/button/button';
|
|
4
|
+
import { Text } from '../../components/typography';
|
|
5
|
+
|
|
6
|
+
// We wrap the tests with a basic setup
|
|
7
|
+
describe('Button Component', () => {
|
|
8
|
+
it('renders correctly with children', () => {
|
|
9
|
+
const { getByText } = render(
|
|
10
|
+
<Button>
|
|
11
|
+
<Text>Click Me</Text>
|
|
12
|
+
</Button>
|
|
13
|
+
);
|
|
14
|
+
expect(getByText('Click Me')).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('calls onPress when pressed', () => {
|
|
18
|
+
const onPressMock = jest.fn();
|
|
19
|
+
const { getByText } = render(
|
|
20
|
+
<Button onPress={onPressMock}>
|
|
21
|
+
<Text>Pressable</Text>
|
|
22
|
+
</Button>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
fireEvent.press(getByText('Pressable'));
|
|
26
|
+
expect(onPressMock).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not call onPress when disabled', () => {
|
|
30
|
+
const onPressMock = jest.fn();
|
|
31
|
+
const { getByText } = render(
|
|
32
|
+
<Button onPress={onPressMock} disabled>
|
|
33
|
+
<Text>Disabled</Text>
|
|
34
|
+
</Button>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
fireEvent.press(getByText('Disabled'));
|
|
38
|
+
expect(onPressMock).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('applies variant classes correctly', () => {
|
|
42
|
+
const { getByTestId } = render(
|
|
43
|
+
<Button variant="destructive" testID="my-button">
|
|
44
|
+
<Text>Destructive</Text>
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// We expect the native element to have the destruct style strings in its className or style
|
|
49
|
+
// In React Native testing with nativewind, checking exact styles is complex without a full provider,
|
|
50
|
+
// but we can check if it renders without throwing.
|
|
51
|
+
expect(getByTestId('my-button')).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../components/card/card';
|
|
4
|
+
import { Text } from '../../components/typography';
|
|
5
|
+
|
|
6
|
+
describe('Card Component', () => {
|
|
7
|
+
it('renders all card subcomponents correctly', () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<Card>
|
|
10
|
+
<CardHeader>
|
|
11
|
+
<CardTitle>Card Title</CardTitle>
|
|
12
|
+
<CardDescription>Card Description</CardDescription>
|
|
13
|
+
</CardHeader>
|
|
14
|
+
<CardContent>
|
|
15
|
+
<Text>Card Content</Text>
|
|
16
|
+
</CardContent>
|
|
17
|
+
<CardFooter>
|
|
18
|
+
<Text>Card Footer</Text>
|
|
19
|
+
</CardFooter>
|
|
20
|
+
</Card>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(getByText('Card Title')).toBeTruthy();
|
|
24
|
+
expect(getByText('Card Description')).toBeTruthy();
|
|
25
|
+
expect(getByText('Card Content')).toBeTruthy();
|
|
26
|
+
expect(getByText('Card Footer')).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { Input } from '../../components/input/input';
|
|
4
|
+
|
|
5
|
+
describe('Input Component', () => {
|
|
6
|
+
it('renders correctly', () => {
|
|
7
|
+
const { getByPlaceholderText } = render(
|
|
8
|
+
<Input placeholder="Enter email" />
|
|
9
|
+
);
|
|
10
|
+
expect(getByPlaceholderText('Enter email')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('handles text changes', () => {
|
|
14
|
+
const onChangeTextMock = jest.fn();
|
|
15
|
+
const { getByPlaceholderText } = render(
|
|
16
|
+
<Input placeholder="Type here" onChangeText={onChangeTextMock} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const input = getByPlaceholderText('Type here');
|
|
20
|
+
fireEvent.changeText(input, 'Hello Nativecn');
|
|
21
|
+
|
|
22
|
+
expect(onChangeTextMock).toHaveBeenCalledWith('Hello Nativecn');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('passes generic props correctly', () => {
|
|
26
|
+
const { getByDisplayValue } = render(
|
|
27
|
+
<Input value="test value" secureTextEntry={true} />
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const input = getByDisplayValue('test value');
|
|
31
|
+
expect(input.props.secureTextEntry).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
2
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
3
|
+
|
|
4
|
+
describe('useControllableState', () => {
|
|
5
|
+
it('should use defaultProp when no prop is provided', () => {
|
|
6
|
+
const { result } = renderHook(() =>
|
|
7
|
+
useControllableState({ defaultProp: 'default' })
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
expect(result.current[0]).toBe('default');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should use prop when provided', () => {
|
|
14
|
+
const { result } = renderHook(() =>
|
|
15
|
+
useControllableState({ prop: 'controlled', defaultProp: 'default' })
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(result.current[0]).toBe('controlled');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should update state internally when uncontrolled', () => {
|
|
22
|
+
const { result } = renderHook(() =>
|
|
23
|
+
useControllableState({ defaultProp: 'default' })
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current[1]('new value');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.current[0]).toBe('new value');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should NOT update state internally when controlled', () => {
|
|
34
|
+
const { result } = renderHook(() =>
|
|
35
|
+
useControllableState({ prop: 'controlled', defaultProp: 'default' })
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
result.current[1]('new value');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Still 'controlled' because the parent didn't change the prop
|
|
43
|
+
expect(result.current[0]).toBe('controlled');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should call onChange with new value', () => {
|
|
47
|
+
const onChange = jest.fn();
|
|
48
|
+
const { result } = renderHook(() =>
|
|
49
|
+
useControllableState({ defaultProp: 'default', onChange })
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
result.current[1]('new value');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(onChange).toHaveBeenCalledWith('new value');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { NativecnProvider } from '../context/provider';
|
|
4
|
+
import { Button } from '../components/button/button';
|
|
5
|
+
import { Text } from '../components/typography';
|
|
6
|
+
import { ToastProvider, useToastContext } from '../context/toast-context';
|
|
7
|
+
|
|
8
|
+
const TestComponent = () => {
|
|
9
|
+
const { toast } = useToastContext();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Button onPress={() => toast({ title: 'Success', description: 'Integration works' })}>
|
|
13
|
+
<Text>Trigger Toast</Text>
|
|
14
|
+
</Button>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('Integration Tests', () => {
|
|
19
|
+
it('should render components within the global provider and trigger context actions', () => {
|
|
20
|
+
// In React Native Testing Library, rendering the provider tree evaluates integration
|
|
21
|
+
const { getByText } = render(
|
|
22
|
+
<NativecnProvider>
|
|
23
|
+
<TestComponent />
|
|
24
|
+
</NativecnProvider>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const btn = getByText('Trigger Toast');
|
|
28
|
+
expect(btn).toBeTruthy();
|
|
29
|
+
|
|
30
|
+
// Fire the button, which triggers the toast context.
|
|
31
|
+
// The visual toast won't appear instantaneously in the same tick without advanceTimers,
|
|
32
|
+
// but the context function will be executed without throwing.
|
|
33
|
+
fireEvent.press(btn);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
|
|
3
|
+
describe('utils', () => {
|
|
4
|
+
describe('cn', () => {
|
|
5
|
+
it('should merge class names correctly', () => {
|
|
6
|
+
expect(cn('bg-red-500', 'text-white')).toBe('bg-red-500 text-white');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should handle conditional classes', () => {
|
|
10
|
+
expect(cn('bg-red-500', true && 'text-white', false && 'p-4')).toBe('bg-red-500 text-white');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should resolve tailwind conflicts using tailwind-merge', () => {
|
|
14
|
+
// tailwind-merge resolves conflicts by keeping the last class
|
|
15
|
+
expect(cn('bg-red-500 bg-blue-500')).toBe('bg-blue-500');
|
|
16
|
+
expect(cn('p-2 p-4')).toBe('p-4');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle arrays of classes', () => {
|
|
20
|
+
expect(cn(['bg-red-500', 'text-white'])).toBe('bg-red-500 text-white');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Mock definitions for integration testing environments
|
|
2
|
+
export const mockHaptics = {
|
|
3
|
+
impactAsync: jest.fn(),
|
|
4
|
+
notificationAsync: jest.fn(),
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const mockReanimated = {
|
|
8
|
+
useSharedValue: jest.fn((init) => ({ value: init })),
|
|
9
|
+
useAnimatedStyle: jest.fn(() => ({})),
|
|
10
|
+
withSpring: jest.fn((val) => val),
|
|
11
|
+
withTiming: jest.fn((val) => val),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const mockThemeContext = {
|
|
15
|
+
theme: 'light',
|
|
16
|
+
isDark: false,
|
|
17
|
+
setColorScheme: jest.fn(),
|
|
18
|
+
toggleTheme: jest.fn(),
|
|
19
|
+
};
|