@campxdev/react-native-blueprint 0.1.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/LICENSE +20 -0
- package/README.md +358 -0
- package/lib/module/app/_layout.js +23 -0
- package/lib/module/app/_layout.js.map +1 -0
- package/lib/module/assets/icons/weather_icons/drizzle.png +0 -0
- package/lib/module/assets/icons/weather_icons/foggy.png +0 -0
- package/lib/module/assets/icons/weather_icons/freezing_rain.png +0 -0
- package/lib/module/assets/icons/weather_icons/partly_cloudy.png +0 -0
- package/lib/module/assets/icons/weather_icons/rainy.png +0 -0
- package/lib/module/assets/icons/weather_icons/showers.png +0 -0
- package/lib/module/assets/icons/weather_icons/sunny_weather.png +0 -0
- package/lib/module/assets/icons/weather_icons/thunderstorm.png +0 -0
- package/lib/module/assets/icons/weather_icons/thunderstorm_hail.png +0 -0
- package/lib/module/components/theme-config.js +265 -0
- package/lib/module/components/theme-config.js.map +1 -0
- package/lib/module/components/ui/Accordion.js +228 -0
- package/lib/module/components/ui/Accordion.js.map +1 -0
- package/lib/module/components/ui/Alert-Dialog.js +266 -0
- package/lib/module/components/ui/Alert-Dialog.js.map +1 -0
- package/lib/module/components/ui/Alert.js +107 -0
- package/lib/module/components/ui/Alert.js.map +1 -0
- package/lib/module/components/ui/AppBar.js +403 -0
- package/lib/module/components/ui/AppBar.js.map +1 -0
- package/lib/module/components/ui/Aspect-Ratio.js +27 -0
- package/lib/module/components/ui/Aspect-Ratio.js.map +1 -0
- package/lib/module/components/ui/Avatar.js +97 -0
- package/lib/module/components/ui/Avatar.js.map +1 -0
- package/lib/module/components/ui/Badge.js +127 -0
- package/lib/module/components/ui/Badge.js.map +1 -0
- package/lib/module/components/ui/Bottom-Sheet.js +144 -0
- package/lib/module/components/ui/Bottom-Sheet.js.map +1 -0
- package/lib/module/components/ui/Button.js +88 -0
- package/lib/module/components/ui/Button.js.map +1 -0
- package/lib/module/components/ui/Card.js +176 -0
- package/lib/module/components/ui/Card.js.map +1 -0
- package/lib/module/components/ui/Checkbox.js +65 -0
- package/lib/module/components/ui/Checkbox.js.map +1 -0
- package/lib/module/components/ui/Collapsible.js +42 -0
- package/lib/module/components/ui/Collapsible.js.map +1 -0
- package/lib/module/components/ui/Context-Menu.js +287 -0
- package/lib/module/components/ui/Context-Menu.js.map +1 -0
- package/lib/module/components/ui/Custom-Card.js +202 -0
- package/lib/module/components/ui/Custom-Card.js.map +1 -0
- package/lib/module/components/ui/Dialog.js +202 -0
- package/lib/module/components/ui/Dialog.js.map +1 -0
- package/lib/module/components/ui/Dropdown-Menu.js +421 -0
- package/lib/module/components/ui/Dropdown-Menu.js.map +1 -0
- package/lib/module/components/ui/Floating-Action.js +50 -0
- package/lib/module/components/ui/Floating-Action.js.map +1 -0
- package/lib/module/components/ui/Greeting-Card.js +392 -0
- package/lib/module/components/ui/Greeting-Card.js.map +1 -0
- package/lib/module/components/ui/Hover-Card.js +96 -0
- package/lib/module/components/ui/Hover-Card.js.map +1 -0
- package/lib/module/components/ui/Icon.js +73 -0
- package/lib/module/components/ui/Icon.js.map +1 -0
- package/lib/module/components/ui/Input.js +74 -0
- package/lib/module/components/ui/Input.js.map +1 -0
- package/lib/module/components/ui/Label.js +44 -0
- package/lib/module/components/ui/Label.js.map +1 -0
- package/lib/module/components/ui/Menubar.js +375 -0
- package/lib/module/components/ui/Menubar.js.map +1 -0
- package/lib/module/components/ui/Native-Only-Animated-View.js +41 -0
- package/lib/module/components/ui/Native-Only-Animated-View.js.map +1 -0
- package/lib/module/components/ui/NavBar.js +352 -0
- package/lib/module/components/ui/NavBar.js.map +1 -0
- package/lib/module/components/ui/Popover.js +101 -0
- package/lib/module/components/ui/Popover.js.map +1 -0
- package/lib/module/components/ui/Progress.js +124 -0
- package/lib/module/components/ui/Progress.js.map +1 -0
- package/lib/module/components/ui/Radio-Group.js +75 -0
- package/lib/module/components/ui/Radio-Group.js.map +1 -0
- package/lib/module/components/ui/Select.js +269 -0
- package/lib/module/components/ui/Select.js.map +1 -0
- package/lib/module/components/ui/Separator.js +58 -0
- package/lib/module/components/ui/Separator.js.map +1 -0
- package/lib/module/components/ui/SizedBox.js +101 -0
- package/lib/module/components/ui/SizedBox.js.map +1 -0
- package/lib/module/components/ui/Skeleton.js +57 -0
- package/lib/module/components/ui/Skeleton.js.map +1 -0
- package/lib/module/components/ui/Slider.js +169 -0
- package/lib/module/components/ui/Slider.js.map +1 -0
- package/lib/module/components/ui/Switch.js +55 -0
- package/lib/module/components/ui/Switch.js.map +1 -0
- package/lib/module/components/ui/Table.js +150 -0
- package/lib/module/components/ui/Table.js.map +1 -0
- package/lib/module/components/ui/Tabs.js +106 -0
- package/lib/module/components/ui/Tabs.js.map +1 -0
- package/lib/module/components/ui/Text.js +69 -0
- package/lib/module/components/ui/Text.js.map +1 -0
- package/lib/module/components/ui/Textarea.js +88 -0
- package/lib/module/components/ui/Textarea.js.map +1 -0
- package/lib/module/components/ui/Theme-Toggle.js +156 -0
- package/lib/module/components/ui/Theme-Toggle.js.map +1 -0
- package/lib/module/components/ui/Toast.js +101 -0
- package/lib/module/components/ui/Toast.js.map +1 -0
- package/lib/module/components/ui/Toggle-Group.js +129 -0
- package/lib/module/components/ui/Toggle-Group.js.map +1 -0
- package/lib/module/components/ui/Toggle.js +106 -0
- package/lib/module/components/ui/Toggle.js.map +1 -0
- package/lib/module/components/ui/Tooltip.js +106 -0
- package/lib/module/components/ui/Tooltip.js.map +1 -0
- package/lib/module/components/ui/index.js +45 -0
- package/lib/module/components/ui/index.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/lib/ThemeProvider.js +173 -0
- package/lib/module/lib/ThemeProvider.js.map +1 -0
- package/lib/module/lib/cornerRadius.js +164 -0
- package/lib/module/lib/cornerRadius.js.map +1 -0
- package/lib/module/lib/fonts.js +25 -0
- package/lib/module/lib/fonts.js.map +1 -0
- package/lib/module/lib/theme.js +212 -0
- package/lib/module/lib/theme.js.map +1 -0
- package/lib/module/lib/utils.js +137 -0
- package/lib/module/lib/utils.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/package.json +208 -0
- package/src/app/_layout.tsx +25 -0
- package/src/assets/icons/weather_icons/drizzle.png +0 -0
- package/src/assets/icons/weather_icons/foggy.png +0 -0
- package/src/assets/icons/weather_icons/freezing_rain.png +0 -0
- package/src/assets/icons/weather_icons/partly_cloudy.png +0 -0
- package/src/assets/icons/weather_icons/rainy.png +0 -0
- package/src/assets/icons/weather_icons/showers.png +0 -0
- package/src/assets/icons/weather_icons/sunny_weather.png +0 -0
- package/src/assets/icons/weather_icons/thunderstorm.png +0 -0
- package/src/assets/icons/weather_icons/thunderstorm_hail.png +0 -0
- package/src/components/theme-config.ts +331 -0
- package/src/components/ui/Accordion.tsx +253 -0
- package/src/components/ui/Alert-Dialog.tsx +295 -0
- package/src/components/ui/Alert.tsx +137 -0
- package/src/components/ui/AppBar.tsx +551 -0
- package/src/components/ui/Aspect-Ratio.tsx +25 -0
- package/src/components/ui/Avatar.tsx +103 -0
- package/src/components/ui/Badge.tsx +121 -0
- package/src/components/ui/Bottom-Sheet.tsx +224 -0
- package/src/components/ui/Button.tsx +100 -0
- package/src/components/ui/Card.tsx +185 -0
- package/src/components/ui/Checkbox.tsx +81 -0
- package/src/components/ui/Collapsible.tsx +40 -0
- package/src/components/ui/Context-Menu.tsx +407 -0
- package/src/components/ui/Custom-Card.tsx +226 -0
- package/src/components/ui/Dialog.tsx +240 -0
- package/src/components/ui/Dropdown-Menu.tsx +544 -0
- package/src/components/ui/Floating-Action.tsx +54 -0
- package/src/components/ui/Greeting-Card.tsx +471 -0
- package/src/components/ui/Hover-Card.tsx +101 -0
- package/src/components/ui/Icon.tsx +75 -0
- package/src/components/ui/Input.tsx +90 -0
- package/src/components/ui/Label.tsx +48 -0
- package/src/components/ui/Menubar.tsx +509 -0
- package/src/components/ui/Native-Only-Animated-View.tsx +37 -0
- package/src/components/ui/NavBar.tsx +397 -0
- package/src/components/ui/Popover.tsx +110 -0
- package/src/components/ui/Progress.tsx +138 -0
- package/src/components/ui/Radio-Group.tsx +79 -0
- package/src/components/ui/Select.tsx +344 -0
- package/src/components/ui/Separator.tsx +68 -0
- package/src/components/ui/SizedBox.tsx +116 -0
- package/src/components/ui/Skeleton.tsx +55 -0
- package/src/components/ui/Slider.tsx +222 -0
- package/src/components/ui/Switch.tsx +67 -0
- package/src/components/ui/Table.tsx +170 -0
- package/src/components/ui/Tabs.tsx +119 -0
- package/src/components/ui/Text.tsx +73 -0
- package/src/components/ui/Textarea.tsx +93 -0
- package/src/components/ui/Theme-Toggle.tsx +204 -0
- package/src/components/ui/Toast.tsx +127 -0
- package/src/components/ui/Toggle-Group.tsx +160 -0
- package/src/components/ui/Toggle.tsx +122 -0
- package/src/components/ui/Tooltip.tsx +117 -0
- package/src/components/ui/index.ts +42 -0
- package/src/index.tsx +24 -0
- package/src/lib/ThemeProvider.tsx +204 -0
- package/src/lib/cornerRadius.ts +160 -0
- package/src/lib/fonts.ts +28 -0
- package/src/lib/theme.ts +151 -0
- package/src/lib/utils.ts +146 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import { Platform, TextInput, type TextInputProps } from 'react-native';
|
|
3
|
+
import { cssInterop } from 'nativewind';
|
|
4
|
+
|
|
5
|
+
cssInterop(TextInput, { className: 'style' });
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for Textarea component
|
|
9
|
+
*
|
|
10
|
+
* @extends TextInputProps - All React Native TextInput properties
|
|
11
|
+
* @property {string} [className] - Additional Tailwind classes for styling
|
|
12
|
+
* @property {string} [placeholderClassName] - Tailwind classes for placeholder text styling
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Textarea
|
|
17
|
+
* placeholder="Enter your message"
|
|
18
|
+
* value={message}
|
|
19
|
+
* onChangeText={setMessage}
|
|
20
|
+
* numberOfLines={4}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
interface TextareaProps extends TextInputProps {
|
|
25
|
+
className?: string;
|
|
26
|
+
placeholderClassName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Multi-line text input component
|
|
31
|
+
*
|
|
32
|
+
* A textarea component for longer text input with configurable height and styling.
|
|
33
|
+
* Automatically configured for multi-line input with appropriate platform-specific defaults.
|
|
34
|
+
*
|
|
35
|
+
* @component
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* // Basic textarea
|
|
39
|
+
* <Textarea
|
|
40
|
+
* placeholder="Enter description"
|
|
41
|
+
* value={description}
|
|
42
|
+
* onChangeText={setDescription}
|
|
43
|
+
* />
|
|
44
|
+
*
|
|
45
|
+
* // Controlled textarea with validation
|
|
46
|
+
* <Textarea
|
|
47
|
+
* value={bio}
|
|
48
|
+
* onChangeText={setBio}
|
|
49
|
+
* placeholder="Tell us about yourself"
|
|
50
|
+
* numberOfLines={6}
|
|
51
|
+
* maxLength={500}
|
|
52
|
+
* />
|
|
53
|
+
*
|
|
54
|
+
* // Disabled textarea
|
|
55
|
+
* <Textarea value={content} editable={false} />
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @accessibility
|
|
59
|
+
* - Supports standard TextInput accessibility props
|
|
60
|
+
* - Placeholder text with appropriate color contrast
|
|
61
|
+
* - Focus states on web for keyboard navigation
|
|
62
|
+
* - Disabled state with reduced opacity
|
|
63
|
+
*/
|
|
64
|
+
function Textarea({
|
|
65
|
+
className,
|
|
66
|
+
multiline = true,
|
|
67
|
+
numberOfLines = Platform.select({ web: 2, native: 8 }),
|
|
68
|
+
placeholderClassName,
|
|
69
|
+
editable = true,
|
|
70
|
+
...props
|
|
71
|
+
}: TextareaProps & React.RefAttributes<TextInput>) {
|
|
72
|
+
return (
|
|
73
|
+
<TextInput
|
|
74
|
+
className={cn(
|
|
75
|
+
'text-foreground border-input dark:bg-input/30 flex min-h-16 w-full flex-row rounded-md border bg-transparent px-3 py-2 text-base shadow-sm shadow-black/5 md:text-sm',
|
|
76
|
+
Platform.select({
|
|
77
|
+
web: 'placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content resize-y outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed',
|
|
78
|
+
}),
|
|
79
|
+
!editable && 'opacity-50',
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
placeholderClassName={cn('text-muted-foreground', placeholderClassName)}
|
|
83
|
+
multiline={multiline}
|
|
84
|
+
numberOfLines={numberOfLines}
|
|
85
|
+
textAlignVertical="top"
|
|
86
|
+
editable={editable}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { Textarea };
|
|
93
|
+
export type { TextareaProps };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, Pressable } from 'react-native';
|
|
3
|
+
import { cssInterop } from 'nativewind';
|
|
4
|
+
import { Moon, Sun, Monitor } from 'lucide-react-native';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { useTheme, type ThemeMode } from '../../lib/ThemeProvider';
|
|
7
|
+
import { Icon } from './Icon';
|
|
8
|
+
import { Text } from './Text';
|
|
9
|
+
import type { SlottableViewProps, ViewRef } from '@rn-primitives/types';
|
|
10
|
+
|
|
11
|
+
cssInterop(View, { className: 'style' });
|
|
12
|
+
cssInterop(Pressable, { className: 'style' });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Theme toggle button variants
|
|
16
|
+
*/
|
|
17
|
+
type ThemeToggleVariant = 'icon' | 'button' | 'segmented';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for ThemeToggle component
|
|
21
|
+
*
|
|
22
|
+
* @property {'icon' | 'button' | 'segmented'} [variant='icon'] - Visual style variant
|
|
23
|
+
* @property {boolean} [showLabel=false] - Show text label next to icon (for button variant)
|
|
24
|
+
* @property {string} [className] - Additional Tailwind classes
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* // Icon only (default)
|
|
29
|
+
* <ThemeToggle />
|
|
30
|
+
*
|
|
31
|
+
* // Button with label
|
|
32
|
+
* <ThemeToggle variant="button" showLabel />
|
|
33
|
+
*
|
|
34
|
+
* // Segmented control
|
|
35
|
+
* <ThemeToggle variant="segmented" />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
type ThemeToggleProps = SlottableViewProps & {
|
|
39
|
+
variant?: ThemeToggleVariant;
|
|
40
|
+
showLabel?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Theme toggle component for switching between light, dark, and system themes
|
|
46
|
+
*
|
|
47
|
+
* Provides three variants:
|
|
48
|
+
* - **icon**: Simple icon button that cycles through themes (light → dark → system)
|
|
49
|
+
* - **button**: Button with icon and optional label
|
|
50
|
+
* - **segmented**: Segmented control showing all three options
|
|
51
|
+
*
|
|
52
|
+
* @component
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* // Icon toggle (minimal)
|
|
56
|
+
* <ThemeToggle />
|
|
57
|
+
*
|
|
58
|
+
* // Button with label
|
|
59
|
+
* <ThemeToggle variant="button" showLabel />
|
|
60
|
+
*
|
|
61
|
+
* // Segmented control (shows all options)
|
|
62
|
+
* <ThemeToggle variant="segmented" />
|
|
63
|
+
*
|
|
64
|
+
* // Custom styling
|
|
65
|
+
* <ThemeToggle className="my-4" />
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @accessibility
|
|
69
|
+
* - Proper button role for screen readers
|
|
70
|
+
* - Announces current theme state
|
|
71
|
+
* - Keyboard navigable on web
|
|
72
|
+
*/
|
|
73
|
+
const ThemeToggle = React.forwardRef<ViewRef, ThemeToggleProps>(
|
|
74
|
+
({ variant = 'icon', showLabel = false, className, ...props }, ref) => {
|
|
75
|
+
const { mode, isDark, setMode } = useTheme();
|
|
76
|
+
|
|
77
|
+
// Cycle through themes: light → dark → system → light
|
|
78
|
+
const cycleTheme = () => {
|
|
79
|
+
const nextMode: ThemeMode =
|
|
80
|
+
mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light';
|
|
81
|
+
setMode(nextMode);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (variant === 'segmented') {
|
|
85
|
+
return (
|
|
86
|
+
<View
|
|
87
|
+
ref={ref as any}
|
|
88
|
+
className={cn(
|
|
89
|
+
'flex-row bg-muted dark:bg-muted rounded-lg p-1',
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{(['light', 'dark', 'system'] as const).map((themeMode) => {
|
|
95
|
+
const isActive = mode === themeMode;
|
|
96
|
+
const IconComponent =
|
|
97
|
+
themeMode === 'light'
|
|
98
|
+
? Sun
|
|
99
|
+
: themeMode === 'dark'
|
|
100
|
+
? Moon
|
|
101
|
+
: Monitor;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Pressable
|
|
105
|
+
key={themeMode}
|
|
106
|
+
onPress={() => setMode(themeMode)}
|
|
107
|
+
className={cn(
|
|
108
|
+
'flex-1 flex-row items-center justify-center rounded-md px-3 py-2 transition-colors',
|
|
109
|
+
isActive
|
|
110
|
+
? 'bg-background dark:bg-background shadow-sm'
|
|
111
|
+
: 'active:bg-background/50 dark:active:bg-background/50'
|
|
112
|
+
)}
|
|
113
|
+
role="button"
|
|
114
|
+
accessibilityLabel={`Switch to ${themeMode} theme`}
|
|
115
|
+
accessibilityState={{ selected: isActive }}
|
|
116
|
+
>
|
|
117
|
+
<Icon
|
|
118
|
+
as={IconComponent}
|
|
119
|
+
size={16}
|
|
120
|
+
className={cn(
|
|
121
|
+
isActive
|
|
122
|
+
? 'text-foreground dark:text-foreground'
|
|
123
|
+
: 'text-muted-foreground dark:text-muted-foreground'
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
<Text
|
|
127
|
+
className={cn(
|
|
128
|
+
'ml-2 text-sm font-medium capitalize',
|
|
129
|
+
isActive
|
|
130
|
+
? 'text-foreground dark:text-foreground'
|
|
131
|
+
: 'text-muted-foreground dark:text-muted-foreground'
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{themeMode}
|
|
135
|
+
</Text>
|
|
136
|
+
</Pressable>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</View>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (variant === 'button') {
|
|
144
|
+
const IconComponent = isDark ? Moon : Sun;
|
|
145
|
+
const label = mode === 'system' ? 'System' : isDark ? 'Dark' : 'Light';
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Pressable
|
|
149
|
+
ref={ref as any}
|
|
150
|
+
onPress={cycleTheme}
|
|
151
|
+
className={cn(
|
|
152
|
+
'flex-row items-center justify-center rounded-md px-4 py-2',
|
|
153
|
+
'bg-secondary dark:bg-secondary active:opacity-80',
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
role="button"
|
|
157
|
+
accessibilityLabel={`Current theme: ${label}. Tap to change.`}
|
|
158
|
+
{...props}
|
|
159
|
+
>
|
|
160
|
+
<Icon
|
|
161
|
+
as={IconComponent}
|
|
162
|
+
size={16}
|
|
163
|
+
className="text-secondary-foreground dark:text-secondary-foreground"
|
|
164
|
+
/>
|
|
165
|
+
{showLabel && (
|
|
166
|
+
<Text className="ml-2 text-sm font-medium text-secondary-foreground dark:text-secondary-foreground">
|
|
167
|
+
{label}
|
|
168
|
+
</Text>
|
|
169
|
+
)}
|
|
170
|
+
</Pressable>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Default: icon variant
|
|
175
|
+
const IconComponent = mode === 'system' ? Monitor : isDark ? Moon : Sun;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Pressable
|
|
179
|
+
ref={ref as any}
|
|
180
|
+
onPress={cycleTheme}
|
|
181
|
+
className={cn(
|
|
182
|
+
'h-10 w-10 items-center justify-center rounded-md',
|
|
183
|
+
'active:bg-accent dark:active:bg-accent',
|
|
184
|
+
'web:hover:bg-accent/50 dark:web:hover:bg-accent/50',
|
|
185
|
+
'web:transition-colors',
|
|
186
|
+
className
|
|
187
|
+
)}
|
|
188
|
+
role="button"
|
|
189
|
+
accessibilityLabel={`Current theme: ${mode}. Tap to cycle themes.`}
|
|
190
|
+
{...props}
|
|
191
|
+
>
|
|
192
|
+
<Icon
|
|
193
|
+
as={IconComponent}
|
|
194
|
+
size={20}
|
|
195
|
+
className="text-foreground dark:text-foreground"
|
|
196
|
+
/>
|
|
197
|
+
</Pressable>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
ThemeToggle.displayName = 'ThemeToggle';
|
|
202
|
+
|
|
203
|
+
export { ThemeToggle };
|
|
204
|
+
export type { ThemeToggleProps, ThemeToggleVariant };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import { Text, TextClassContext } from './Text';
|
|
3
|
+
import { Icon } from './Icon';
|
|
4
|
+
import type { LucideIcon } from 'lucide-react-native';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { View, type ViewProps } from 'react-native';
|
|
7
|
+
import { cssInterop } from 'nativewind';
|
|
8
|
+
|
|
9
|
+
cssInterop(View, { className: 'style' });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Toast notification component
|
|
13
|
+
*
|
|
14
|
+
* Displays temporary messages or notifications with optional icons.
|
|
15
|
+
* Supports default and destructive variants for different message types.
|
|
16
|
+
*
|
|
17
|
+
* @component
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Toast icon={CheckCircle}>
|
|
21
|
+
* <ToastTitle>
|
|
22
|
+
* <Text>Success</Text>
|
|
23
|
+
* </ToastTitle>
|
|
24
|
+
* <ToastDescription>
|
|
25
|
+
* <Text>Your changes have been saved</Text>
|
|
26
|
+
* </ToastDescription>
|
|
27
|
+
* </Toast>
|
|
28
|
+
*
|
|
29
|
+
* <Toast variant="destructive" icon={AlertCircle}>
|
|
30
|
+
* <ToastTitle>
|
|
31
|
+
* <Text>Error</Text>
|
|
32
|
+
* </ToastTitle>
|
|
33
|
+
* <ToastDescription>
|
|
34
|
+
* <Text>Failed to save changes</Text>
|
|
35
|
+
* </ToastDescription>
|
|
36
|
+
* </Toast>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @accessibility
|
|
40
|
+
* - Announces messages to screen readers
|
|
41
|
+
* - Proper role for notifications
|
|
42
|
+
*/
|
|
43
|
+
function Toast({
|
|
44
|
+
className,
|
|
45
|
+
variant,
|
|
46
|
+
children,
|
|
47
|
+
icon,
|
|
48
|
+
iconClassName,
|
|
49
|
+
...props
|
|
50
|
+
}: ViewProps &
|
|
51
|
+
React.RefAttributes<View> & {
|
|
52
|
+
icon?: LucideIcon;
|
|
53
|
+
variant?: 'default' | 'destructive';
|
|
54
|
+
iconClassName?: string;
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<TextClassContext.Provider
|
|
58
|
+
value={cn(
|
|
59
|
+
'text-sm text-foreground',
|
|
60
|
+
variant === 'destructive' && 'text-destructive',
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<View
|
|
65
|
+
className={cn(
|
|
66
|
+
'bg-background border-border relative flex-row items-start gap-3 rounded-lg border p-4 shadow-lg shadow-black/10',
|
|
67
|
+
variant === 'destructive' && 'border-destructive',
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{icon && (
|
|
73
|
+
<Icon
|
|
74
|
+
as={icon}
|
|
75
|
+
className={cn(
|
|
76
|
+
'size-5 shrink-0',
|
|
77
|
+
variant === 'destructive' && 'text-destructive',
|
|
78
|
+
iconClassName
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
<View className="flex-1 gap-1">{children}</View>
|
|
83
|
+
</View>
|
|
84
|
+
</TextClassContext.Provider>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Toast title component
|
|
90
|
+
*
|
|
91
|
+
* @component
|
|
92
|
+
*/
|
|
93
|
+
function ToastTitle({
|
|
94
|
+
className,
|
|
95
|
+
...props
|
|
96
|
+
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
|
97
|
+
return (
|
|
98
|
+
<Text
|
|
99
|
+
className={cn('font-semibold leading-none tracking-tight', className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Toast description component
|
|
107
|
+
*
|
|
108
|
+
* @component
|
|
109
|
+
*/
|
|
110
|
+
function ToastDescription({
|
|
111
|
+
className,
|
|
112
|
+
...props
|
|
113
|
+
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
|
114
|
+
const textClass = React.useContext(TextClassContext);
|
|
115
|
+
return (
|
|
116
|
+
<Text
|
|
117
|
+
className={cn(
|
|
118
|
+
'text-muted-foreground text-sm',
|
|
119
|
+
textClass?.includes('text-destructive') && 'text-destructive/90',
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { Toast, ToastDescription, ToastTitle };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Icon } from './Icon';
|
|
2
|
+
import { TextClassContext } from './Text';
|
|
3
|
+
import { toggleVariants } from './Toggle';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import * as ToggleGroupPrimitive from '@rn-primitives/toggle-group';
|
|
6
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context for sharing variant and size props across toggle group items
|
|
12
|
+
*/
|
|
13
|
+
const ToggleGroupContext = React.createContext<VariantProps<
|
|
14
|
+
typeof toggleVariants
|
|
15
|
+
> | null>(null);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Toggle group component for grouping related toggle buttons
|
|
19
|
+
*
|
|
20
|
+
* Allows single or multiple selection among a set of toggle buttons.
|
|
21
|
+
* Supports both outline and default variants.
|
|
22
|
+
*
|
|
23
|
+
* @component
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <ToggleGroup type="single" value={alignment} onValueChange={setAlignment}>
|
|
27
|
+
* <ToggleGroupItem value="left" isFirst>
|
|
28
|
+
* <ToggleGroupIcon as={AlignLeft} />
|
|
29
|
+
* </ToggleGroupItem>
|
|
30
|
+
* <ToggleGroupItem value="center">
|
|
31
|
+
* <ToggleGroupIcon as={AlignCenter} />
|
|
32
|
+
* </ToggleGroupItem>
|
|
33
|
+
* <ToggleGroupItem value="right" isLast>
|
|
34
|
+
* <ToggleGroupIcon as={AlignRight} />
|
|
35
|
+
* </ToggleGroupItem>
|
|
36
|
+
* </ToggleGroup>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @accessibility
|
|
40
|
+
* - Uses proper ARIA attributes for grouped toggles
|
|
41
|
+
* - Keyboard navigation support
|
|
42
|
+
*/
|
|
43
|
+
function ToggleGroup({
|
|
44
|
+
className,
|
|
45
|
+
variant,
|
|
46
|
+
size,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: ToggleGroupPrimitive.RootProps &
|
|
50
|
+
VariantProps<typeof toggleVariants> &
|
|
51
|
+
React.RefAttributes<ToggleGroupPrimitive.RootRef>) {
|
|
52
|
+
return (
|
|
53
|
+
<ToggleGroupPrimitive.Root
|
|
54
|
+
className={cn(
|
|
55
|
+
'flex flex-row items-center rounded-md shadow-none',
|
|
56
|
+
Platform.select({ web: 'w-fit' }),
|
|
57
|
+
variant === 'outline' && 'shadow-sm shadow-black/5',
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
<ToggleGroupContext.Provider value={{ variant, size }}>
|
|
63
|
+
{children}
|
|
64
|
+
</ToggleGroupContext.Provider>
|
|
65
|
+
</ToggleGroupPrimitive.Root>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access toggle group context
|
|
71
|
+
*
|
|
72
|
+
* @throws Error if used outside of ToggleGroup component
|
|
73
|
+
*/
|
|
74
|
+
function useToggleGroupContext() {
|
|
75
|
+
const context = React.useContext(ToggleGroupContext);
|
|
76
|
+
if (context === null) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'ToggleGroup compound components cannot be rendered outside the ToggleGroup component'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return context;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Individual toggle item within a toggle group
|
|
86
|
+
*
|
|
87
|
+
* @component
|
|
88
|
+
*/
|
|
89
|
+
function ToggleGroupItem({
|
|
90
|
+
className,
|
|
91
|
+
children,
|
|
92
|
+
variant,
|
|
93
|
+
size,
|
|
94
|
+
isFirst,
|
|
95
|
+
isLast,
|
|
96
|
+
...props
|
|
97
|
+
}: ToggleGroupPrimitive.ItemProps &
|
|
98
|
+
VariantProps<typeof toggleVariants> &
|
|
99
|
+
React.RefAttributes<ToggleGroupPrimitive.ItemRef> & {
|
|
100
|
+
isFirst?: boolean;
|
|
101
|
+
isLast?: boolean;
|
|
102
|
+
}) {
|
|
103
|
+
const context = useToggleGroupContext();
|
|
104
|
+
const { value } = ToggleGroupPrimitive.useRootContext();
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<TextClassContext.Provider
|
|
108
|
+
value={cn(
|
|
109
|
+
'text-sm text-foreground font-medium',
|
|
110
|
+
ToggleGroupPrimitive.utils.getIsSelected(value, props.value)
|
|
111
|
+
? 'text-accent-foreground'
|
|
112
|
+
: Platform.select({ web: 'group-hover:text-muted-foreground' })
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<ToggleGroupPrimitive.Item
|
|
116
|
+
className={cn(
|
|
117
|
+
toggleVariants({
|
|
118
|
+
variant: context.variant || variant,
|
|
119
|
+
size: context.size || size,
|
|
120
|
+
}),
|
|
121
|
+
props.disabled && 'opacity-50',
|
|
122
|
+
ToggleGroupPrimitive.utils.getIsSelected(value, props.value) &&
|
|
123
|
+
'bg-accent',
|
|
124
|
+
'min-w-0 shrink-0 rounded-none shadow-none',
|
|
125
|
+
isFirst && 'rounded-l-md',
|
|
126
|
+
isLast && 'rounded-r-md',
|
|
127
|
+
(context.variant === 'outline' || variant === 'outline') &&
|
|
128
|
+
'border-l-0',
|
|
129
|
+
(context.variant === 'outline' || variant === 'outline') &&
|
|
130
|
+
isFirst &&
|
|
131
|
+
'border-l',
|
|
132
|
+
Platform.select({
|
|
133
|
+
web: 'flex-1 focus:z-10 focus-visible:z-10',
|
|
134
|
+
}),
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</ToggleGroupPrimitive.Item>
|
|
141
|
+
</TextClassContext.Provider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Icon component for use within toggle group items
|
|
147
|
+
*
|
|
148
|
+
* @component
|
|
149
|
+
*/
|
|
150
|
+
function ToggleGroupIcon({
|
|
151
|
+
className,
|
|
152
|
+
...props
|
|
153
|
+
}: React.ComponentProps<typeof Icon>) {
|
|
154
|
+
const textClass = React.useContext(TextClassContext);
|
|
155
|
+
return (
|
|
156
|
+
<Icon className={cn('size-4 shrink-0', textClass, className)} {...props} />
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Icon } from './Icon';
|
|
2
|
+
import { TextClassContext } from './Text';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import * as TogglePrimitive from '@rn-primitives/toggle';
|
|
5
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { Platform } from 'react-native';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Toggle button style variants using class-variance-authority
|
|
11
|
+
*
|
|
12
|
+
* @variant default - Standard toggle with transparent background
|
|
13
|
+
* @variant outline - Toggle with border and background
|
|
14
|
+
*
|
|
15
|
+
* @size default - Standard size (40px/36px)
|
|
16
|
+
* @size sm - Small size (36px/32px)
|
|
17
|
+
* @size lg - Large size (44px/40px)
|
|
18
|
+
*/
|
|
19
|
+
const toggleVariants = cva(
|
|
20
|
+
cn(
|
|
21
|
+
'active:bg-muted group flex flex-row items-center justify-center gap-2 rounded-md',
|
|
22
|
+
Platform.select({
|
|
23
|
+
web: 'hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex cursor-default whitespace-nowrap outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:pointer-events-none [&_svg]:pointer-events-none',
|
|
24
|
+
})
|
|
25
|
+
),
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
default: 'bg-transparent',
|
|
30
|
+
outline: cn(
|
|
31
|
+
'border-input active:bg-accent border bg-transparent shadow-sm shadow-black/5',
|
|
32
|
+
Platform.select({
|
|
33
|
+
web: 'hover:bg-accent hover:text-accent-foreground',
|
|
34
|
+
})
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
size: {
|
|
38
|
+
default: 'h-10 min-w-10 px-2.5 sm:h-9 sm:min-w-9 sm:px-2',
|
|
39
|
+
sm: 'h-9 min-w-9 px-2 sm:h-8 sm:min-w-8 sm:px-1.5',
|
|
40
|
+
lg: 'h-11 min-w-11 px-3 sm:h-10 sm:min-w-10 sm:px-2.5',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
variant: 'default',
|
|
45
|
+
size: 'default',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Toggle button component for on/off states
|
|
52
|
+
*
|
|
53
|
+
* A two-state button that can be toggled between pressed and unpressed states.
|
|
54
|
+
* Supports multiple variants and sizes.
|
|
55
|
+
*
|
|
56
|
+
* @component
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <Toggle pressed={isBold} onPressedChange={setIsBold}>
|
|
60
|
+
* <ToggleIcon as={Bold} />
|
|
61
|
+
* <Text>Bold</Text>
|
|
62
|
+
* </Toggle>
|
|
63
|
+
*
|
|
64
|
+
* <Toggle variant="outline" size="sm">
|
|
65
|
+
* <ToggleIcon as={Italic} />
|
|
66
|
+
* </Toggle>
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @accessibility
|
|
70
|
+
* - Uses proper ARIA pressed state
|
|
71
|
+
* - Disabled state prevents interaction
|
|
72
|
+
* - Focus visible states on web
|
|
73
|
+
*/
|
|
74
|
+
function Toggle({
|
|
75
|
+
className,
|
|
76
|
+
variant,
|
|
77
|
+
size,
|
|
78
|
+
...props
|
|
79
|
+
}: TogglePrimitive.RootProps &
|
|
80
|
+
VariantProps<typeof toggleVariants> &
|
|
81
|
+
React.RefAttributes<TogglePrimitive.RootRef>) {
|
|
82
|
+
return (
|
|
83
|
+
<TextClassContext.Provider
|
|
84
|
+
value={cn(
|
|
85
|
+
'text-sm text-foreground font-medium',
|
|
86
|
+
props.pressed
|
|
87
|
+
? 'text-accent-foreground'
|
|
88
|
+
: Platform.select({ web: 'group-hover:text-muted-foreground' }),
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<TogglePrimitive.Root
|
|
93
|
+
className={cn(
|
|
94
|
+
toggleVariants({ variant, size }),
|
|
95
|
+
props.disabled && 'opacity-50',
|
|
96
|
+
props.pressed && 'bg-accent',
|
|
97
|
+
className
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
</TextClassContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Icon component for use within Toggle buttons
|
|
107
|
+
*
|
|
108
|
+
* Automatically inherits text styling from the toggle context.
|
|
109
|
+
*
|
|
110
|
+
* @component
|
|
111
|
+
*/
|
|
112
|
+
function ToggleIcon({
|
|
113
|
+
className,
|
|
114
|
+
...props
|
|
115
|
+
}: React.ComponentProps<typeof Icon>) {
|
|
116
|
+
const textClass = React.useContext(TextClassContext);
|
|
117
|
+
return (
|
|
118
|
+
<Icon className={cn('size-4 shrink-0', textClass, className)} {...props} />
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { Toggle, ToggleIcon, toggleVariants };
|