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,103 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Modal, type ViewProps, type LayoutRectangle } from 'react-native';
|
|
3
|
+
import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated';
|
|
4
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { Text } from '../typography';
|
|
7
|
+
|
|
8
|
+
const TooltipContext = React.createContext<{
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
triggerLayout: LayoutRectangle | null;
|
|
12
|
+
setTriggerLayout: (layout: LayoutRectangle | null) => void;
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
export const TooltipProvider = ({ children }: { children: React.ReactNode }) => {
|
|
16
|
+
return <>{children}</>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Tooltip = ({ open: openProp, defaultOpen, onOpenChange, children, delayDuration = 300 }: any) => {
|
|
20
|
+
const [open, setOpen] = useControllableState({
|
|
21
|
+
prop: openProp,
|
|
22
|
+
defaultProp: defaultOpen || false,
|
|
23
|
+
onChange: onOpenChange,
|
|
24
|
+
});
|
|
25
|
+
const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<TooltipContext.Provider value={{ open, onOpenChange: setOpen, triggerLayout, setTriggerLayout }}>
|
|
29
|
+
{children}
|
|
30
|
+
</TooltipContext.Provider>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const TooltipTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
35
|
+
({ children, ...props }, ref) => {
|
|
36
|
+
const context = React.useContext(TooltipContext);
|
|
37
|
+
if (!context) throw new Error('TooltipTrigger must be used within Tooltip');
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
ref={ref}
|
|
42
|
+
collapsable={false}
|
|
43
|
+
onLayout={(e) => {
|
|
44
|
+
(e.target as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
|
|
45
|
+
context.setTriggerLayout({ x: pageX, y: pageY, width, height });
|
|
46
|
+
});
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Pressable
|
|
50
|
+
onPress={() => context.onOpenChange(true)}
|
|
51
|
+
onLongPress={() => context.onOpenChange(true)}
|
|
52
|
+
delayLongPress={200}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</Pressable>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
TooltipTrigger.displayName = 'TooltipTrigger';
|
|
62
|
+
|
|
63
|
+
export const TooltipContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
64
|
+
({ className, children, ...props }, ref) => {
|
|
65
|
+
const context = React.useContext(TooltipContext);
|
|
66
|
+
if (!context) throw new Error('TooltipContent must be used within Tooltip');
|
|
67
|
+
|
|
68
|
+
if (!context.triggerLayout) return null;
|
|
69
|
+
|
|
70
|
+
const { x, y, width } = context.triggerLayout;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Modal
|
|
74
|
+
visible={context.open}
|
|
75
|
+
transparent
|
|
76
|
+
animationType="none"
|
|
77
|
+
onRequestClose={() => context.onOpenChange(false)}
|
|
78
|
+
>
|
|
79
|
+
<View className="flex-1">
|
|
80
|
+
<Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
|
|
81
|
+
<Animated.View
|
|
82
|
+
entering={ZoomIn.duration(200)}
|
|
83
|
+
exiting={FadeOut.duration(150)}
|
|
84
|
+
style={{
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
top: y - 40, // Simple offset for now
|
|
87
|
+
left: x + width / 2,
|
|
88
|
+
transform: [{ translateX: '-50%' }],
|
|
89
|
+
}}
|
|
90
|
+
className={cn(
|
|
91
|
+
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md',
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
<Text className="text-xs font-medium text-popover-foreground">{children as any}</Text>
|
|
97
|
+
</Animated.View>
|
|
98
|
+
</View>
|
|
99
|
+
</Modal>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
TooltipContent.displayName = 'TooltipContent';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './typography';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Text as RNText } from 'react-native';
|
|
2
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
3
|
+
import { createComponent } from '../../lib/create-component';
|
|
4
|
+
|
|
5
|
+
const typographyVariants = cva('text-foreground', {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'text-base font-normal',
|
|
9
|
+
h1: 'text-4xl font-extrabold tracking-tight lg:text-5xl',
|
|
10
|
+
h2: 'text-3xl font-semibold tracking-tight first:mt-0',
|
|
11
|
+
h3: 'text-2xl font-semibold tracking-tight',
|
|
12
|
+
h4: 'text-xl font-semibold tracking-tight',
|
|
13
|
+
p: 'text-base leading-7',
|
|
14
|
+
blockquote: 'mt-6 border-l-2 pl-6 italic text-muted-foreground',
|
|
15
|
+
code: 'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold',
|
|
16
|
+
lead: 'text-xl text-muted-foreground',
|
|
17
|
+
large: 'text-lg font-semibold',
|
|
18
|
+
small: 'text-sm font-medium leading-none',
|
|
19
|
+
muted: 'text-sm text-muted-foreground',
|
|
20
|
+
},
|
|
21
|
+
align: {
|
|
22
|
+
left: 'text-left',
|
|
23
|
+
center: 'text-center',
|
|
24
|
+
right: 'text-right',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
variant: 'default',
|
|
29
|
+
align: 'left',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type TextProps = React.ComponentPropsWithoutRef<typeof RNText> &
|
|
34
|
+
VariantProps<typeof typographyVariants> & {
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Standardized typography component based on shadcn/ui text styles.
|
|
40
|
+
*/
|
|
41
|
+
export const Text = createComponent<RNText, TextProps>({
|
|
42
|
+
Component: RNText,
|
|
43
|
+
variants: typographyVariants,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Semantic exports for easier usage
|
|
47
|
+
export const H1 = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'h1' }) });
|
|
48
|
+
export const H2 = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'h2' }) });
|
|
49
|
+
export const H3 = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'h3' }) });
|
|
50
|
+
export const H4 = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'h4' }) });
|
|
51
|
+
export const P = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'p' }) });
|
|
52
|
+
export const BlockQuote = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'blockquote' }) });
|
|
53
|
+
export const Code = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'code' }) });
|
|
54
|
+
export const Lead = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'lead' }) });
|
|
55
|
+
export const Large = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'large' }) });
|
|
56
|
+
export const Small = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'small' }) });
|
|
57
|
+
export const Muted = createComponent<RNText, TextProps>({ Component: RNText, variants: () => typographyVariants({ variant: 'muted' }) });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ThemeProvider, type ThemeProviderProps } from './theme-context';
|
|
3
|
+
import { ToastProvider } from './toast-context';
|
|
4
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
5
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
6
|
+
|
|
7
|
+
export interface NativecnProviderProps extends ThemeProviderProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The root provider for the Nativecn library.
|
|
13
|
+
* Wraps your application with all required contexts (Theme, Toast, SafeArea, GestureHandler).
|
|
14
|
+
*/
|
|
15
|
+
export const NativecnProvider: React.FC<NativecnProviderProps> = ({
|
|
16
|
+
children,
|
|
17
|
+
defaultColorScheme,
|
|
18
|
+
defaultThemeName,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
22
|
+
<SafeAreaProvider>
|
|
23
|
+
<ThemeProvider
|
|
24
|
+
defaultColorScheme={defaultColorScheme}
|
|
25
|
+
defaultThemeName={defaultThemeName}
|
|
26
|
+
>
|
|
27
|
+
<ToastProvider>
|
|
28
|
+
{/* The ToastRenderer would go here to automatically render toasts */}
|
|
29
|
+
{children}
|
|
30
|
+
</ToastProvider>
|
|
31
|
+
</ThemeProvider>
|
|
32
|
+
</SafeAreaProvider>
|
|
33
|
+
</GestureHandlerRootView>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useColorScheme as useNativeColorScheme } from 'react-native';
|
|
3
|
+
import { defaultTheme } from '../tokens/themes/default';
|
|
4
|
+
import { darkTheme } from '../tokens/themes/dark';
|
|
5
|
+
import type { SemanticColors } from '../tokens/colors';
|
|
6
|
+
|
|
7
|
+
export type ColorScheme = 'light' | 'dark' | 'system';
|
|
8
|
+
export type ThemeName = 'default' | 'ocean' | 'rose'; // Extend as we add more themes
|
|
9
|
+
|
|
10
|
+
export interface ThemeContextType {
|
|
11
|
+
colorScheme: ColorScheme;
|
|
12
|
+
setColorScheme: (scheme: ColorScheme) => void;
|
|
13
|
+
isDark: boolean;
|
|
14
|
+
themeName: ThemeName;
|
|
15
|
+
setThemeName: (name: ThemeName) => void;
|
|
16
|
+
theme: SemanticColors;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
20
|
+
|
|
21
|
+
export interface ThemeProviderProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
defaultColorScheme?: ColorScheme;
|
|
24
|
+
defaultThemeName?: ThemeName;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Map theme names to their light/dark color definitions
|
|
28
|
+
const themes: Record<ThemeName, { light: SemanticColors; dark: SemanticColors }> = {
|
|
29
|
+
default: { light: defaultTheme, dark: darkTheme },
|
|
30
|
+
ocean: {
|
|
31
|
+
light: require('../tokens/themes/ocean').oceanThemeLight,
|
|
32
|
+
dark: require('../tokens/themes/ocean').oceanThemeDark,
|
|
33
|
+
},
|
|
34
|
+
rose: {
|
|
35
|
+
light: require('../tokens/themes/rose').roseThemeLight,
|
|
36
|
+
dark: require('../tokens/themes/rose').roseThemeDark,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
41
|
+
children,
|
|
42
|
+
defaultColorScheme = 'system',
|
|
43
|
+
defaultThemeName = 'default',
|
|
44
|
+
}) => {
|
|
45
|
+
const nativeColorScheme = useNativeColorScheme();
|
|
46
|
+
const [colorScheme, setColorScheme] = useState<ColorScheme>(defaultColorScheme);
|
|
47
|
+
const [themeName, setThemeName] = useState<ThemeName>(defaultThemeName);
|
|
48
|
+
|
|
49
|
+
const isDark =
|
|
50
|
+
colorScheme === 'system'
|
|
51
|
+
? nativeColorScheme === 'dark'
|
|
52
|
+
: colorScheme === 'dark';
|
|
53
|
+
|
|
54
|
+
const theme = isDark ? themes[themeName].dark : themes[themeName].light;
|
|
55
|
+
|
|
56
|
+
const value = useMemo(
|
|
57
|
+
() => ({
|
|
58
|
+
colorScheme,
|
|
59
|
+
setColorScheme,
|
|
60
|
+
isDark,
|
|
61
|
+
themeName,
|
|
62
|
+
setThemeName,
|
|
63
|
+
theme,
|
|
64
|
+
}),
|
|
65
|
+
[colorScheme, isDark, themeName, theme]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ThemeContext.Provider value={value}>
|
|
70
|
+
{children}
|
|
71
|
+
</ThemeContext.Provider>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const useThemeContext = () => {
|
|
76
|
+
const context = useContext(ThemeContext);
|
|
77
|
+
if (!context) {
|
|
78
|
+
throw new Error('useThemeContext must be used within a ThemeProvider');
|
|
79
|
+
}
|
|
80
|
+
return context;
|
|
81
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'destructive';
|
|
4
|
+
|
|
5
|
+
export interface ToastProps {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
type?: ToastType;
|
|
10
|
+
duration?: number;
|
|
11
|
+
action?: {
|
|
12
|
+
label: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToastContextType {
|
|
18
|
+
toasts: ToastProps[];
|
|
19
|
+
toast: (props: Omit<ToastProps, 'id'>) => string;
|
|
20
|
+
dismiss: (id: string) => void;
|
|
21
|
+
clearAll: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
27
|
+
const [toasts, setToasts] = useState<ToastProps[]>([]);
|
|
28
|
+
|
|
29
|
+
const toast = useCallback((props: Omit<ToastProps, 'id'>) => {
|
|
30
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
31
|
+
setToasts((prev) => [...prev, { ...props, id }]);
|
|
32
|
+
|
|
33
|
+
// Auto-dismiss
|
|
34
|
+
const duration = props.duration ?? 3000;
|
|
35
|
+
if (duration > 0) {
|
|
36
|
+
setTimeout(() => dismiss(id), duration);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return id;
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const dismiss = useCallback((id: string) => {
|
|
43
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const clearAll = useCallback(() => {
|
|
47
|
+
setToasts([]);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ToastContext.Provider value={{ toasts, toast, dismiss, clearAll }}>
|
|
52
|
+
{children}
|
|
53
|
+
</ToastContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const useToastContext = () => {
|
|
58
|
+
const context = useContext(ToastContext);
|
|
59
|
+
if (!context) {
|
|
60
|
+
throw new Error('useToastContext must be used within a ToastProvider');
|
|
61
|
+
}
|
|
62
|
+
return context;
|
|
63
|
+
};
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './use-controllable';
|
|
2
|
+
export * from './use-debounce';
|
|
3
|
+
export * from './use-previous';
|
|
4
|
+
export * from './use-disclosure';
|
|
5
|
+
export * from './use-theme';
|
|
6
|
+
export * from './use-toast';
|
|
7
|
+
export * from './use-spring';
|
|
8
|
+
export * from './use-haptics';
|
|
9
|
+
export * from './use-press-animation';
|
|
10
|
+
export * from './use-keyboard';
|
|
11
|
+
export * from './use-color-scheme';
|
|
12
|
+
export * from './use-media-query';
|
|
13
|
+
export * from './use-scroll-header';
|
|
14
|
+
export * from './use-countdown';
|
|
15
|
+
export * from './use-biometric';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stub biometric hook.
|
|
5
|
+
* In a real app this would use `@sbaiahmed1/react-native-biometrics` or `expo-local-authentication`.
|
|
6
|
+
*/
|
|
7
|
+
export function useBiometric() {
|
|
8
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
9
|
+
const [biometryType, setBiometryType] = useState<'FaceID' | 'TouchID' | 'Biometrics' | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// In actual implementation:
|
|
13
|
+
// LocalAuthentication.hasHardwareAsync()
|
|
14
|
+
// LocalAuthentication.supportedAuthenticationTypesAsync()
|
|
15
|
+
setIsSupported(true);
|
|
16
|
+
setBiometryType('FaceID');
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const authenticate = async (promptMessage: string = 'Authenticate to continue') => {
|
|
20
|
+
// Simulate auth
|
|
21
|
+
return new Promise<boolean>((resolve) => {
|
|
22
|
+
setTimeout(() => resolve(true), 1000);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return { isSupported, biometryType, authenticate };
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useColorScheme as useNativeColorScheme } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get the current system color scheme.
|
|
5
|
+
* This is native 'light' | 'dark', unaware of Nativecn's ThemeProvider.
|
|
6
|
+
* For most use cases, you should use `useTheme().isDark` instead.
|
|
7
|
+
*/
|
|
8
|
+
export function useColorScheme() {
|
|
9
|
+
return useNativeColorScheme() ?? 'light';
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface UseControllableStateParams<T> {
|
|
4
|
+
prop?: T;
|
|
5
|
+
defaultProp?: T;
|
|
6
|
+
onChange?: (state: T) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A hook that allows a component to be both controlled and uncontrolled.
|
|
11
|
+
*/
|
|
12
|
+
export function useControllableState<T>({
|
|
13
|
+
prop,
|
|
14
|
+
defaultProp,
|
|
15
|
+
onChange = () => {},
|
|
16
|
+
}: UseControllableStateParams<T>) {
|
|
17
|
+
const [uncontrolledProp, setUncontrolledProp] = useState<T | undefined>(defaultProp);
|
|
18
|
+
const isControlled = prop !== undefined;
|
|
19
|
+
const value = isControlled ? prop : uncontrolledProp;
|
|
20
|
+
|
|
21
|
+
const setValue = useCallback(
|
|
22
|
+
(nextValue: T | ((prev: T) => T)) => {
|
|
23
|
+
if (isControlled) {
|
|
24
|
+
const setter = nextValue as (prev: T) => T;
|
|
25
|
+
const value = typeof nextValue === 'function' ? setter(prop) : nextValue;
|
|
26
|
+
if (value !== prop) onChange(value);
|
|
27
|
+
} else {
|
|
28
|
+
setUncontrolledProp((prevValue) => {
|
|
29
|
+
const setter = nextValue as (prev: T | undefined) => T;
|
|
30
|
+
const value = typeof nextValue === 'function' ? setter(prevValue) : nextValue;
|
|
31
|
+
if (value !== prevValue) onChange(value);
|
|
32
|
+
return value;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[isControlled, prop, onChange]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return [value as T, setValue] as const;
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing a countdown timer (e.g. for OTP resend).
|
|
5
|
+
*/
|
|
6
|
+
export function useCountdown(initialSeconds: number) {
|
|
7
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
8
|
+
const [isActive, setIsActive] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
12
|
+
if (isActive && seconds > 0) {
|
|
13
|
+
interval = setInterval(() => {
|
|
14
|
+
setSeconds((seconds) => seconds - 1);
|
|
15
|
+
}, 1000);
|
|
16
|
+
} else if (seconds === 0) {
|
|
17
|
+
setIsActive(false);
|
|
18
|
+
if (interval) clearInterval(interval);
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
if (interval) clearInterval(interval);
|
|
22
|
+
};
|
|
23
|
+
}, [isActive, seconds]);
|
|
24
|
+
|
|
25
|
+
const start = useCallback(() => setIsActive(true), []);
|
|
26
|
+
const stop = useCallback(() => setIsActive(false), []);
|
|
27
|
+
const reset = useCallback(() => {
|
|
28
|
+
setSeconds(initialSeconds);
|
|
29
|
+
setIsActive(false);
|
|
30
|
+
}, [initialSeconds]);
|
|
31
|
+
|
|
32
|
+
return { seconds, isActive, start, stop, reset };
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to debounce a rapidly changing value.
|
|
5
|
+
*/
|
|
6
|
+
export function useDebounce<T>(value: T, delay?: number): T {
|
|
7
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
|
11
|
+
|
|
12
|
+
return () => {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
};
|
|
15
|
+
}, [value, delay]);
|
|
16
|
+
|
|
17
|
+
return debouncedValue;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manage open/close state (e.g. for dialogs, sheets, modals).
|
|
5
|
+
*/
|
|
6
|
+
export function useDisclosure(defaultIsOpen: boolean = false) {
|
|
7
|
+
const [isOpen, setIsOpen] = useState(defaultIsOpen);
|
|
8
|
+
|
|
9
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
10
|
+
const close = useCallback(() => setIsOpen(false), []);
|
|
11
|
+
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
|
|
12
|
+
|
|
13
|
+
return { isOpen, open, close, toggle, setIsOpen };
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import * as Haptics from 'expo-haptics';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import type { HapticFeedbackType } from '../lib/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Triggers haptic feedback based on our semantic intent tokens.
|
|
8
|
+
* Fails silently on platforms without haptic support (e.g., Web).
|
|
9
|
+
*/
|
|
10
|
+
export function useHaptics() {
|
|
11
|
+
const trigger = useCallback((type: HapticFeedbackType = 'selection') => {
|
|
12
|
+
if (Platform.OS === 'web') return;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
switch (type) {
|
|
16
|
+
case 'light':
|
|
17
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
18
|
+
break;
|
|
19
|
+
case 'medium':
|
|
20
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
21
|
+
break;
|
|
22
|
+
case 'heavy':
|
|
23
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
|
24
|
+
break;
|
|
25
|
+
case 'selection':
|
|
26
|
+
Haptics.selectionAsync();
|
|
27
|
+
break;
|
|
28
|
+
case 'success':
|
|
29
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
30
|
+
break;
|
|
31
|
+
case 'warning':
|
|
32
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
|
33
|
+
break;
|
|
34
|
+
case 'error':
|
|
35
|
+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
36
|
+
break;
|
|
37
|
+
case 'none':
|
|
38
|
+
default:
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Ignore haptic errors on unsupported devices
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return trigger;
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Keyboard, Platform, KeyboardEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to track keyboard height and visibility.
|
|
6
|
+
*/
|
|
7
|
+
export function useKeyboard() {
|
|
8
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
9
|
+
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
13
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
14
|
+
|
|
15
|
+
const onKeyboardShow = (e: KeyboardEvent) => {
|
|
16
|
+
setKeyboardHeight(e.endCoordinates.height);
|
|
17
|
+
setIsKeyboardVisible(true);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const onKeyboardHide = () => {
|
|
21
|
+
setKeyboardHeight(0);
|
|
22
|
+
setIsKeyboardVisible(false);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const showSubscription = Keyboard.addListener(showEvent, onKeyboardShow);
|
|
26
|
+
const hideSubscription = Keyboard.addListener(hideEvent, onKeyboardHide);
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
showSubscription.remove();
|
|
30
|
+
hideSubscription.remove();
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return { keyboardHeight, isKeyboardVisible };
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useWindowDimensions } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export const breakpoints = {
|
|
4
|
+
sm: 640,
|
|
5
|
+
md: 768,
|
|
6
|
+
lg: 1024,
|
|
7
|
+
xl: 1280,
|
|
8
|
+
'2xl': 1536,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type Breakpoint = keyof typeof breakpoints;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook for responsive layout based on screen width.
|
|
15
|
+
*/
|
|
16
|
+
export function useMediaQuery() {
|
|
17
|
+
const { width } = useWindowDimensions();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
isSm: width >= breakpoints.sm,
|
|
21
|
+
isMd: width >= breakpoints.md,
|
|
22
|
+
isLg: width >= breakpoints.lg,
|
|
23
|
+
isXl: width >= breakpoints.xl,
|
|
24
|
+
is2Xl: width >= breakpoints['2xl'],
|
|
25
|
+
width,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
2
|
+
import { useSpring } from './use-spring';
|
|
3
|
+
import { pressScales, type PressScaleKey, type SpringPreset } from '../tokens/motion';
|
|
4
|
+
|
|
5
|
+
export interface UsePressAnimationProps {
|
|
6
|
+
scale?: PressScaleKey;
|
|
7
|
+
springConfig?: SpringPreset;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to handle scaling animations for pressable components (Buttons, Cards, etc.)
|
|
12
|
+
*/
|
|
13
|
+
export function usePressAnimation({
|
|
14
|
+
scale = 'normal',
|
|
15
|
+
springConfig = 'snappy',
|
|
16
|
+
}: UsePressAnimationProps = {}) {
|
|
17
|
+
const pressed = useSharedValue(0);
|
|
18
|
+
const spring = useSpring(springConfig);
|
|
19
|
+
const scaleTarget = pressScales[scale];
|
|
20
|
+
|
|
21
|
+
const handlePressIn = () => {
|
|
22
|
+
pressed.value = 1;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handlePressOut = () => {
|
|
26
|
+
pressed.value = 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
30
|
+
const currentScale = pressed.value === 1 ? scaleTarget : 1;
|
|
31
|
+
return {
|
|
32
|
+
transform: [
|
|
33
|
+
{
|
|
34
|
+
scale: withSpring(currentScale, spring),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
handlePressIn,
|
|
42
|
+
handlePressOut,
|
|
43
|
+
animatedStyle,
|
|
44
|
+
};
|
|
45
|
+
}
|