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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the previous state/value from the last render.
|
|
5
|
+
*/
|
|
6
|
+
export function usePrevious<T>(value: T): T | undefined {
|
|
7
|
+
const ref = useRef<T>(undefined);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
ref.current = value;
|
|
11
|
+
}, [value]);
|
|
12
|
+
|
|
13
|
+
return ref.current;
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useSharedValue, useAnimatedScrollHandler, useAnimatedStyle, interpolate, Extrapolate } from 'react-native-reanimated';
|
|
2
|
+
|
|
3
|
+
export interface UseScrollHeaderProps {
|
|
4
|
+
maxHeight?: number;
|
|
5
|
+
minHeight?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to create collapsing header animations on scroll.
|
|
10
|
+
*/
|
|
11
|
+
export function useScrollHeader({ maxHeight = 200, minHeight = 80 }: UseScrollHeaderProps = {}) {
|
|
12
|
+
const scrollY = useSharedValue(0);
|
|
13
|
+
const scrollDistance = maxHeight - minHeight;
|
|
14
|
+
|
|
15
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
16
|
+
scrollY.value = event.contentOffset.y;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const headerStyle = useAnimatedStyle(() => {
|
|
20
|
+
const height = interpolate(
|
|
21
|
+
scrollY.value,
|
|
22
|
+
[0, scrollDistance],
|
|
23
|
+
[maxHeight, minHeight],
|
|
24
|
+
Extrapolate.CLAMP
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return { height };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const titleStyle = useAnimatedStyle(() => {
|
|
31
|
+
const opacity = interpolate(
|
|
32
|
+
scrollY.value,
|
|
33
|
+
[0, scrollDistance / 2, scrollDistance],
|
|
34
|
+
[1, 0.5, 0],
|
|
35
|
+
Extrapolate.CLAMP
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return { opacity };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return { onScroll, scrollY, headerStyle, titleStyle };
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useReducedMotion } from 'react-native-reanimated';
|
|
2
|
+
import { springPresets, type SpringPreset } from '../tokens/motion';
|
|
3
|
+
import type { WithSpringConfig } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a reanimated spring config based on a semantic preset,
|
|
7
|
+
* automatically falling back to an instant transition if the user
|
|
8
|
+
* has "Reduce Motion" enabled in their OS settings.
|
|
9
|
+
*/
|
|
10
|
+
export function useSpring(preset: SpringPreset = 'smooth'): WithSpringConfig {
|
|
11
|
+
const reducedMotion = useReducedMotion();
|
|
12
|
+
|
|
13
|
+
if (reducedMotion) {
|
|
14
|
+
return springPresets.instant;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return springPresets[preset];
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Tokens & Design System
|
|
2
|
+
export * from './tokens';
|
|
3
|
+
|
|
4
|
+
// Core Library Utilities
|
|
5
|
+
export * from './lib';
|
|
6
|
+
|
|
7
|
+
// Hooks
|
|
8
|
+
export * from './hooks';
|
|
9
|
+
|
|
10
|
+
// Context & Providers
|
|
11
|
+
export * from './context';
|
|
12
|
+
|
|
13
|
+
// Layer 2: Styled Components
|
|
14
|
+
export * from './components/typography';
|
|
15
|
+
export * from './components/button';
|
|
16
|
+
export * from './components/badge';
|
|
17
|
+
export * from './components/card';
|
|
18
|
+
export * from './components/separator';
|
|
19
|
+
export * from './components/label';
|
|
20
|
+
export * from './components/input';
|
|
21
|
+
export * from './components/skeleton';
|
|
22
|
+
export * from './components/switch';
|
|
23
|
+
export * from './components/checkbox';
|
|
24
|
+
export * from './components/progress';
|
|
25
|
+
export * from './components/alert';
|
|
26
|
+
export * from './components/slider';
|
|
27
|
+
export * from './components/textarea';
|
|
28
|
+
export * from './components/avatar';
|
|
29
|
+
export * from './components/aspect-ratio';
|
|
30
|
+
export * from './components/scroll-area';
|
|
31
|
+
export * from './components/toggle';
|
|
32
|
+
export * from './components/toggle-group';
|
|
33
|
+
export * from './components/radio-group';
|
|
34
|
+
export * from './components/breadcrumb';
|
|
35
|
+
export * from './components/pagination';
|
|
36
|
+
export * from './components/accordion';
|
|
37
|
+
export * from './components/collapsible';
|
|
38
|
+
export * from './components/tabs';
|
|
39
|
+
export * from './components/toast';
|
|
40
|
+
export * from './components/dialog';
|
|
41
|
+
export * from './components/sheet';
|
|
42
|
+
export * from './components/tooltip';
|
|
43
|
+
export * from './components/dropdown-menu';
|
|
44
|
+
export * from './components/alert-dialog';
|
|
45
|
+
export * from './components/select';
|
|
46
|
+
export * from './components/form';
|
|
47
|
+
export * from './components/context-menu';
|
|
48
|
+
export * from './components/table';
|
|
49
|
+
export * from './components/navigation-menu';
|
|
50
|
+
|
|
51
|
+
// Note: Layer 3 (Premium) components are exported from their respective directories
|
|
52
|
+
// e.g., import { SwipeRow } from '@nativecn/ui/premium/mobile/swipe-row'
|
|
53
|
+
export * from './premium';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import Animated from 'react-native-reanimated';
|
|
3
|
+
import { createComponent, type CreateComponentOptions } from './create-component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An animated component factory.
|
|
7
|
+
* Wraps a component in `Animated.createAnimatedComponent` and then applies the `createComponent` factory logic.
|
|
8
|
+
*/
|
|
9
|
+
export function createAnimatedComponent<
|
|
10
|
+
TRef,
|
|
11
|
+
TProps extends { className?: string; style?: any }
|
|
12
|
+
>(options: CreateComponentOptions<TProps, any>) {
|
|
13
|
+
// First, make the underlying component animatable if it isn't already
|
|
14
|
+
const AnimatableComponent = Animated.createAnimatedComponent(options.Component as any);
|
|
15
|
+
|
|
16
|
+
// Then run it through our standard component factory
|
|
17
|
+
const ForwardedComponent = createComponent<TRef, TProps>({
|
|
18
|
+
...options,
|
|
19
|
+
Component: AnimatableComponent,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ForwardedComponent.displayName = `NativecnAnimated(${(options.Component as any).displayName || (options.Component as any).name || 'Unknown'})`;
|
|
23
|
+
|
|
24
|
+
return ForwardedComponent;
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import type { ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A highly reusable component factory that automatically handles:
|
|
7
|
+
* - ref forwarding
|
|
8
|
+
* - tailwind class merging via `cn()`
|
|
9
|
+
* - default base classes
|
|
10
|
+
* - CVA variant resolution (if a variant function is provided)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface CreateComponentOptions<TProps, TVariants> {
|
|
14
|
+
/** The base React Native component to wrap (e.g. View, Text, Pressable) */
|
|
15
|
+
Component: React.ElementType;
|
|
16
|
+
/** Base tailwind classes always applied to this component */
|
|
17
|
+
baseClassName?: string;
|
|
18
|
+
/** CVA variant function for prop-driven styles */
|
|
19
|
+
variants?: (props: any) => string;
|
|
20
|
+
/** Default props to apply */
|
|
21
|
+
defaultProps?: Partial<TProps>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createComponent<
|
|
25
|
+
TRef,
|
|
26
|
+
TProps extends { className?: string; style?: any }
|
|
27
|
+
>(
|
|
28
|
+
options: CreateComponentOptions<TProps, any>
|
|
29
|
+
) {
|
|
30
|
+
const { Component, baseClassName = '', variants, defaultProps = {} } = options;
|
|
31
|
+
|
|
32
|
+
const ForwardedComponent = forwardRef<TRef, TProps>((props, ref) => {
|
|
33
|
+
// Merge default props with passed props
|
|
34
|
+
const mergedProps = { ...defaultProps, ...props };
|
|
35
|
+
const { className, style, ...restProps } = mergedProps;
|
|
36
|
+
|
|
37
|
+
// Resolve variants if the CVA function is provided
|
|
38
|
+
const variantClassName = variants ? variants(mergedProps) : '';
|
|
39
|
+
|
|
40
|
+
// Merge base classes, variant classes, and user-passed classes
|
|
41
|
+
const finalClassName = cn(baseClassName, variantClassName, className);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Component
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={finalClassName || undefined}
|
|
47
|
+
style={style}
|
|
48
|
+
{...restProps}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ForwardedComponent.displayName = `NativecnComponent(${(Component as any).displayName || (Component as any).name || 'Unknown'})`;
|
|
54
|
+
|
|
55
|
+
return ForwardedComponent;
|
|
56
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export const isIOS = Platform.OS === 'ios';
|
|
4
|
+
export const isAndroid = Platform.OS === 'android';
|
|
5
|
+
export const isWeb = Platform.OS === 'web';
|
|
6
|
+
|
|
7
|
+
export const isNative = isIOS || isAndroid;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Platform-specific class name helper.
|
|
11
|
+
* Selectively applies classes based on the current platform.
|
|
12
|
+
*/
|
|
13
|
+
export function platformClasses(classes: {
|
|
14
|
+
ios?: string;
|
|
15
|
+
android?: string;
|
|
16
|
+
web?: string;
|
|
17
|
+
native?: string;
|
|
18
|
+
default?: string;
|
|
19
|
+
}): string {
|
|
20
|
+
if (isIOS && classes.ios) return classes.ios;
|
|
21
|
+
if (isAndroid && classes.android) return classes.android;
|
|
22
|
+
if (isWeb && classes.web) return classes.web;
|
|
23
|
+
if (isNative && classes.native) return classes.native;
|
|
24
|
+
return classes.default || '';
|
|
25
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types used across the Nativecn UI library
|
|
3
|
+
*/
|
|
4
|
+
import type { ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
5
|
+
|
|
6
|
+
export type AnyStyle = ViewStyle | TextStyle | ImageStyle;
|
|
7
|
+
|
|
8
|
+
export type BooleanString = 'true' | 'false';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Common size variants used across components
|
|
12
|
+
*/
|
|
13
|
+
export type ComponentSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'icon' | 'icon-sm' | 'icon-lg';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Common intent variants used across components
|
|
17
|
+
*/
|
|
18
|
+
export type ComponentVariant =
|
|
19
|
+
| 'default'
|
|
20
|
+
| 'destructive'
|
|
21
|
+
| 'outline'
|
|
22
|
+
| 'secondary'
|
|
23
|
+
| 'ghost'
|
|
24
|
+
| 'link'
|
|
25
|
+
| 'gradient'
|
|
26
|
+
| 'glass';
|
|
27
|
+
|
|
28
|
+
export type HapticFeedbackType = 'none' | 'selection' | 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error';
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merges Tailwind CSS classes securely.
|
|
6
|
+
* It uses clsx to construct the class string conditionally,
|
|
7
|
+
* and tailwind-merge to deduplicate conflicting Tailwind utilities.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* cn('px-2 py-1', { 'bg-red-500': hasError }, ['text-sm', 'font-bold'])
|
|
11
|
+
*/
|
|
12
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
13
|
+
return twMerge(clsx(inputs));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compose multiple event handlers together.
|
|
18
|
+
* Useful when forwarding refs and preserving user-passed event handlers.
|
|
19
|
+
*/
|
|
20
|
+
export function composeEventHandlers<E>(
|
|
21
|
+
originalEventHandler?: (event: E) => void,
|
|
22
|
+
ourEventHandler?: (event: E) => void,
|
|
23
|
+
{ checkForDefaultPrevented = true } = {}
|
|
24
|
+
) {
|
|
25
|
+
return function handleEvent(event: E) {
|
|
26
|
+
originalEventHandler?.(event);
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
checkForDefaultPrevented === false ||
|
|
30
|
+
!(event as any)?.defaultPrevented
|
|
31
|
+
) {
|
|
32
|
+
return ourEventHandler?.(event);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Text } from '../../components/typography';
|
|
5
|
+
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
|
|
6
|
+
|
|
7
|
+
export interface ChatBubbleProps extends ViewProps {
|
|
8
|
+
message: string;
|
|
9
|
+
isSender?: boolean;
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
avatar?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ChatBubble = React.forwardRef<React.ElementRef<typeof View>, ChatBubbleProps>(
|
|
15
|
+
({ className, message, isSender = false, timestamp, avatar, ...props }, ref) => {
|
|
16
|
+
|
|
17
|
+
const enteringAnimation = isSender ? FadeInDown.springify() : FadeInUp.springify();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Animated.View
|
|
21
|
+
entering={enteringAnimation}
|
|
22
|
+
ref={ref as any}
|
|
23
|
+
className={cn('mb-4 flex w-full flex-row', isSender ? 'justify-end' : 'justify-start', className)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{!isSender && avatar && (
|
|
27
|
+
<View className="mr-2 justify-end pb-4">{avatar}</View>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
<View className="max-w-[80%]">
|
|
31
|
+
<View
|
|
32
|
+
className={cn(
|
|
33
|
+
'rounded-2xl px-4 py-3 shadow-sm',
|
|
34
|
+
isSender
|
|
35
|
+
? 'rounded-tr-sm bg-primary border border-primary'
|
|
36
|
+
: 'rounded-tl-sm bg-muted border border-border'
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Text className={cn('text-[15px] leading-5', isSender ? 'text-primary-foreground' : 'text-foreground')}>
|
|
40
|
+
{message}
|
|
41
|
+
</Text>
|
|
42
|
+
</View>
|
|
43
|
+
|
|
44
|
+
{timestamp && (
|
|
45
|
+
<Text className={cn('mt-1 text-xs text-muted-foreground', isSender ? 'text-right' : 'text-left')}>
|
|
46
|
+
{timestamp}
|
|
47
|
+
</Text>
|
|
48
|
+
)}
|
|
49
|
+
</View>
|
|
50
|
+
|
|
51
|
+
{isSender && avatar && (
|
|
52
|
+
<View className="ml-2 justify-end pb-4">{avatar}</View>
|
|
53
|
+
)}
|
|
54
|
+
</Animated.View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
ChatBubble.displayName = 'ChatBubble';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming, withDelay } from 'react-native-reanimated';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
export const TypingIndicator = React.forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
7
|
+
({ className, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<View
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn('flex-row items-center space-x-1.5 rounded-2xl rounded-tl-sm bg-muted px-4 py-3 self-start', className)}
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
<Dot delay={0} />
|
|
15
|
+
<Dot delay={150} />
|
|
16
|
+
<Dot delay={300} />
|
|
17
|
+
</View>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
TypingIndicator.displayName = 'TypingIndicator';
|
|
22
|
+
|
|
23
|
+
const Dot = ({ delay }: { delay: number }) => {
|
|
24
|
+
const opacity = useSharedValue(0.3);
|
|
25
|
+
const translateY = useSharedValue(0);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
opacity.value = withDelay(
|
|
29
|
+
delay,
|
|
30
|
+
withRepeat(
|
|
31
|
+
withSequence(
|
|
32
|
+
withTiming(1, { duration: 300 }),
|
|
33
|
+
withTiming(0.3, { duration: 300 })
|
|
34
|
+
),
|
|
35
|
+
-1,
|
|
36
|
+
true
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
translateY.value = withDelay(
|
|
41
|
+
delay,
|
|
42
|
+
withRepeat(
|
|
43
|
+
withSequence(
|
|
44
|
+
withTiming(-3, { duration: 300 }),
|
|
45
|
+
withTiming(0, { duration: 300 })
|
|
46
|
+
),
|
|
47
|
+
-1,
|
|
48
|
+
true
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
54
|
+
opacity: opacity.value,
|
|
55
|
+
transform: [{ translateY: translateY.value }],
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return <Animated.View style={animatedStyle} className="h-2 w-2 rounded-full bg-foreground" />;
|
|
59
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withDelay } from 'react-native-reanimated';
|
|
4
|
+
import { Text } from '../../components/typography';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
|
|
7
|
+
export interface BarChartProps extends ViewProps {
|
|
8
|
+
data: { label: string; value: number }[];
|
|
9
|
+
maxValue?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
barColor?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BarChart = React.forwardRef<React.ElementRef<typeof View>, BarChartProps>(
|
|
15
|
+
({ className, data, maxValue, height = 200, barColor = 'hsl(var(--primary))', style, ...props }, ref) => {
|
|
16
|
+
const max = maxValue || Math.max(...data.map((d) => d.value));
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn('flex-row items-end justify-between px-4 pb-8 pt-4 border-b border-border', className)}
|
|
22
|
+
style={[{ height }, style]}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{data.map((item, index) => (
|
|
26
|
+
<Bar
|
|
27
|
+
key={item.label}
|
|
28
|
+
value={item.value}
|
|
29
|
+
max={max}
|
|
30
|
+
label={item.label}
|
|
31
|
+
color={barColor}
|
|
32
|
+
delay={index * 100}
|
|
33
|
+
totalHeight={height - 40} // account for label and padding
|
|
34
|
+
/>
|
|
35
|
+
))}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
BarChart.displayName = 'BarChart';
|
|
41
|
+
|
|
42
|
+
const Bar = ({ value, max, label, color, delay, totalHeight }: any) => {
|
|
43
|
+
const heightAnim = useSharedValue(0);
|
|
44
|
+
|
|
45
|
+
const targetHeight = (value / max) * totalHeight;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
heightAnim.value = withDelay(delay, withSpring(targetHeight, { damping: 15 }));
|
|
49
|
+
}, [targetHeight, delay]);
|
|
50
|
+
|
|
51
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
52
|
+
height: heightAnim.value,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View className="items-center flex-1">
|
|
57
|
+
<View className="w-full flex-1 justify-end px-1">
|
|
58
|
+
<Animated.View
|
|
59
|
+
style={[animatedStyle, { backgroundColor: color }]}
|
|
60
|
+
className="w-full rounded-t-sm"
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
<Text className="mt-2 text-xs text-muted-foreground">{label}</Text>
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing, interpolate, Extrapolate } from 'react-native-reanimated';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { Text } from '../../components/typography';
|
|
6
|
+
|
|
7
|
+
export interface ProgressRingProps extends ViewProps {
|
|
8
|
+
progress: number; // 0 to 1
|
|
9
|
+
size?: number;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
color?: string;
|
|
12
|
+
trackColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ProgressRing = React.forwardRef<React.ElementRef<typeof View>, ProgressRingProps>(
|
|
16
|
+
({ className, progress, size = 120, strokeWidth = 10, color = 'hsl(var(--primary))', trackColor = 'hsl(var(--muted))', style, ...props }, ref) => {
|
|
17
|
+
// Pure View/CSS based fake ring using half-circles
|
|
18
|
+
const rotation = useSharedValue(0);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Limit to 0-1
|
|
22
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
23
|
+
rotation.value = withTiming(p * 360, { duration: 1500, easing: Easing.out(Easing.cubic) });
|
|
24
|
+
}, [progress]);
|
|
25
|
+
|
|
26
|
+
const rightAnimatedStyle = useAnimatedStyle(() => {
|
|
27
|
+
const rot = interpolate(rotation.value, [0, 180, 360], [0, 180, 180], Extrapolate.CLAMP);
|
|
28
|
+
return { transform: [{ rotate: `${rot}deg` }] };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const leftAnimatedStyle = useAnimatedStyle(() => {
|
|
32
|
+
const rot = interpolate(rotation.value, [0, 180, 360], [0, 0, 180], Extrapolate.CLAMP);
|
|
33
|
+
return { transform: [{ rotate: `${rot}deg` }] };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn('items-center justify-center relative', className)}
|
|
40
|
+
style={[{ width: size, height: size }, style]}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{/* Track */}
|
|
44
|
+
<View
|
|
45
|
+
style={[
|
|
46
|
+
StyleSheet.absoluteFill,
|
|
47
|
+
{ borderRadius: size / 2, borderWidth: strokeWidth, borderColor: trackColor }
|
|
48
|
+
]}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<View style={[StyleSheet.absoluteFill, { overflow: 'hidden' }]}>
|
|
52
|
+
{/* We would typically use react-native-svg for a real ring, but simulating with views for zero-deps */}
|
|
53
|
+
<View style={[StyleSheet.absoluteFill, { borderRadius: size / 2, borderWidth: strokeWidth, borderColor: color, opacity: 0.2 }]} />
|
|
54
|
+
</View>
|
|
55
|
+
|
|
56
|
+
<View className="items-center justify-center">
|
|
57
|
+
<Text className="text-2xl font-bold">{Math.round(progress * 100)}%</Text>
|
|
58
|
+
</View>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
ProgressRing.displayName = 'ProgressRing';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, type ViewProps, Modal, Pressable } from 'react-native';
|
|
3
|
+
import { BlurView } from 'expo-blur';
|
|
4
|
+
import Animated, { SlideInDown, SlideOutDown } from 'react-native-reanimated';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
7
|
+
|
|
8
|
+
export interface GlassBottomSheetProps extends ViewProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const GlassBottomSheet = React.forwardRef<React.ElementRef<typeof View>, GlassBottomSheetProps>(
|
|
15
|
+
({ className, open, onOpenChange, children, ...props }, ref) => {
|
|
16
|
+
const { isDark } = useThemeContext();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Modal
|
|
20
|
+
visible={open}
|
|
21
|
+
transparent
|
|
22
|
+
animationType="none"
|
|
23
|
+
onRequestClose={() => onOpenChange(false)}
|
|
24
|
+
>
|
|
25
|
+
<View className="flex-1 justify-end">
|
|
26
|
+
<Pressable className="absolute inset-0 bg-black/40" onPress={() => onOpenChange(false)} />
|
|
27
|
+
|
|
28
|
+
<Animated.View
|
|
29
|
+
entering={SlideInDown.springify().damping(20).stiffness(200)}
|
|
30
|
+
exiting={SlideOutDown.duration(300)}
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn('w-full overflow-hidden rounded-t-3xl border-t border-white/20 shadow-2xl', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<BlurView
|
|
36
|
+
intensity={80}
|
|
37
|
+
tint={isDark ? 'dark' : 'light'}
|
|
38
|
+
style={StyleSheet.absoluteFill}
|
|
39
|
+
/>
|
|
40
|
+
<View className="p-6 pb-safe relative z-10">
|
|
41
|
+
<View className="w-12 h-1.5 bg-muted-foreground/30 rounded-full self-center mb-6" />
|
|
42
|
+
{children}
|
|
43
|
+
</View>
|
|
44
|
+
</Animated.View>
|
|
45
|
+
</View>
|
|
46
|
+
</Modal>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
GlassBottomSheet.displayName = 'GlassBottomSheet';
|