diff --git a/apps/expo-stylesheet/app/(components)/accordion.tsx b/apps/expo-stylesheet/app/(components)/accordion.tsx new file mode 100644 index 00000000..800118bd --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/accordion.tsx @@ -0,0 +1,66 @@ +import { View, StyleSheet } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '~/components/ui/accordion'; +import { Text } from '~/components/ui/text'; + +export default function AccordionScreen() { + const { colors } = useTheme(); + + return ( + + + + + Is it accessible? + + + + Yes. It adheres to the WAI-ARIA design pattern. + + + + + + + What are universal components? + + + + In the world of React Native, universal components are components that work on both + web and native platforms. + + + + + + + Is this component universal? + + + + Yes. Try it out on the web, iOS, and/or Android. + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + accordion: { + width: '100%', + maxWidth: 384, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/alert-dialog.tsx b/apps/expo-stylesheet/app/(components)/alert-dialog.tsx new file mode 100644 index 00000000..e0502a35 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/alert-dialog.tsx @@ -0,0 +1,53 @@ +import { View, StyleSheet } from 'react-native'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '~/components/ui/alert-dialog'; +import { Button } from '~/components/ui/button'; +import { Text } from '~/components/ui/text'; + +export default function AlertDialogScreen() { + return ( + + + + + Show Alert Dialog + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove + your data from our servers. + + + + + Cancel + + + Continue + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/aspect-ratio.tsx b/apps/expo-stylesheet/app/(components)/aspect-ratio.tsx new file mode 100644 index 00000000..6c93ec1d --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/aspect-ratio.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { AspectRatio } from '~/components/ui/aspect-ratio'; +import { H1 } from '~/components/ui/typography'; + +export default function AspectRatioScreen() { + return ( + + + + + 16:9 + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + wrapper: { + width: '50%', + }, + innerBox: { + backgroundColor: '#3b82f6', + height: '100%', + width: '100%', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/avatar.tsx b/apps/expo-stylesheet/app/(components)/avatar.tsx new file mode 100644 index 00000000..a3016633 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/avatar.tsx @@ -0,0 +1,28 @@ +import { View, StyleSheet } from 'react-native'; +import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; +import { Text } from '~/components/ui/text'; + +const GITHUB_AVATAR_URI = 'https://github.com/mrzachnugent.png'; + +export default function AvatarScreen() { + return ( + + + + + ZN + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, // p-6 + gap: 48, // gap-12 + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/checkbox.tsx b/apps/expo-stylesheet/app/(components)/checkbox.tsx new file mode 100644 index 00000000..8f981e54 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/checkbox.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { Platform, View, StyleSheet } from 'react-native'; +import { Checkbox } from '~/components/ui/checkbox'; +import { Label } from '~/components/ui/label'; + +export default function CheckboxScreen() { + const [checked, setChecked] = React.useState(false); + + return ( + + + + setChecked((prev) => !prev) })} + htmlFor='checkbox' + > + Accept terms and conditions + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + row: { + flexDirection: 'row', + gap: 12, + alignItems: 'center', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/collapsible.tsx b/apps/expo-stylesheet/app/(components)/collapsible.tsx new file mode 100644 index 00000000..8161e2b7 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/collapsible.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { Platform, Text, View, StyleSheet } from 'react-native'; +import Animated, { FadeInDown, LinearTransition } from 'react-native-reanimated'; +import { ChevronsDownUp, ChevronsUpDown } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { Button } from '~/components/ui/button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/collapsible'; + +export default function CollapsibleScreen() { + const [open, setOpen] = React.useState(false); + const { colors } = useTheme(); + + return ( + + + + + + + @peduarte starred 3 repositories + + + + {open ? ( + + ) : ( + + )} + Toggle + + + + + + @radix-ui/primitives + + + + @radix-ui/react + @stitches/core + + + + + + ); +} + +function CollapsibleItem({ children, delay }: { children: string; delay: number }) { + const { colors } = useTheme(); + + if (Platform.OS === 'web') { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + contentWrapper: { + width: '100%', + maxWidth: 350, + gap: 8, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 16, + paddingHorizontal: 16, + }, + headerText: { + fontSize: Platform.OS === 'web' ? 14 : 16, + fontWeight: '600', + }, + card: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 12, + }, + cardText: { + fontSize: Platform.OS === 'web' ? 14 : 16, + lineHeight: 20, + }, + collapsibleContent: { + gap: 8, + }, + srOnly: { + position: 'absolute', + width: 1, + height: 1, + margin: -1, + padding: 0, + borderWidth: 0, + overflow: 'hidden', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/context-menu.tsx b/apps/expo-stylesheet/app/(components)/context-menu.tsx new file mode 100644 index 00000000..47f9a8b6 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/context-menu.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import { Platform, Pressable, View, StyleSheet } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuItem, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from '~/components/ui/context-menu'; +import { Text } from '~/components/ui/text'; +import { useTheme } from '@react-navigation/native'; + +export default function ContextScreen() { + const triggerRef = React.useRef>(null); + const insets = useSafeAreaInsets(); + const { colors } = useTheme(); + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + + const [checkboxValue, setCheckboxValue] = React.useState(false); + const [subCheckboxValue, setSubCheckboxValue] = React.useState(false); + const [radioValue, setRadioValue] = React.useState('pedro'); + + return ( + + { + // Only for Native platforms: open menu programmatically + triggerRef.current?.open(); + }} + /> + + + + {Platform.OS === 'web' ? 'Right click here' : 'Long press here'} + + + + + + Back + ⌘[ + + + Forward + ⌘] + + + Reload + ⌘R + + + + + More Tools + + + + + Save Page As... + ⇧⌘S + + + Create Shortcut... + + + + + Developer Tools + + + + + + + + Show Bookmarks Bar + ⌘⇧B + + + Show Full URLs + + + + People + + + Elmer Fudd + + + Foghorn Leghorn + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + gap: 48, + }, + topRightButton: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, + contextTrigger: { + height: 150, + width: '100%', + maxWidth: 300, + marginHorizontal: 'auto', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 6, + borderWidth: 1, + borderStyle: 'dashed', + }, + triggerText: { + fontSize: Platform.OS === 'web' ? 14 : 16, + }, + contextContent: { + width: Platform.OS === 'web' ? 256 : 288, + }, + subContent: { + width: Platform.OS === 'web' ? 192 : 'auto', + marginTop: Platform.OS === 'web' ? 0 : 4, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/dialog.tsx b/apps/expo-stylesheet/app/(components)/dialog.tsx new file mode 100644 index 00000000..0a339170 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/dialog.tsx @@ -0,0 +1,55 @@ +import { ScrollView, StyleSheet } from 'react-native'; +import { Button } from '~/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '~/components/ui/dialog'; +import { Text } from '~/components/ui/text'; + +export default function DialogScreen() { + return ( + + + + + Edit Profile + + + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + + + + + + OK + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + dialogContent: { + maxWidth: 425, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/dropdown-menu.tsx b/apps/expo-stylesheet/app/(components)/dropdown-menu.tsx new file mode 100644 index 00000000..b8b86c99 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/dropdown-menu.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import { Pressable, View, StyleSheet } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button } from '~/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '~/components/ui/dropdown-menu'; +import { Text } from '~/components/ui/text'; +import { + Cloud, + Github, + LifeBuoy, + LogOut, + Mail, + MessageSquare, + Plus, + PlusCircle, + UserPlus, + Users, +} from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; + +export default function DropdownMenuScreen() { + const triggerRef = React.useRef>(null); + const insets = useSafeAreaInsets(); + const { colors } = useTheme(); + + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + + return ( + + { + // open menu programmatically + triggerRef.current?.open(); + }} + /> + + + + Open + + + + My Account + + + + + Team + + + + + Invite users + + + + + + Email + + + + Message + + + + + More... + + + + + + + New Team + ⌘+T + + + + + + GitHub + + + + Support + + + + API + + + + + Log out + ⇧⌘Q + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + floatingButton: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, + dropdownContent: { + width: 256, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/hover-card.tsx b/apps/expo-stylesheet/app/(components)/hover-card.tsx new file mode 100644 index 00000000..ea345aa0 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/hover-card.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { Pressable, View, StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; +import { Button } from '~/components/ui/button'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '~/components/ui/hover-card'; +import { Text } from '~/components/ui/text'; +import { CalendarDays } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +export default function HoverCardScreen() { + const triggerRef = React.useRef>(null); + const insets = useSafeAreaInsets(); + const { colors } = useTheme() as ICustomTheme; + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + + return ( + + { + // open programmatically + triggerRef.current?.open(); + }} + /> + + + + @nextjs + + + + + + + + VA + + + + @nextjs + + The React Framework – created and maintained by @vercel. + + + + + Joined December 2021 + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + floatingButton: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, + cardContent: { + width: 320, + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: 16, + }, + userInfo: { + flex: 1, + gap: 4, + }, + username: { + fontSize: 14, + fontWeight: '600', + }, + description: { + fontSize: 14, + lineHeight: 20, + }, + joinedRow: { + flexDirection: 'row', + alignItems: 'center', + paddingTop: 8, + gap: 8, + }, + joinedText: { + fontSize: 12, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/menubar.tsx b/apps/expo-stylesheet/app/(components)/menubar.tsx new file mode 100644 index 00000000..0edfdfd0 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/menubar.tsx @@ -0,0 +1,228 @@ +import { useNavigation } from 'expo-router'; +import * as React from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from '~/components/ui/menubar'; +import { Text } from '~/components/ui/text'; + +export default function MenubarScreen() { + const insets = useSafeAreaInsets(); + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + const [value, setValue] = React.useState(); + const [isSubOpen, setIsSubOpen] = React.useState(false); + const [isSubOpen2, setIsSubOpen2] = React.useState(false); + const [isChecked, setIsChecked] = React.useState(false); + const [isChecked2, setIsChecked2] = React.useState(false); + const [radio, setRadio] = React.useState('michael'); + const navigation = useNavigation(); + React.useEffect(() => { + const sub = navigation.addListener('blur', () => { + onValueChange(undefined); + }); + + return sub; + }, []); + + function closeSubs() { + setIsSubOpen(false); + setIsSubOpen2(false); + } + + function onValueChange(val: string | undefined) { + if (typeof val === 'string') { + setValue(val); + return; + } + closeSubs(); + setValue(undefined); + } + + return ( + + {!!value && ( + { + onValueChange(undefined); + }} + style={StyleSheet.absoluteFillObject} + /> + )} + + + + File + + + + New Tab + ⌘T + + + New Window + ⌘N + + + New Incognito Window + + + + + Share + + + + + Email link + + + Messages + + + Notes + + + + + + + Print... + ⌘P + + + + + + Edit + + + + Undo + ⌘Z + + + Redo + ⇧⌘Z + + + + + Find + + + + + Search the web + + + + Find... + + + Find Next + + + Find Previous + + + + + + + Cut + + + Copy + + + Paste + + + + + + View + + + + Always Show Bookmarks Bar + + + Always Show Full URLs + + + + Reload + ⌘R + + + Force Reload + ⇧⌘R + + + + Toggle Fullscreen + + + + Hide Sidebar + + + + + + Profiles + + + + + Andy + + + Michael + + + Creed + + + + + Edit... + + + + Add Profile... + + + + + + ); +} diff --git a/apps/expo-stylesheet/app/(components)/navigation-menu.tsx b/apps/expo-stylesheet/app/(components)/navigation-menu.tsx new file mode 100644 index 00000000..857d2c37 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/navigation-menu.tsx @@ -0,0 +1,237 @@ +import { ViewRef } from '@rn-primitives/types'; +import { useNavigation } from 'expo-router'; +import * as React from 'react'; +import { Platform, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from '~/components/ui/navigation-menu'; +import { Text } from '~/components/ui/text'; +import { Sparkles } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { ICustomTheme } from '~/lib/constants'; + +export default function NavigationMenuScreen() { + const insets = useSafeAreaInsets(); + const { colors } = useTheme() as ICustomTheme; + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + const [value, setValue] = React.useState(); + const navigation = useNavigation(); + const { width } = useWindowDimensions(); + const isMediumOrAboveScreen = width >= 768; + + function closeAll() { + setValue(''); + } + + React.useEffect(() => { + const sub = navigation.addListener('blur', () => { + closeAll(); + }); + + return sub; + }, []); + + return ( + + {Platform.OS !== 'web' && !!value && ( + { + setValue(''); + }} + style={StyleSheet.absoluteFill} + /> + )} + + + + + Getting started + + + + + + + + react-native-reusables + + Universal components that you can copy and paste into your apps. Accessible. + Customizable. Open Source. + + + + + + Re-usable components built using Radix UI on the web and Tailwind CSS. + + + How to install dependencies and structure your app. + + + Styles for headings, paragraphs, lists...etc + + + + + + + Components + + + + {components.map((component) => ( + + {component.description} + + ))} + + + + + + Documentation + + + + + + ); +} + +const components: { title: string; href: string; description: string }[] = [ + { + title: 'Alert Dialog', + href: '/alert-dialog/alert-dialog-universal', + description: + 'A modal dialog that interrupts the user with important content and expects a response.', + }, + { + title: 'Hover Card', + href: '/hover-card/hover-card-universal', + description: 'For sighted users to preview content available behind a link.', + }, + { + title: 'Progress', + href: '/progress/progress-universal', + description: + 'Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.', + }, + { + title: 'Scroll-area', + href: '/scroll-area/scroll-area-universal', + description: + 'Visually or semantically separates content. Typically used to create a scrollable area.', + }, + { + title: 'Tabs', + href: '/tabs/tabs-universal', + description: + 'A set of layered sections of content—known as tab panels—that are displayed one at a time.', + }, + { + title: 'Tooltip', + href: '/tooltip/tooltip-universal', + description: + 'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.', + }, +]; + +const ListItem = React.forwardRef< + ViewRef, + React.ComponentPropsWithoutRef & { title: string; href: string } +>(({ title, children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const { width } = useWindowDimensions(); + const isMediumOrAboveScreen = width >= 768; + + return ( + + + {title} + + {children} + + + + ); +}); +ListItem.displayName = 'ListItem'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + paddingHorizontal: 24, + paddingVertical: 10, + gap: 48, + }, + menuContent: { + gap: 12, + padding: 20, + }, + featureCardWrapper: { + flex: 1, + }, + featureCard: { + flexDirection: 'column', + justifyContent: 'flex-end', + borderRadius: 6, + padding: 24, + borderWidth: 1, + }, + featureTitle: { + marginBottom: 8, + marginTop: 16, + fontSize: 20, + fontWeight: '500', + }, + featureDescription: { + fontSize: 14, + lineHeight: 20, + }, + componentsGrid: { + gap: 12, + padding: 16, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-around', + }, + listItem: { + width: 268, + gap: 4, + borderRadius: 6, + padding: 12, + textDecorationLine: 'none', + }, + listItemTitle: { + fontSize: 14, + lineHeight: 20, + fontWeight: '500', + }, + listItemDescription: { + fontSize: 14, + lineHeight: 20, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/popover.tsx b/apps/expo-stylesheet/app/(components)/popover.tsx new file mode 100644 index 00000000..449d39ff --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/popover.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import { Platform, Pressable, View, StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button } from '~/components/ui/button'; +import { Input } from '~/components/ui/input'; +import { Label } from '~/components/ui/label'; +import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'; +import { Text } from '~/components/ui/text'; +import { useTheme } from '@react-navigation/native'; +import { ICustomTheme } from '~/lib/constants'; + +export default function PopoverScreen() { + const triggerRef = React.useRef>(null); + const insets = useSafeAreaInsets(); + const { colors } = useTheme() as ICustomTheme; + + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + + return ( + + [ + styles.fab, + { backgroundColor: pressed ? colors.accent : 'transparent' }, + ]} + onPress={() => { + // open programmatically + triggerRef.current?.open(); + }} + /> + + + + Open popover + + + + + + Dimensions + + Set the dimensions for the layer. + + + + + + + + + + + + + ); +} + +function LabelledInput({ + id, + label, + autoFocus = false, +}: { + id: string; + label: string; + autoFocus?: boolean; +}) { + const inputRef = React.useRef>(null); + + function onPress() { + inputRef.current?.focus(); + } + + return ( + + + {label} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + fab: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, + popoverContent: { + width: 320, + }, + section: { + gap: 16, + }, + title: { + fontWeight: '500', + fontSize: Platform.OS === 'web' ? 16 : 18, + lineHeight: Platform.OS === 'web' ? 20 : 28, + }, + subtitle: { + fontSize: Platform.OS === 'web' ? 14 : 12, + }, + inputGroup: { + gap: 8, + }, + labelledInputRow: { + flexDirection: 'row', + alignItems: 'center', + gap: Platform.OS === 'web' ? 8 : 16, + }, + label: { + width: 96, + }, + input: { + flex: 1, + height: Platform.OS === 'web' ? 32 : 40, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/progress.tsx b/apps/expo-stylesheet/app/(components)/progress.tsx new file mode 100644 index 00000000..dc9b51b0 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/progress.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { View, StyleSheet, Platform } from 'react-native'; +import { Button } from '~/components/ui/button'; +import { Progress } from '~/components/ui/progress'; +import { Text } from '~/components/ui/text'; + +export default function ProgressScreen() { + const [progress, setProgress] = React.useState(13); + + function onPress() { + setProgress(Math.floor(Math.random() * 100)); + } + + return ( + + + + + Randomize + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + inner: { + width: '100%', + gap: 32, + alignItems: 'center', + }, + progress: { + width: Platform.OS === 'web' ? '60%' : '100%', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/radio-group.tsx b/apps/expo-stylesheet/app/(components)/radio-group.tsx new file mode 100644 index 00000000..22687ccb --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/radio-group.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Label } from '~/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '~/components/ui/radio-group'; + +export default function RadioGroupScreen() { + const [value, setValue] = React.useState('Comfortable'); + + function onLabelPress(label: string) { + return () => { + setValue(label); + }; + } + + return ( + + + + + + + + ); +} + +function RadioGroupItemWithLabel({ + value, + onLabelPress, +}: { + value: string; + onLabelPress: () => void; +}) { + return ( + + + + {value} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + group: { + gap: 12, + }, + itemWithLabel: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/select.tsx b/apps/expo-stylesheet/app/(components)/select.tsx index 318c0430..d8e5d61e 100644 --- a/apps/expo-stylesheet/app/(components)/select.tsx +++ b/apps/expo-stylesheet/app/(components)/select.tsx @@ -1,5 +1,6 @@ +import { useTheme } from '@react-navigation/native'; import * as React from 'react'; -import { Pressable, View } from 'react-native'; +import { Pressable, View, StyleSheet } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Select, @@ -10,10 +11,12 @@ import { SelectTrigger, SelectValue, } from '~/components/ui/select'; +import { type ICustomTheme } from '~/lib/constants'; export default function SelectScreen() { const triggerRef = React.useRef>(null); const insets = useSafeAreaInsets(); + const { colors } = useTheme() as ICustomTheme; const contentInsets = { top: insets.top, bottom: insets.bottom, @@ -23,32 +26,20 @@ export default function SelectScreen() { return ( <> - + { - console.log('open'); // open programmatically triggerRef.current?.open(); }} - style={{ - position: 'absolute', - top: 0, - right: 0, - width: 64, - height: 64, - }} /> - + @@ -75,3 +66,20 @@ export default function SelectScreen() { > ); } + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + floatingButton: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/separator.tsx b/apps/expo-stylesheet/app/(components)/separator.tsx new file mode 100644 index 00000000..f4bfdc5f --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/separator.tsx @@ -0,0 +1,70 @@ +import { View, StyleSheet, Platform } from 'react-native'; +import { Separator } from '~/components/ui/separator'; +import { H4, P, Small } from '~/components/ui/typography'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +export default function SeparatorScreen() { + const { colors } = useTheme() as ICustomTheme; + + return ( + + + + Radix Primitives + + An open-source UI component library. + + + + + Blog + + Docs + + Source + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 24, + paddingHorizontal: 48, + gap: 48, + }, + card: { + width: '100%', + maxWidth: 320, + }, + header: { + gap: 4, + }, + headerTitle: { + fontSize: 14, + lineHeight: Platform.OS === 'web' ? 16 : 20, + fontWeight: '500', + }, + headerDescription: { + fontSize: 14, + lineHeight: 20, + }, + separatorMargin: { + marginVertical: 16, + }, + row: { + flexDirection: 'row', + height: 20, + alignItems: 'center', + gap: 16, + }, + smallText: { + fontSize: Platform.OS === 'web' ? 14 : 12, + fontWeight: '400', + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/slider.tsx b/apps/expo-stylesheet/app/(components)/slider.tsx new file mode 100644 index 00000000..1e29ecda --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/slider.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { Pressable, View, Text, Platform, StyleSheet, ViewStyle } from 'react-native'; +import * as Slider from '@rn-primitives/slider'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +export default function SliderScreen() { + const { colors } = useTheme() as ICustomTheme; + const [value, setValue] = React.useState(50); + const flattenTrackStyle = StyleSheet.flatten([ + styles.track, + { backgroundColor: colors.accent, borderColor: colors.border }, + ]); + const flattenRangeStyle = StyleSheet.flatten([styles.range, { backgroundColor: colors.primary }]); + const flattenThumbStyle = StyleSheet.flatten([ + styles.thumb, + { backgroundColor: colors.primary, left: `${value}%` }, + ]); + + return ( + + { + setValue(Math.floor(Math.random() * 100)); + }} + > + {Math.round(value)} + + + { + const nextValue = vals[0]; + if (typeof nextValue !== 'number') return; + setValue(nextValue); + }} + style={styles.sliderRoot} + > + + + + + + + + + {Platform.OS !== 'web' && ( + + + You will have to implement the gesture handling + + + Press the number to change the slider's value + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + valueText: { + fontSize: Platform.OS === 'web' ? 48 : 40, + lineHeight: Platform.OS === 'web' ? 48 : 40, + textAlign: 'center', + }, + sliderRoot: { + width: '100%', + justifyContent: 'center', + }, + track: { + height: Platform.OS === 'web' ? 16 : 14, + borderRadius: 9999, + borderWidth: 1, + }, + range: { + height: '100%', + width: '100%', + borderRadius: 9999, + }, + thumb: { + height: Platform.OS === 'web' ? 40 : 36, + width: Platform.OS === 'web' ? 40 : 36, + position: 'absolute', + transform: [{ translateY: -12 }, { translateX: -20 }], + borderRadius: 9999, + }, + helperTitle: { + fontSize: 18, + textAlign: 'center', + paddingBottom: 8, + }, + helperText: { + textAlign: 'center', + fontSize: 12, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/switch.tsx b/apps/expo-stylesheet/app/(components)/switch.tsx new file mode 100644 index 00000000..637fa05e --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/switch.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Label } from '~/components/ui/label'; +import { Switch } from '~/components/ui/switch'; + +export default function SwitchScreen() { + const [checked, setChecked] = React.useState(false); + + return ( + + + + setChecked((prev) => !prev)}> + Airplane Mode + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/table.tsx b/apps/expo-stylesheet/app/(components)/table.tsx new file mode 100644 index 00000000..695fa4dd --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/table.tsx @@ -0,0 +1,319 @@ +import { Stack } from 'expo-router'; +import * as React from 'react'; +import { + Alert, + FlatList, + ScrollView, + View, + useWindowDimensions, + StyleSheet, + Platform, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button } from '~/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'; +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from '~/components/ui/table'; +import { Text } from '~/components/ui/text'; +import { ChevronDown } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const MIN_COLUMN_WIDTHS = [120, 120, 110, 120]; + +export default function TableScreen() { + const { width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const isMediumOrAboveScreen = width >= 768; + const { colors } = useTheme() as ICustomTheme; + + const columnWidths = React.useMemo(() => { + return MIN_COLUMN_WIDTHS.map((minWidth) => { + const evenWidth = width / MIN_COLUMN_WIDTHS.length; + return evenWidth > minWidth ? evenWidth : minWidth; + }); + }, [width]); + + return ( + <> + + + + + + {/* Invoice with popover */} + + + + + Invoice + + + + + + Table Head + + This is the Invoice column. Just an example of a popover. + + + + + + + {/* Other table heads */} + + Status + + + Method + + + + Amount + + + + + + + { + return ( + + + {invoice.invoice} + + + {invoice.paymentStatus} + + + {invoice.paymentMethod} + + + { + Alert.alert( + invoice.totalAmount, + `You pressed the price button on invoice ${invoice.invoice}.` + ); + }} + > + {invoice.totalAmount} + + + + ); + }} + ListFooterComponent={() => { + return ( + <> + + + + Total + + + { + Alert.alert( + 'Total Amount', + `You pressed the total amount price button.` + ); + }} + > + $2,500.00 + + + + + + + A list of your recent invoices. + + + > + ); + }} + /> + + + + > + ); +} + +const styles = StyleSheet.create({ + popoverButton: { + flexDirection: 'row', + justifyContent: 'flex-start', + gap: 12, + }, + invoiceText: { + fontSize: Platform.OS === 'web' ? 16 : 14, + fontWeight: '500', + }, + popoverTitle: { + fontSize: Platform.OS === 'web' ? 24 : 22, + lineHeight: Platform.OS === 'web' ? 32 : 28, + fontWeight: '700', + }, + popoverDescription: { + fontSize: Platform.OS === 'web' ? 18 : 16, + lineHeight: Platform.OS === 'web' ? 28 : 24, + }, + amountHeadNative: { + textAlign: 'center', + }, + amountHeadWeb: { + textAlign: 'right', + paddingRight: 20, + }, + priceButton: { + shadowOpacity: 0.1, + shadowRadius: 2, + marginRight: 12, + paddingHorizontal: 8, + }, + tableFooterCell: { + flex: 1, + justifyContent: 'center', + }, + tableFooterCellAmount: { + alignItems: 'flex-end', + paddingRight: 24, + }, + footerNote: { + alignItems: 'center', + paddingVertical: 12, + }, + footerNoteText: { + textAlign: 'center', + fontSize: Platform.OS === 'web' ? 14 : 12, + }, +}); + +const INVOICES = [ + { + invoice: 'INV001', + paymentStatus: 'Paid', + totalAmount: '$250.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV002', paymentStatus: 'Pending', totalAmount: '$150.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV003', + paymentStatus: 'Unpaid', + totalAmount: '$350.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV004', + paymentStatus: 'Paid', + totalAmount: '$450.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV005', paymentStatus: 'Paid', totalAmount: '$550.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV006', + paymentStatus: 'Pending', + totalAmount: '$200.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV007', + paymentStatus: 'Unpaid', + totalAmount: '$300.00', + paymentMethod: 'Credit Card', + }, + { + invoice: 'INV008', + paymentStatus: 'Paid', + totalAmount: '$250.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV009', paymentStatus: 'Pending', totalAmount: '$150.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV0010', + paymentStatus: 'Unpaid', + totalAmount: '$350.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV0011', + paymentStatus: 'Paid', + totalAmount: '$450.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV0012', paymentStatus: 'Paid', totalAmount: '$550.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV0013', + paymentStatus: 'Pending', + totalAmount: '$200.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV0014', + paymentStatus: 'Unpaid', + totalAmount: '$300.00', + paymentMethod: 'Credit Card', + }, + { + invoice: 'INV0015', + paymentStatus: 'Paid', + totalAmount: '$250.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV0016', paymentStatus: 'Pending', totalAmount: '$150.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV0017', + paymentStatus: 'Unpaid', + totalAmount: '$350.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV0018', + paymentStatus: 'Paid', + totalAmount: '$450.00', + paymentMethod: 'Credit Card', + }, + { invoice: 'INV0019', paymentStatus: 'Paid', totalAmount: '$550.00', paymentMethod: 'PayPal' }, + { + invoice: 'INV0020', + paymentStatus: 'Pending', + totalAmount: '$200.00', + paymentMethod: 'Bank Transfer', + }, + { + invoice: 'INV0021', + paymentStatus: 'Unpaid', + totalAmount: '$300.00', + paymentMethod: 'Credit Card', + }, +]; diff --git a/apps/expo-stylesheet/app/(components)/tabs.tsx b/apps/expo-stylesheet/app/(components)/tabs.tsx new file mode 100644 index 00000000..426058c0 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/tabs.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Button } from '~/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '~/components/ui/card'; +import { Input } from '~/components/ui/input'; +import { Label } from '~/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs'; +import { Text } from '~/components/ui/text'; + +export default function TabsScreen() { + const [value, setValue] = React.useState('account'); + + return ( + + + + + Account + + + Password + + + + {/* Account tab */} + + + + Account + + Make changes to your account here. Click save when you're done. + + + + + Name + + + + Username + + + + + + Save changes + + + + + + {/* Password tab */} + + + + Password + + Change your password here. After saving, you'll be logged out. + + + + + Current password + + + + New password + + + + + + Save password + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + }, + tabs: { + width: '100%', + maxWidth: 400, + marginHorizontal: 'auto', + flexDirection: 'column', + gap: 6, + }, + tabsList: { + flexDirection: 'row', + width: '100%', + }, + cardContent: { + gap: 16, + }, + inputGroup: { + gap: 4, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/toggle-group.tsx b/apps/expo-stylesheet/app/(components)/toggle-group.tsx new file mode 100644 index 00000000..448cb0ba --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/toggle-group.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { ToggleGroup, ToggleGroupIcon, ToggleGroupItem } from '~/components/ui/toggle-group'; +import { Bold, Italic, Underline } from 'lucide-react-native'; + +export default function ToggleGroupScreen() { + const [value, setValue] = React.useState([]); + + return ( + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/toggle.tsx b/apps/expo-stylesheet/app/(components)/toggle.tsx new file mode 100644 index 00000000..e5fabf53 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/toggle.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Toggle, ToggleIcon } from '~/components/ui/toggle'; +import { Bold } from 'lucide-react-native'; + +export default function ToggleUniversalcreen() { + const [pressed, setPressed] = React.useState(false); + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/toolbar.tsx b/apps/expo-stylesheet/app/(components)/toolbar.tsx new file mode 100644 index 00000000..f3d4de2d --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/toolbar.tsx @@ -0,0 +1,152 @@ +import * as Toolbar from '@rn-primitives/toolbar'; +import * as React from 'react'; +import { Text, View, StyleSheet, Platform } from 'react-native'; +import { AlignCenter, AlignLeft, Bold, Italic } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +export default function ToolbarScreen() { + const [singleValue, setSingleValue] = React.useState(); + const [multipleValue, setMultipleValue] = React.useState([]); + const { colors } = useTheme() as ICustomTheme; + const flattenToolbarRootStyle = StyleSheet.flatten([ + styles.toolbarRoot, + { borderColor: colors.border }, + ]); + const nativeToolbarButtonStyle = ({ pressed }: { pressed: boolean }) => [ + styles.button, + { + backgroundColor: pressed ? colors.accent : colors.buttonSecondary, + }, + ]; + const webToolbarButtonStyle = { + ...styles.button, + backgroundColor: colors.buttonSecondary, + }; + + return ( + + + {/* Bold & Italic Group */} + + + + + + + + + + + + {/* Align Group */} + + + + + + + + + + + + {/* Button aligned right */} + + console.log('Button pressed')} + style={Platform.OS === 'web' ? webToolbarButtonStyle : nativeToolbarButtonStyle} + > + Button + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + gap: 48, + }, + toolbarRoot: { + flexDirection: 'row', + gap: 12, + justifyContent: 'center', + borderWidth: 1, + padding: 8, + borderRadius: 8, + }, + group: { + flexDirection: 'row', + gap: 4, + }, + item: { + padding: 8, + borderRadius: 8, + }, + separator: { + height: '100%', + width: 2, + }, + flexEnd: { + flexDirection: 'row', + flex: 1, + justifyContent: 'flex-end', + alignItems: 'center', + }, + button: { + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 8, + }, + buttonText: { + fontSize: 18, + }, +}); diff --git a/apps/expo-stylesheet/app/(components)/tooltip.tsx b/apps/expo-stylesheet/app/(components)/tooltip.tsx new file mode 100644 index 00000000..248d78c8 --- /dev/null +++ b/apps/expo-stylesheet/app/(components)/tooltip.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { Platform, Pressable, View, StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button } from '~/components/ui/button'; +import { Text } from '~/components/ui/text'; +import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip'; + +export default function TooltipScreen() { + const triggerRef = React.useRef>(null); + const insets = useSafeAreaInsets(); + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + }; + + return ( + + { + // open programmatically + triggerRef.current?.open(); + }} + /> + + + + {Platform.OS === 'web' ? 'Hover me' : 'Press me'} + + + + Add to library + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + floatingButton: { + position: 'absolute', + top: 0, + right: 0, + width: 64, + height: 64, + }, + tooltipText: { + fontSize: Platform.OS === 'web' ? 14 : 16, + }, +}); diff --git a/apps/expo-stylesheet/app/+not-found.tsx b/apps/expo-stylesheet/app/+not-found.tsx index 001d13ff..252b2798 100644 --- a/apps/expo-stylesheet/app/+not-found.tsx +++ b/apps/expo-stylesheet/app/+not-found.tsx @@ -1,15 +1,18 @@ +import { useTheme } from '@react-navigation/native'; import { Link, Stack } from 'expo-router'; import { View, Text } from 'react-native'; export default function NotFoundScreen() { + const { colors } = useTheme(); + return ( <> - This screen doesn't exist. + This screen doesn't exist. - Go to home screen! + Go to home screen! > diff --git a/apps/expo-stylesheet/app/_layout.tsx b/apps/expo-stylesheet/app/_layout.tsx index ba3f0f78..3d1040d7 100644 --- a/apps/expo-stylesheet/app/_layout.tsx +++ b/apps/expo-stylesheet/app/_layout.tsx @@ -5,7 +5,9 @@ import { SplashScreen, Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import * as React from 'react'; import { Platform, Text } from 'react-native'; +import { ThemeToggle } from '~/components/ThemeToggle'; import { NAV_THEME } from '~/lib/constants'; +import { useColorScheme } from '~/lib/useColorScheme'; const LIGHT_THEME = { ...DefaultTheme, @@ -25,12 +27,9 @@ export { SplashScreen.preventAutoHideAsync(); export default function RootLayout() { - const { colorScheme, setColorScheme, isDarkColorScheme } = { - colorScheme: 'light', - setColorScheme: (c: any) => {}, - isDarkColorScheme: false, - }; + const { colorScheme, setColorScheme } = useColorScheme(); const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false); + const isDarkColorScheme = colorScheme === 'dark'; React.useEffect(() => { (async () => { @@ -68,7 +67,15 @@ export default function RootLayout() { screenOptions={{ headerTitle(props) { return ( - {toOptions(props.children)} + + {toOptions(props.children)} + ); }, }} @@ -77,6 +84,7 @@ export default function RootLayout() { name='index' options={{ title: 'Examples', + headerRight: () => , }} /> diff --git a/apps/expo-stylesheet/app/index.tsx b/apps/expo-stylesheet/app/index.tsx index f70a694e..d1abfa30 100644 --- a/apps/expo-stylesheet/app/index.tsx +++ b/apps/expo-stylesheet/app/index.tsx @@ -1,36 +1,55 @@ import { Link } from 'expo-router'; import * as React from 'react'; -import { FlatList, Pressable, View, Text } from 'react-native'; -import { ChevronRight } from '~/lib/icons/ChevronRight'; +import { FlatList, View, StyleSheet, Platform } from 'react-native'; +import { Button } from '~/components/ui/button'; +import { Input } from '~/components/ui/input'; +import { Text } from '~/components/ui/text'; +import { ChevronRight } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; export default function ComponentsScreen() { + const [search, setSearch] = React.useState(''); const ref = React.useRef(null); + const { colors } = useTheme() as ICustomTheme; + + const data = !search + ? COMPONENTS + : COMPONENTS.filter((item) => item.toLowerCase().includes(search.toLowerCase())); return ( - + + + + ( + contentContainerStyle={styles.listContainer} + renderItem={({ item, index }) => ( - - {toOptions(item)} - - + {toOptions(item)} + + )} - ListFooterComponent={} + ListFooterComponent={} /> ); @@ -49,29 +68,69 @@ function toOptions(name: string) { } const COMPONENTS = [ - // 'accordion', - // 'alert-dialog', - // 'aspect-ratio', - // 'avatar', - // 'checkbox', - // 'collapsible', - // 'context-menu', - // 'dialog', - // 'dropdown-menu', - // 'hover-card', - // 'menubar', - // 'navigation-menu', - // 'popover', - // 'progress', - // 'radio-group', + 'accordion', + 'alert-dialog', + 'aspect-ratio', + 'avatar', + 'checkbox', + 'collapsible', + 'context-menu', + 'dialog', + 'dropdown-menu', + 'hover-card', + 'menubar', + 'navigation-menu', + 'popover', + 'progress', + 'radio-group', 'select', - // 'separator', - // 'slider', - // 'switch', - // 'table', - // 'tabs', - // 'toggle', - // 'toggle-group', - // 'toolbar', - // 'tooltip', + 'separator', + 'slider', + 'switch', + 'table', + 'tabs', + 'toggle', + 'toggle-group', + 'toolbar', + 'tooltip', ] as const; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 16, + }, + searchWrapper: { + paddingVertical: 12, + }, + listContainer: { + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + overflow: 'hidden', + }, + button: { + paddingLeft: 16, + paddingRight: 6, + borderLeftWidth: 1, + borderRightWidth: 1, + borderTopWidth: 1, + borderRadius: 0, + flexDirection: 'row', + justifyContent: 'space-between', + }, + firstButton: { + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + }, + lastButton: { + borderBottomWidth: 1, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + buttonText: { + fontSize: Platform.OS === 'web' ? 20 : 16, + }, + footerSpacing: { + paddingVertical: 16, + }, +}); diff --git a/apps/expo-stylesheet/babel.config.js b/apps/expo-stylesheet/babel.config.js index 7d507e11..9d89e131 100644 --- a/apps/expo-stylesheet/babel.config.js +++ b/apps/expo-stylesheet/babel.config.js @@ -1,6 +1,6 @@ module.exports = function (api) { api.cache(true); return { - presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'], + presets: ['babel-preset-expo'], }; }; diff --git a/apps/expo-stylesheet/components/ThemeToggle.tsx b/apps/expo-stylesheet/components/ThemeToggle.tsx new file mode 100644 index 00000000..a769d330 --- /dev/null +++ b/apps/expo-stylesheet/components/ThemeToggle.tsx @@ -0,0 +1,44 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Pressable, View, StyleSheet, Platform } from 'react-native'; +import { setAndroidNavigationBar } from '~/lib/android-navigation-bar'; +import { MoonStar, Sun } from 'lucide-react-native'; +import { useColorScheme } from '~/lib/useColorScheme'; +import { useTheme } from '@react-navigation/native'; + +export function ThemeToggle() { + const { colors } = useTheme(); + const { colorScheme, setColorScheme } = useColorScheme(); + const isDarkColorScheme = colorScheme === 'dark'; + + return ( + { + const newTheme = isDarkColorScheme ? 'light' : 'dark'; + setColorScheme(newTheme); + setAndroidNavigationBar(newTheme); + AsyncStorage.setItem('theme', newTheme); + }} + > + {({ pressed }) => ( + + {isDarkColorScheme ? ( + + ) : ( + + )} + + )} + + ); +} + +const styles = StyleSheet.create({ + iconContainer: { + flex: 1, + aspectRatio: 1, + paddingTop: 2, + justifyContent: 'center', + alignItems: 'flex-start', + paddingHorizontal: Platform.OS === 'web' ? 20 : 0, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/accordion.tsx b/apps/expo-stylesheet/components/ui/accordion.tsx new file mode 100644 index 00000000..012e4326 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/accordion.tsx @@ -0,0 +1,156 @@ +import * as AccordionPrimitive from '@rn-primitives/accordion'; +import * as React from 'react'; +import { Platform, Pressable, View, StyleSheet, ViewStyle, StyleProp } from 'react-native'; +import Animated, { + Extrapolation, + FadeIn, + FadeOutUp, + LayoutAnimationConfig, + LinearTransition, + interpolate, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import { useTheme } from '@react-navigation/native'; +import { ChevronDown } from 'lucide-react-native'; +import { TextClassContext } from '~/components/ui/text'; + +const Accordion = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, style, ...props }, ref) => { + return ( + + + {children} + + + ); +}); + +Accordion.displayName = AccordionPrimitive.Root.displayName; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ value, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([ + styles.item, + { borderBottomColor: colors.border }, + style, + ]); + + return ( + + + + ); +}); +AccordionItem.displayName = AccordionPrimitive.Item.displayName; + +const Trigger = Platform.OS === 'web' ? View : Pressable; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { style?: StyleProp } +>(({ children, style, ...props }, ref) => { + const { isExpanded } = AccordionPrimitive.useItemContext(); + const { colors } = useTheme(); + + const progress = useDerivedValue(() => + isExpanded ? withTiming(1, { duration: 250 }) : withTiming(0, { duration: 200 }) + ); + + const chevronStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${progress.value * 180}deg` }], + opacity: interpolate(progress.value, [0, 1], [1, 0.8], Extrapolation.CLAMP), + })); + + return ( + + + + + <>{children}> + + + + + + + + ); +}); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, style, ...props }, ref) => ( + + + {children} + + +)); + +function InnerContent({ + children, + style, +}: { + children: React.ReactNode; + style?: StyleProp; +}) { + if (Platform.OS === 'web') { + return {children}; + } + + return ( + + {children} + + ); +} + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; + +const styles = StyleSheet.create({ + overflowHidden: { + overflow: 'hidden', + }, + item: { + borderBottomWidth: 1, + }, + triggerProvider: { + fontSize: 16, + fontWeight: '500', + }, + triggerHeader: { + display: 'flex', + }, + trigger: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 16, + }, + contentProvider: { + fontSize: 16, + lineHeight: 24, + }, + content: { + overflow: 'hidden', + fontSize: 14, + }, + innerContent: { + paddingBottom: 16, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/alert-dialog.tsx b/apps/expo-stylesheet/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..7bd19136 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/alert-dialog.tsx @@ -0,0 +1,222 @@ +import * as React from 'react'; +import { Platform, StyleSheet, View, ViewStyle, StyleProp } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { buttonVariants, buttonTextVariants } from '~/components/ui/button'; +import * as AlertDialogPrimitive from '@rn-primitives/alert-dialog'; +import { TextClassContext } from '~/components/ui/text'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlayWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + // const { open } = AlertDialogPrimitive.useRootContext(); + return ; +}); +AlertDialogOverlayWeb.displayName = 'AlertDialogOverlayWeb'; + +const AlertDialogOverlayNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, style, ...props }, ref) => { + return ( + + + {children} + + + ); +}); +AlertDialogOverlayNative.displayName = 'AlertDialogOverlayNative'; + +const AlertDialogOverlay = Platform.select({ + web: AlertDialogOverlayWeb, + default: AlertDialogOverlayNative, +}); + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + portalHost?: string; + } +>(({ portalHost, style, ...props }, ref) => { + // const { open } = AlertDialogPrimitive.useRootContext(); + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + backgroundColor: colors.card, + borderColor: colors.border, + shadowColor: colors.text, + }, + style, + ]); + + return ( + + + + + + ); +}); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ style, ...props }: React.ComponentPropsWithoutRef) => ( + +); +AlertDialogHeader.displayName = 'AlertDialogHeader'; + +const AlertDialogFooter = ({ style, ...props }: React.ComponentPropsWithoutRef) => ( + +); +AlertDialogFooter.displayName = 'AlertDialogFooter'; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenTitleStyles = StyleSheet.flatten([styles.title, { color: colors.text }, style]); + + return ; +}); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenDescriptionStyles = StyleSheet.flatten([ + styles.description, + { color: colors.mutedText }, + style, + ]); + + return ; +}); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStylesFunc = ({ pressed }: { pressed: boolean }) => [ + buttonVariants({ colors, style }), + pressed && { backgroundColor: colors.borderMedium }, + ]; + const actionBtnStyle = + Platform.OS === 'web' ? buttonVariants({ colors, style }) : nativeStylesFunc; + + return ( + + + + ); +}); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStylesFunc = ({ pressed }: { pressed: boolean }) => [ + buttonVariants({ variant: 'outline', colors, style }), + pressed && { backgroundColor: colors.accent }, + ]; + const cancelBtnStyle = + Platform.OS === 'web' + ? buttonVariants({ variant: 'outline', colors, style }) + : nativeStylesFunc; + + return ( + + + + ); +}); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +}; + +const styles = StyleSheet.create({ + overlayWeb: { + zIndex: 50, + backgroundColor: 'rgba(0,0,0,0.8)', + justifyContent: 'center', + alignItems: 'center', + padding: 8, + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + overlayNative: { + zIndex: 50, + backgroundColor: 'rgba(0,0,0,0.8)', + justifyContent: 'center', + alignItems: 'center', + padding: 8, + }, + content: { + zIndex: 50, + maxWidth: 512, + gap: 16, + borderWidth: 1, + padding: 24, + borderRadius: 8, + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 6, + }, + header: { + flexDirection: 'column', + gap: 8, + }, + footer: { + flexDirection: Platform.OS === 'web' ? 'row' : 'column-reverse', + justifyContent: 'flex-end', + gap: 8, + }, + title: { + fontSize: 18, + lineHeight: 28, + fontWeight: '600', + }, + description: { + fontSize: 14, + lineHeight: 20, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/aspect-ratio.tsx b/apps/expo-stylesheet/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..d5f98c2e --- /dev/null +++ b/apps/expo-stylesheet/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from '@rn-primitives/aspect-ratio'; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/apps/expo-stylesheet/components/ui/avatar.tsx b/apps/expo-stylesheet/components/ui/avatar.tsx new file mode 100644 index 00000000..ade88e97 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/avatar.tsx @@ -0,0 +1,71 @@ +import * as AvatarPrimitive from '@rn-primitives/avatar'; +import * as React from 'react'; +import { StyleSheet } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { ICustomTheme } from '~/lib/constants'; + +const AvatarPrimitiveRoot = AvatarPrimitive.Root; +const AvatarPrimitiveImage = AvatarPrimitive.Image; +const AvatarPrimitiveFallback = AvatarPrimitive.Fallback; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + return ; +}); +Avatar.displayName = AvatarPrimitiveRoot.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + return ; +}); +AvatarImage.displayName = AvatarPrimitiveImage.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}); +AvatarFallback.displayName = AvatarPrimitiveFallback.displayName; + +export { Avatar, AvatarFallback, AvatarImage }; + +const styles = StyleSheet.create({ + root: { + position: 'relative', + height: 40, // h-10 + width: 40, // w-10 + flexShrink: 0, + overflow: 'hidden', + borderRadius: 50, // rounded-full + }, + image: { + aspectRatio: 1, + height: '100%', + width: '100%', + // resizeMode: 'cover', + }, + fallback: { + height: '100%', + width: '100%', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 50, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/button.tsx b/apps/expo-stylesheet/components/ui/button.tsx new file mode 100644 index 00000000..6d28b7fc --- /dev/null +++ b/apps/expo-stylesheet/components/ui/button.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { Pressable, StyleProp, StyleSheet, TextStyle, ViewStyle, Platform } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import { ICustomTheme, ICustomThemeColor } from '~/lib/constants'; +import { mergeBaseStyleWithUserStyle } from '~/lib/utils'; + +type Variant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; +type Size = 'default' | 'sm' | 'lg' | 'icon'; + +type ButtonProps = React.ComponentPropsWithoutRef & { + variant?: Variant; + size?: Size; + disabled?: boolean; + style?: StyleProp; +}; + +const Button = React.forwardRef, ButtonProps>( + ({ variant = 'default', size = 'default', disabled, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const baseButtonStyles = [ + buttonVariants({ variant, size, colors }), + disabled && styles.disabled, + ]; + + // merging base styles with user passed styles + const mergedStyles = mergeBaseStyleWithUserStyle(baseButtonStyles, style); + + return ( + + [ + ...mergedStyles, + !disabled && (pressed || hovered) && { + backgroundColor: variant === 'default' ? colors.borderMedium : colors.accent, + opacity: 0.7, + }, + ]} + ref={ref} + role='button' + disabled={disabled} + {...props} + /> + + ); + } +); + +Button.displayName = 'Button'; + +export { Button }; +export type { ButtonProps }; +const styles = StyleSheet.create({ + baseButton: { + alignItems: 'center', + justifyContent: 'center', + borderRadius: 6, + }, + baseButtonText: { + fontSize: 16, + fontWeight: '500', + }, + disabled: { + opacity: 0.5, + }, + + sizeDefault: { + height: Platform.OS === 'web' ? 40 : 44, + paddingHorizontal: 16, + paddingVertical: 8, + }, + sizeSm: { + height: 36, + borderRadius: 6, + paddingHorizontal: 12, + }, + sizeLg: { + height: Platform.OS === 'web' ? 44 : 50, + paddingHorizontal: 32, + borderRadius: 6, + }, + sizeIcon: { + height: 40, + width: 40, + }, +}); + +export const buttonVariants = ({ + variant = 'default', + size = 'default', + colors, + style, +}: { + variant?: Variant; + size?: Size; + colors: ICustomThemeColor; + style?: StyleProp; +}): ViewStyle => { + let variantStyle: ViewStyle = {}; + let sizeStyle: ViewStyle = {}; + + // === VARIANT STYLES === + if (variant === 'default') { + variantStyle = { backgroundColor: colors.buttonPrimary }; + } else if (variant === 'destructive') { + variantStyle = { backgroundColor: colors.notification }; + } else if (variant === 'outline') { + variantStyle = { + borderWidth: 1, + borderColor: colors.border, + backgroundColor: colors.buttonOutline, + }; + } else if (variant === 'secondary') { + variantStyle = { backgroundColor: colors.buttonSecondary }; + } else if (variant === 'ghost') { + variantStyle = { backgroundColor: colors.buttonGhost }; + } else if (variant === 'link') { + variantStyle = { backgroundColor: colors.buttonLink }; + } + + // === SIZE STYLES === + if (size === 'default') { + sizeStyle = styles.sizeDefault; + } else if (size === 'sm') { + sizeStyle = styles.sizeSm; + } else if (size === 'lg') { + sizeStyle = styles.sizeLg; + } else if (size === 'icon') { + sizeStyle = styles.sizeIcon; + } + + return StyleSheet.flatten([ + { + ...styles.baseButton, + ...variantStyle, + ...sizeStyle, + }, + style, + ]); +}; + +export const buttonTextVariants = ({ + variant = 'default', + size = 'default', + colors, + style, +}: { + variant?: Variant; + size?: Size; + colors: ICustomThemeColor; + style?: StyleProp; +}): TextStyle => { + let variantStyle: TextStyle = {}; + let sizeStyle: TextStyle = {}; + + // === VARIANT STYLES === + if (variant === 'default') { + variantStyle = { color: colors.buttonPrimaryText }; + } else if (variant === 'destructive') { + variantStyle = { color: '#fff' }; + } else if (variant === 'outline') { + variantStyle = { color: colors.buttonOutlineText }; + } else if (variant === 'secondary') { + variantStyle = { color: colors.buttonSecondaryText }; + } else if (variant === 'ghost') { + variantStyle = { color: colors.buttonGhostText }; + } else if (variant === 'link') { + variantStyle = { color: colors.buttonLinkText }; + } + + // === SIZE STYLES === + if (size === 'default') { + sizeStyle = { fontSize: 14 }; + } else if (size === 'sm') { + sizeStyle = { fontSize: 14 }; + } else if (size === 'lg') { + sizeStyle = { fontSize: Platform.OS === 'web' ? 14 : 16 }; + } else if (size === 'icon') { + sizeStyle = { fontSize: 16 }; + } + + return StyleSheet.flatten([ + { + ...styles.baseButtonText, + ...variantStyle, + ...sizeStyle, + }, + style, + ]); +}; diff --git a/apps/expo-stylesheet/components/ui/card.tsx b/apps/expo-stylesheet/components/ui/card.tsx new file mode 100644 index 00000000..c27f5221 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/card.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { Text, View, StyleSheet, Platform } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import type { TextRef, ViewRef } from '@rn-primitives/types'; +import { type ICustomTheme } from '~/lib/constants'; + +const Card = React.forwardRef>( + ({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); + } +); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef>( + ({ style, ...props }, ref) => +); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef>( + ({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); + } +); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef>( + ({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); + } +); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef>( + ({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + + + ); + } +); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef>( + ({ style, ...props }, ref) => +); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; + +const styles = StyleSheet.create({ + card: { + borderRadius: 8, + borderWidth: 1, + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 1 }, + shadowRadius: 2, + }, + cardHeader: { + flexDirection: 'column', + gap: 6, + padding: 24, + }, + cardTitle: { + fontSize: 24, + fontWeight: '600', + lineHeight: 24, + letterSpacing: -0.25, + }, + cardDescription: { + fontSize: Platform.OS === 'web' ? 14 : 12, + lineHeight: Platform.OS === 'web' ? 20 : 16, + }, + cardContent: { + padding: 24, + paddingTop: 0, + }, + cardFooter: { + flexDirection: 'row', + alignItems: 'center', + padding: 24, + paddingTop: 0, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/checkbox.tsx b/apps/expo-stylesheet/components/ui/checkbox.tsx new file mode 100644 index 00000000..f86ab34b --- /dev/null +++ b/apps/expo-stylesheet/components/ui/checkbox.tsx @@ -0,0 +1,62 @@ +import * as CheckboxPrimitive from '@rn-primitives/checkbox'; +import * as React from 'react'; +import { Platform, StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { Check } from 'lucide-react-native'; +import { ICustomTheme } from '~/lib/constants'; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, checked, disabled, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenStyle = StyleSheet.flatten([ + styles.base, + { + borderColor: colors.primary, + backgroundColor: checked ? colors.primary : colors.background, + opacity: disabled ? 0.5 : 1, + }, + style, + ]); + + return ( + + + + + + ); +}); + +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; + +const styles = StyleSheet.create({ + base: { + height: 16, + width: 16, + flexShrink: 0, + borderRadius: 2, + borderWidth: 1, + }, + indicator: { + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + }, +}); diff --git a/apps/expo-stylesheet/components/ui/collapsible.tsx b/apps/expo-stylesheet/components/ui/collapsible.tsx new file mode 100644 index 00000000..a2d66dd5 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from '@rn-primitives/collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.Trigger; + +const CollapsibleContent = CollapsiblePrimitive.Content; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/apps/expo-stylesheet/components/ui/context-menu.tsx b/apps/expo-stylesheet/components/ui/context-menu.tsx new file mode 100644 index 00000000..3eb5d7af --- /dev/null +++ b/apps/expo-stylesheet/components/ui/context-menu.tsx @@ -0,0 +1,379 @@ +import * as ContextMenuPrimitive from '@rn-primitives/context-menu'; +import * as React from 'react'; +import { Platform, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { Check, ChevronDown, ChevronRight, ChevronUp } from 'lucide-react-native'; +import { TextClassContext } from '~/components/ui/text'; +import { type ICustomTheme } from '~/lib/constants'; + +const ContextMenu = ContextMenuPrimitive.Root; + +// const ContextMenuTrigger = ContextMenuPrimitive.Trigger; +const ContextMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const flattenStyle: ViewStyle = StyleSheet.flatten([{ display: 'flex' }, style]); + + return ; +}); +ContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName; + +const ContextMenuGroup = ContextMenuPrimitive.Group; +const ContextMenuSub = ContextMenuPrimitive.Sub; +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const { open } = ContextMenuPrimitive.useSubContext(); + const Icon = Platform.OS === 'web' ? ChevronRight : open ? ChevronUp : ChevronDown; + const nativeStyles = ({ pressed }: { pressed: boolean }) => [ + styles.itemBase, + open && { backgroundColor: colors.card }, + inset && { paddingLeft: 32 }, + style, + pressed && { backgroundColor: colors.accent }, + ]; + const webStyles = StyleSheet.flatten([ + styles.itemBase, + open && { backgroundColor: colors.card }, + inset && { paddingLeft: 32 }, + style, + ]); + + return ( + + + <>{children}> + + + + ); +}); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + overlayStyle?: StyleProp; + portalHost?: string; + } +>(({ overlayStyle, portalHost, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyle = StyleSheet.flatten([ + styles.content, + { + borderColor: colors.border, + backgroundColor: colors.card, + }, + style, + ]); + + return ( + + + + + + ); +}); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, disabled, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyles = ({ pressed }: { pressed: boolean }) => [ + styles.itemBase, + inset && { paddingLeft: 32 }, + disabled && { opacity: 0.5 }, + style, + pressed && { backgroundColor: colors.accent }, + ]; + const webStyles = StyleSheet.flatten([ + styles.itemBase, + inset && { paddingLeft: 32 }, + disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + + + ); +}); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ disabled, children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyles = ({ pressed }: { pressed: boolean }) => [ + styles.checkboxItem, + disabled && { opacity: 0.5 }, + style, + { + backgroundColor: pressed ? colors.accent : 'transparent', + }, + ]; + const webStyles = StyleSheet.flatten([styles.checkboxItem, disabled && { opacity: 0.5 }, style]); + + return ( + + + + + + + + <>{children}> + + + ); +}); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ disabled, children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyles = ({ pressed }: { pressed: boolean }) => [ + styles.radioItem, + disabled && { opacity: 0.5 }, + style, + { + backgroundColor: pressed ? colors.accent : 'transparent', + }, + ]; + const webStyles = StyleSheet.flatten([styles.radioItem, disabled && { opacity: 0.5 }, style]); + + return ( + + + + + + + + <>{children}> + + + ); +}); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ inset, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([ + styles.label, + { color: colors.text }, + inset && { paddingLeft: 32 }, + style, + ]); + + return ; +}); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([ + styles.separator, + { backgroundColor: colors.border }, + style, + ]); + + return ; +}); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ style, ...props }: React.ComponentPropsWithoutRef) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}; +ContextMenuShortcut.displayName = 'ContextMenuShortcut'; + +export { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuTrigger, + ContextMenuGroup, + ContextMenuItem, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, +}; + +const styles = StyleSheet.create({ + content: { + zIndex: 50, + minWidth: 128, + overflow: 'hidden', + borderRadius: 6, + borderWidth: 1, + marginTop: 4, + padding: 4, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowOffset: { width: 0, height: 1 }, + shadowRadius: 3, + }, + itemBase: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + gap: 8, + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 2, + }, + checkboxItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + radioItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + indicatorBox: { + position: 'absolute', + left: 8, + height: 14, + width: 14, + alignItems: 'center', + justifyContent: 'center', + }, + label: { + paddingHorizontal: 8, + paddingVertical: 6, + fontSize: 14, + fontWeight: '600', + }, + separator: { + marginHorizontal: -4, + marginVertical: 4, + height: StyleSheet.hairlineWidth, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/dialog.tsx b/apps/expo-stylesheet/components/ui/dialog.tsx new file mode 100644 index 00000000..ab65cba7 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/dialog.tsx @@ -0,0 +1,192 @@ +import * as DialogPrimitive from '@rn-primitives/dialog'; +import * as React from 'react'; +import { Platform, StyleSheet, View, ViewStyle, StyleProp } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { X } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { ICustomTheme } from '~/lib/constants'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlayWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + return ( + ]} + {...props} + /> + ); +}); + +DialogOverlayWeb.displayName = 'DialogOverlayWeb'; + +const DialogOverlayNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, style, ...props }, ref) => { + return ( + ]} + {...props} + > + + <>{children}> + + + ); +}); + +DialogOverlayNative.displayName = 'DialogOverlayNative'; + +const DialogOverlay = Platform.select({ + web: DialogOverlayWeb, + default: DialogOverlayNative, +}); + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + portalHost?: string; + } +>(({ children, portalHost, style, ...props }, ref) => { + const { open } = DialogPrimitive.useRootContext(); + const { colors } = useTheme(); + + return ( + + + + {children} + + + + + + + ); +}); + +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ style, ...props }: React.ComponentPropsWithoutRef) => ( + +); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ style, ...props }: React.ComponentPropsWithoutRef) => ( + +); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([styles.title, { color: colors.text }, style]); + + return ; +}); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenStyle = StyleSheet.flatten([styles.description, { color: colors.mutedText }, style]); + + return ; +}); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; + +const styles = StyleSheet.create({ + overlayWeb: { + backgroundColor: 'rgba(0,0,0,0.8)', + justifyContent: 'center', + alignItems: 'center', + padding: 8, + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + overlayNative: { + backgroundColor: 'rgba(0,0,0,0.8)', + justifyContent: 'center', + alignItems: 'center', + padding: 8, + }, + content: { + maxWidth: 512, + gap: 16, + borderWidth: 1, + padding: 24, + borderRadius: 8, + shadowOpacity: 0.2, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + }, + closeButton: { + position: 'absolute', + right: 16, + top: 16, + padding: 2, + borderRadius: 2, + opacity: 0.7, + }, + header: { + flexDirection: 'column', + gap: 6, + }, + footer: { + flexDirection: Platform.OS === 'web' ? 'row' : 'column-reverse', + justifyContent: Platform.OS === 'web' ? 'flex-end' : 'flex-start', + gap: 8, + }, + title: { + fontSize: 18, + fontWeight: '600', + letterSpacing: -0.25, + }, + description: { + fontSize: 14, + lineHeight: 20, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/dropdown-menu.tsx b/apps/expo-stylesheet/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..86c33de2 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/dropdown-menu.tsx @@ -0,0 +1,355 @@ +import * as DropdownMenuPrimitive from '@rn-primitives/dropdown-menu'; +import * as React from 'react'; +import { Platform, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native'; +import { Check, ChevronDown, ChevronRight, ChevronUp } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import { ICustomTheme } from '~/lib/constants'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const { open } = DropdownMenuPrimitive.useSubContext(); + const Icon = Platform.OS === 'web' ? ChevronRight : open ? ChevronUp : ChevronDown; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.item, + { backgroundColor: open ? colors.accent : 'transparent' }, + inset && { paddingLeft: 32 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.item, + { backgroundColor: open ? colors.accent : 'transparent' }, + inset && { paddingLeft: 32 }, + style, + ]); + + return ( + + + <>{children}> + + + + ); +}); +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + overlayStyle?: StyleProp; + portalHost?: string; + } +>(({ overlayStyle, portalHost, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + borderColor: colors.border, + backgroundColor: colors.card, + shadowColor: colors.text, + }, + style, + ]); + + return ( + + + + + + ); +}); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, style, disabled, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.item, + inset && { paddingLeft: 32 }, + disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.item, + inset && { paddingLeft: 32 }, + disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + + + ); +}); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ children, checked, style, disabled, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + [ + styles.checkboxItem, + disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]} + {...props} + > + + + + + + <>{children}> + + ); +}); +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ children, style, disabled, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + [ + styles.radioItem, + disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]} + {...props} + > + + + + + + <>{children}> + + ); +}); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ inset, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle: TextStyle = StyleSheet.flatten([ + { + paddingHorizontal: 8, + paddingVertical: 6, + fontSize: 14, + fontWeight: '600', + color: colors.text, + }, + inset && { paddingLeft: 32 }, + style, + ]); + + return ; +}); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenStyle = StyleSheet.flatten([ + { + marginHorizontal: -4, + marginVertical: 4, + height: StyleSheet.hairlineWidth, + backgroundColor: colors.borderMedium, + }, + style, + ]); + + return ; +}); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = (props: React.ComponentPropsWithoutRef) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}; +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; + +const styles = StyleSheet.create({ + content: { + zIndex: 50, + minWidth: 128, + overflow: 'hidden', + borderRadius: 6, + borderWidth: 1, + padding: 4, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + item: { + position: 'relative', + flexDirection: 'row', + gap: 8, + alignItems: 'center', + borderRadius: 2, + paddingHorizontal: 8, + paddingVertical: 6, + }, + checkboxItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + radioItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + iconWrapper: { + position: 'absolute', + left: 8, + height: 14, + width: 14, + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/apps/expo-stylesheet/components/ui/hover-card.tsx b/apps/expo-stylesheet/components/ui/hover-card.tsx new file mode 100644 index 00000000..4b775498 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/hover-card.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Platform, StyleSheet } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; +import { TextClassContext } from '~/components/ui/text'; +import * as HoverCardPrimitive from '@rn-primitives/hover-card'; +import { useTheme } from '@react-navigation/native'; + +const HoverCard = HoverCardPrimitive.Root; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ align = 'center', sideOffset = 4, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.base, + { + borderColor: colors.border, + backgroundColor: colors.card, + shadowColor: colors.text, + }, + style, + ]); + + return ( + + + + + + + + + + ); +}); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardContent, HoverCardTrigger }; + +const styles = StyleSheet.create({ + base: { + zIndex: 50, + width: 256, + borderRadius: 6, + borderWidth: 1, + padding: 16, + shadowOpacity: 0.05, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 4, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/input.tsx b/apps/expo-stylesheet/components/ui/input.tsx new file mode 100644 index 00000000..4aefd4d1 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/input.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { TextInput, StyleSheet, Platform } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const Input = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, editable = true, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}); + +Input.displayName = 'Input'; + +export { Input }; + +const styles = StyleSheet.create({ + base: { + height: 40, + borderRadius: 6, + borderWidth: 1, + paddingHorizontal: 12, + fontSize: Platform.OS === 'web' ? 14 : 16, + lineHeight: 20, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/label.tsx b/apps/expo-stylesheet/components/ui/label.tsx new file mode 100644 index 00000000..02dc700b --- /dev/null +++ b/apps/expo-stylesheet/components/ui/label.tsx @@ -0,0 +1,35 @@ +import * as LabelPrimitive from '@rn-primitives/label'; +import * as React from 'react'; +import { Platform, StyleSheet } from 'react-native'; +import { useTheme } from '@react-navigation/native'; + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, onPress, onLongPress, onPressIn, onPressOut, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([styles.text, { color: colors.text }, style]); + + return ( + + + + ); +}); + +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; + +const styles = StyleSheet.create({ + text: { + fontSize: 14, + fontWeight: '500', + lineHeight: Platform.OS === 'web' ? 16 : 20, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/menubar.tsx b/apps/expo-stylesheet/components/ui/menubar.tsx new file mode 100644 index 00000000..b9aa14d5 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/menubar.tsx @@ -0,0 +1,444 @@ +import * as MenubarPrimitive from '@rn-primitives/menubar'; +import * as React from 'react'; +import { Platform, Text, View, StyleSheet, ViewStyle, StyleProp } from 'react-native'; +import { Check, ChevronDown, ChevronRight, ChevronUp } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import { ICustomTheme } from '~/lib/constants'; + +const MenubarMenu = MenubarPrimitive.Menu; + +const MenubarGroup = MenubarPrimitive.Group; + +const MenubarPortal = MenubarPrimitive.Portal; + +const MenubarSub = MenubarPrimitive.Sub; + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup; + +const Menubar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +Menubar.displayName = MenubarPrimitive.Root.displayName; + +const MenubarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { value } = MenubarPrimitive.useRootContext(); + const { value: itemValue } = MenubarPrimitive.useMenuContext(); + const { colors } = useTheme() as ICustomTheme; + const isActive = value === itemValue; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.trigger, + { backgroundColor: isActive ? colors.accent : 'transparent' }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.trigger, + { backgroundColor: isActive ? colors.accent : 'transparent' }, + style, + ]); + + return ( + + + + ); +}); +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, children, style, ...props }, ref) => { + const { open } = MenubarPrimitive.useSubContext(); + const { colors } = useTheme() as ICustomTheme; + const Icon = Platform.OS === 'web' ? ChevronRight : open ? ChevronUp : ChevronDown; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.subTrigger, + { backgroundColor: open ? colors.accent : 'transparent' }, + inset && { paddingLeft: 32 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.subTrigger, + { backgroundColor: open ? colors.accent : 'transparent' }, + inset && { paddingLeft: 32 }, + style, + ]); + + return ( + + + <>{children}> + + + + ); +}); +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; + +const MenubarSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; + +const MenubarContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + portalHost?: string; + } +>(({ portalHost, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + borderColor: colors.border, + backgroundColor: colors.card, + shadowColor: colors.text, + }, + style, + ]); + + return ( + + + + ); +}); +MenubarContent.displayName = MenubarPrimitive.Content.displayName; + +const MenubarItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + style?: StyleProp; + } +>(({ inset, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.item, + inset && { paddingLeft: 32 }, + props.disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.item, + inset && { paddingLeft: 32 }, + props.disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + + + ); +}); +MenubarItem.displayName = MenubarPrimitive.Item.displayName; + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, checked, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.checkboxItem, + props.disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + ]; + const webStyle = StyleSheet.flatten([styles.checkboxItem, props.disabled && { opacity: 0.5 }]); + + return ( + + + + + + + + <>{children}> + + + ); +}); +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; + +const MenubarRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ children, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + styles.radioItem, + props.disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + const webStyle = StyleSheet.flatten([ + styles.radioItem, + props.disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + + + + + + + <>{children}> + + + ); +}); +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; + +const MenubarLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ inset, style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +MenubarLabel.displayName = MenubarPrimitive.Label.displayName; + +const MenubarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([ + styles.separator, + { backgroundColor: colors.border }, + style, + ]); + + return ; +}); +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; + +const MenubarShortcut = ({ style, ...props }: React.ComponentPropsWithoutRef) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}; +MenubarShortcut.displayName = 'MenubarShortcut'; + +export { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarGroup, + MenubarItem, + MenubarLabel, + MenubarMenu, + MenubarPortal, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +}; + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + height: 44, + alignItems: 'center', + borderRadius: 6, + borderWidth: 1, + padding: 4, + }, + trigger: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingHorizontal: Platform.OS === 'web' ? 12 : 16, + paddingVertical: 6, + }, + subTrigger: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + borderRadius: 2, + paddingHorizontal: 8, + paddingVertical: 6, + }, + subContent: { + zIndex: 50, + minWidth: 128, + overflow: 'hidden', + borderRadius: 6, + borderWidth: 1, + marginTop: 4, + padding: 4, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + content: { + zIndex: 50, + minWidth: 128, + overflow: 'hidden', + borderRadius: 6, + borderWidth: 1, + padding: 4, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + item: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + gap: 8, + borderRadius: 2, + paddingHorizontal: 8, + paddingVertical: 6, + }, + checkboxItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + radioItem: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + borderRadius: 2, + paddingVertical: 6, + paddingLeft: 32, + paddingRight: 8, + }, + indicatorBox: { + position: 'absolute', + left: 8, + height: 14, + width: 14, + alignItems: 'center', + justifyContent: 'center', + }, + label: { + paddingHorizontal: 8, + paddingVertical: 6, + fontSize: 14, + fontWeight: '600', + }, + separator: { + marginHorizontal: -4, + marginVertical: 4, + height: 1, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/navigation-menu.tsx b/apps/expo-stylesheet/components/ui/navigation-menu.tsx new file mode 100644 index 00000000..8ff762c7 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/navigation-menu.tsx @@ -0,0 +1,268 @@ +import * as NavigationMenuPrimitive from '@rn-primitives/navigation-menu'; +import * as React from 'react'; +import { Platform, View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import Animated, { + Extrapolation, + FadeInLeft, + FadeOutLeft, + interpolate, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import { ChevronDown } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { ICustomTheme } from '~/lib/constants'; + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, style, ...props }, ref) => ( + + {children} + {Platform.OS === 'web' && } + +)); +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const flattenStyle = StyleSheet.flatten([styles.list, style]); + + return ; +}); +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; + +const NavigationMenuItem = NavigationMenuPrimitive.Item; + +const navigationMenuTriggerStyle = { + flexDirection: 'row', + height: 40, + width: 'auto', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 6, + paddingHorizontal: 9, + paddingVertical: 8, +} as ViewStyle; + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ children, disabled, style, ...props }, ref) => { + const { value } = NavigationMenuPrimitive.useRootContext(); + const { value: itemValue } = NavigationMenuPrimitive.useItemContext(); + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + navigationMenuTriggerStyle, + style, + { + gap: 6, + backgroundColor: value === itemValue ? colors.accent : 'transparent', + opacity: disabled ? 0.5 : 1, + }, + pressed && { backgroundColor: colors.accent }, + ]; + const webStyle = StyleSheet.flatten([ + navigationMenuTriggerStyle, + style, + { + gap: 6, + backgroundColor: value === itemValue ? colors.accent : 'transparent', + opacity: disabled ? 0.5 : 1, + }, + ]); + + const progress = useDerivedValue(() => + value === itemValue ? withTiming(1, { duration: 250 }) : withTiming(0, { duration: 200 }) + ); + const chevronStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${progress.value * 180}deg` }], + opacity: interpolate(progress.value, [0, 1], [1, 0.8], Extrapolation.CLAMP), + })); + + return ( + + <>{children}> + + + + + ); +}); +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + portalHost?: string; + } +>(({ children, portalHost, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + borderColor: colors.border, + backgroundColor: colors.card, + }, + style, + ]); + + return ( + + + + {children} + + + + ); +}); +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; + +// const NavigationMenuLink = NavigationMenuPrimitive.Link; +const NavigationMenuLink = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const nativeStyle = ({ pressed }: { pressed: boolean }) => [ + style, + pressed && { backgroundColor: colors.accent }, + ]; + const webStyle: ViewStyle = StyleSheet.flatten([{}, style]); + + return ( + + ); +}); +NavigationMenuLink.displayName = NavigationMenuPrimitive.Link.displayName; + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + + + + + ); +}); +NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName; + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + + + ); +}); +NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName; + +export { + NavigationMenu, + NavigationMenuContent, + NavigationMenuIndicator, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + NavigationMenuViewport, + navigationMenuTriggerStyle, +}; + +const styles = StyleSheet.create({ + root: { + position: 'relative', + zIndex: 10, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + list: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + }, + content: { + width: '100%', + borderWidth: 1, + borderRadius: 8, + overflow: 'hidden', + shadowOpacity: 0.2, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + }, + viewportContainer: { + position: 'absolute', + left: 0, + top: '100%', + justifyContent: 'center', + }, + viewport: { + position: 'relative', + marginTop: 6, + width: '100%', + overflow: 'hidden', + borderRadius: 6, + borderWidth: 1, + shadowOpacity: 0.2, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + }, + indicator: { + top: '100%', + zIndex: 1, + height: 6, + alignItems: 'flex-end', + justifyContent: 'center', + overflow: 'hidden', + }, + indicatorSquare: { + position: 'relative', + top: '60%', + height: 8, + width: 8, + transform: [{ rotate: '45deg' }], + borderTopLeftRadius: 2, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/popover.tsx b/apps/expo-stylesheet/components/ui/popover.tsx new file mode 100644 index 00000000..04d3cce5 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/popover.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { Platform, StyleSheet } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { TextClassContext } from '~/components/ui/text'; +import * as PopoverPrimitive from '@rn-primitives/popover'; +import { useTheme } from '@react-navigation/native'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { portalHost?: string } +>(({ align = 'center', sideOffset = 4, portalHost, style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + backgroundColor: colors.card, + borderColor: colors.border, + shadowColor: colors.text, + }, + style, + ]); + + return ( + + + + + + + + + + ); +}); + +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverContent, PopoverTrigger }; + +const styles = StyleSheet.create({ + content: { + zIndex: 50, + width: 288, + borderRadius: 6, + borderWidth: 1, + padding: 16, + shadowOpacity: 0.05, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 4, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/progress.tsx b/apps/expo-stylesheet/components/ui/progress.tsx new file mode 100644 index 00000000..9e77c7fd --- /dev/null +++ b/apps/expo-stylesheet/components/ui/progress.tsx @@ -0,0 +1,90 @@ +import * as ProgressPrimitive from '@rn-primitives/progress'; +import * as React from 'react'; +import { Platform, View, StyleSheet, ViewStyle } from 'react-native'; +import Animated, { + Extrapolation, + interpolate, + useAnimatedStyle, + useDerivedValue, + withSpring, +} from 'react-native-reanimated'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + indicatorStyle?: ViewStyle; + } +>(({ style, value, indicatorStyle, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenStyle = StyleSheet.flatten([styles.root, { backgroundColor: colors.accent }, style]); + + return ( + + + + ); +}); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; + +function Indicator({ + value, + indicatorStyle, +}: { + value: number | undefined | null; + indicatorStyle?: ViewStyle; +}) { + const { colors } = useTheme(); + const progress = useDerivedValue(() => value ?? 0); + const flattenWebIndicatorStyle = StyleSheet.flatten([styles.webIndicator, indicatorStyle]); + + const animatedStyle = useAnimatedStyle(() => { + return { + width: withSpring( + `${interpolate(progress.value, [0, 100], [1, 100], Extrapolation.CLAMP)}%`, + { overshootClamping: true } + ), + height: '100%', + backgroundColor: colors.primary, + ...indicatorStyle, + }; + }); + + if (Platform.OS === 'web') { + return ( + + + + ); + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + root: { + position: 'relative', + height: Platform.OS === 'web' ? 16 : 14, + width: '100%', + overflow: 'hidden', + borderRadius: 9999, + }, + webIndicator: { + height: '100%', + width: '100%', + flex: 1, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/radio-group.tsx b/apps/expo-stylesheet/components/ui/radio-group.tsx new file mode 100644 index 00000000..8d071898 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/radio-group.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import * as RadioGroupPrimitive from '@rn-primitives/radio-group'; +import { useTheme } from '@react-navigation/native'; + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const flattenStyle = StyleSheet.flatten([styles.group, style]); + + return ; +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, disabled, ...props }, ref) => { + const { colors } = useTheme(); + const flattenRadioItemStyle = StyleSheet.flatten([ + styles.item, + { + borderColor: colors.primary, + opacity: disabled ? 0.5 : 1, + }, + style, + ]); + + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; + +const styles = StyleSheet.create({ + group: { + flexDirection: 'column', + gap: 8, + }, + item: { + aspectRatio: 1, + height: 16, + width: 16, + borderRadius: 9999, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + }, + indicatorWrapper: { + alignItems: 'center', + justifyContent: 'center', + }, + indicator: { + aspectRatio: 1, + height: 9, + width: 9, + borderRadius: 9999, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/select.tsx b/apps/expo-stylesheet/components/ui/select.tsx index a3f58565..d45de8d4 100644 --- a/apps/expo-stylesheet/components/ui/select.tsx +++ b/apps/expo-stylesheet/components/ui/select.tsx @@ -1,10 +1,10 @@ import * as SelectPrimitive from '@rn-primitives/select'; import * as React from 'react'; -import { Platform, View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import { Platform, View, StyleSheet, ViewStyle, StyleProp } from 'react-native'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; -import { Check } from '~/lib/icons/Check'; -import { ChevronDown } from '~/lib/icons/ChevronDown'; -import { ChevronUp } from '~/lib/icons/ChevronUp'; +import { useTheme } from '@react-navigation/native'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react-native'; +import { type ICustomTheme } from '~/lib/constants'; type Option = SelectPrimitive.Option; @@ -16,49 +16,39 @@ const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ style, children, ...props }, ref) => ( - - {({ pressed }) => ( - , - ])} - > - <>{children}> - - - )} - -)); + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ children, style, disabled, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([ + styles.trigger, + { backgroundColor: colors.card, borderColor: colors.border }, + disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + <>{children}> + + + ); +}); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; /** * Platform: WEB ONLY */ -const SelectScrollUpButton = ({ - className, - ...props -}: React.ComponentPropsWithoutRef) => { - if (Platform.OS !== 'web') { - return null; - } +const SelectScrollUpButton = ( + props: React.ComponentPropsWithoutRef +) => { + const { colors } = useTheme(); + if (Platform.OS !== 'web') return null; + return ( - - + + ); }; @@ -66,67 +56,52 @@ const SelectScrollUpButton = ({ /** * Platform: WEB ONLY */ -const SelectScrollDownButton = ({ - className, - ...props -}: React.ComponentPropsWithoutRef) => { - if (Platform.OS !== 'web') { - return null; - } +const SelectScrollDownButton = ( + props: React.ComponentPropsWithoutRef +) => { + const { colors } = useTheme(); + if (Platform.OS !== 'web') return null; return ( - - + + ); }; const SelectContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { portalHost?: string } ->(({ style, children, position = 'popper', portalHost, ...props }, ref) => { + React.ComponentPropsWithoutRef & { + portalHost?: string; + } +>(({ children, position = 'popper', portalHost, style, ...props }, ref) => { + // const { open } = SelectPrimitive.useRootContext(); + const { colors } = useTheme(); + const flattenOverlayStyles = StyleSheet.flatten([ + StyleSheet.absoluteFillObject, + { backgroundColor: 'rgba(0,0,0,0.3)' }, + ]); + const flattenContentStyles = StyleSheet.flatten([ + styles.content, + { + backgroundColor: colors.card, + borderColor: colors.border, + shadowColor: colors.text, + }, + style, + ]); // single style object + return ( - + - - {children} - + {children} @@ -139,83 +114,129 @@ SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ style, ...props }, ref) => ( - -)); +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + const flattenStyle = StyleSheet.flatten([styles.label, { color: colors.text }, style]); + + return ; +}); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ style, children, ...props }, ref) => ( - , - ])} - {...props} - > - & { + style?: StyleProp; + } +>(({ children, disabled, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenItemTextStyle = StyleSheet.flatten([styles.itemText, { color: colors.text }]); + const webIndicatorStyle = StyleSheet.flatten([styles.item, disabled && { opacity: 0.5 }, style]); + const nativeIndicatorStyle = ({ pressed }: { pressed: boolean }) => [ + styles.item, + disabled && { opacity: 0.5 }, + pressed && { backgroundColor: colors.accent }, + style, + ]; + + return ( + - - - - - - -)); + + + + + + + + ); +}); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ style, ...props }, ref) => ( - -)); +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; +const styles = StyleSheet.create({ + trigger: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + height: 44, + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 12, + }, + scrollButton: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 4, + }, + content: { + borderWidth: 1, + borderRadius: 6, + paddingVertical: 8, + paddingHorizontal: 4, + maxHeight: 300, + minWidth: 120, + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 4, + }, + viewport: { + padding: 4, + }, + label: { + paddingVertical: 6, + paddingBottom: 8, + paddingLeft: 40, + paddingRight: 8, + fontSize: 14, + fontWeight: '600', + }, + item: { + position: 'relative', + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingLeft: 40, + paddingRight: 8, + borderRadius: 6, + }, + itemIndicatorWrapper: { + position: 'absolute', + left: 14, + height: 14, + width: 14, + paddingTop: 2, + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'center', + }, + itemText: { + fontSize: Platform.OS === 'web' ? 14 : 16, + lineHeight: Platform.OS === 'web' ? 28 : 24, + }, + separator: { + height: 1, + marginVertical: 8, + }, +}); + export { Select, SelectContent, diff --git a/apps/expo-stylesheet/components/ui/separator.tsx b/apps/expo-stylesheet/components/ui/separator.tsx new file mode 100644 index 00000000..105865c4 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@rn-primitives/separator'; +import { useTheme } from '@react-navigation/native'; +import { ViewStyle } from 'react-native'; +import { type ICustomTheme } from '~/lib/constants'; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ orientation = 'horizontal', decorative = true, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + const separatorStyle: ViewStyle = + orientation === 'horizontal' ? { height: 1, width: '100%' } : { width: 1, height: '100%' }; + + return ( + + ); +}); + +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/apps/expo-stylesheet/components/ui/switch.tsx b/apps/expo-stylesheet/components/ui/switch.tsx new file mode 100644 index 00000000..5e5f1cde --- /dev/null +++ b/apps/expo-stylesheet/components/ui/switch.tsx @@ -0,0 +1,138 @@ +import * as SwitchPrimitives from '@rn-primitives/switch'; +import * as React from 'react'; +import { Platform, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import Animated, { + interpolateColor, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const SwitchWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, checked, disabled, ...props }, ref) => { + const { colors } = useTheme(); + const flattenRootStyle = StyleSheet.flatten([ + styles.webRoot, + { + backgroundColor: checked ? colors.primary : colors.border, + opacity: disabled ? 0.5 : 1, + }, + style, + ]); + const flattenThumbStyle = StyleSheet.flatten([ + styles.webThumb, + { + backgroundColor: colors.background, + shadowColor: colors.text, + transform: [{ translateX: checked ? 20 : 0 }], + }, + ]); + + return ( + + + + ); +}); +SwitchWeb.displayName = 'SwitchWeb'; + +const SwitchNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + style?: StyleProp; + } +>(({ style, checked, disabled, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const translateX = useDerivedValue(() => (checked ? 18 : 0)); + const animatedRootStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor(translateX.value, [0, 18], [colors.muted, colors.primary]), + opacity: disabled ? 0.5 : 1, + }; + }); + + const animatedThumbStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: withTiming(translateX.value, { duration: 200 }) }], + })); + + return ( + + + + + + + + ); +}); +SwitchNative.displayName = 'SwitchNative'; + +const Switch = Platform.select({ + web: SwitchWeb, + default: SwitchNative, +}); + +export { Switch }; + +const styles = StyleSheet.create({ + webRoot: { + flexDirection: 'row', + height: 24, + width: 44, + flexShrink: 0, + cursor: 'pointer', + alignItems: 'center', + borderRadius: 9999, + borderWidth: 2, + borderColor: 'transparent', + }, + webThumb: { + pointerEvents: 'none', + height: 20, + width: 20, + borderRadius: 9999, + shadowOpacity: 0.1, + shadowRadius: 2, + }, + nativeRoot: { + height: 28, + width: 46, + borderRadius: 9999, + }, + nativeInnerRoot: { + flexDirection: 'row', + height: 28, + width: 46, + flexShrink: 0, + alignItems: 'center', + borderRadius: 9999, + borderWidth: 2, + borderColor: 'transparent', + }, + nativeThumb: { + height: 24, + width: 24, + borderRadius: 9999, + shadowOpacity: 0.25, + shadowRadius: 3, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/table.tsx b/apps/expo-stylesheet/components/ui/table.tsx new file mode 100644 index 00000000..757cab95 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/table.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import * as TablePrimitive from '@rn-primitives/table'; +import { StyleSheet, ViewStyle, StyleProp } from 'react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import { type ICustomTheme } from '~/lib/constants'; + +const Table = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => ( + +)); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme(); + + return ( + + ); +}); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + ); +}); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { style?: StyleProp } +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + [ + styles.row, + { borderColor: colors.borderMedium }, + style, + pressed && { backgroundColor: colors.accent }, + hovered && { backgroundColor: colors.accent }, + ]} + {...props} + /> + ); +}); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + + return ( + + + + ); +}); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + return ; +}); +TableCell.displayName = 'TableCell'; + +export { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow }; + +const styles = StyleSheet.create({ + table: { + width: '100%', + fontSize: 14, + }, + header: { + borderBottomWidth: StyleSheet.hairlineWidth, + }, + body: { + flex: 1, + minHeight: 2, + }, + footer: { + fontWeight: '500', + borderBottomWidth: 0, + }, + row: { + flexDirection: 'row', + borderBottomWidth: StyleSheet.hairlineWidth, + }, + head: { + height: 48, + paddingHorizontal: 16, + textAlign: 'left', + justifyContent: 'center', + fontWeight: '500', + }, + cell: { + padding: 16, + verticalAlign: 'middle', + }, +}); diff --git a/apps/expo-stylesheet/components/ui/tabs.tsx b/apps/expo-stylesheet/components/ui/tabs.tsx new file mode 100644 index 00000000..6c167ed4 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/tabs.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { StyleSheet, ViewStyle, StyleProp } from 'react-native'; +import { TextClassContext } from '~/components/ui/text'; +import * as TabsPrimitive from '@rn-primitives/tabs'; +import { useTheme } from '@react-navigation/native'; +import { type ICustomTheme } from '~/lib/constants'; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenStyle = StyleSheet.flatten([styles.list, { backgroundColor: colors.muted }, style]); + + return ; +}); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { style?: StyleProp } +>(({ style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const { value } = TabsPrimitive.useRootContext(); + const isActive = value === props.value; + + return ( + + + + ); +}); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ style, ...props }, ref) => ); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsContent, TabsList, TabsTrigger }; + +const styles = StyleSheet.create({ + list: { + height: 40, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8, + padding: 4, + }, + trigger: { + alignItems: 'center', + justifyContent: 'center', + borderRadius: 2, + paddingHorizontal: 12, + paddingVertical: 6, + shadowOpacity: 0.1, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + }, + triggerText: { + fontSize: 14, + fontWeight: '500', + }, +}); diff --git a/apps/expo-stylesheet/components/ui/text.tsx b/apps/expo-stylesheet/components/ui/text.tsx new file mode 100644 index 00000000..3e6f5ecf --- /dev/null +++ b/apps/expo-stylesheet/components/ui/text.tsx @@ -0,0 +1,31 @@ +import { useTheme } from '@react-navigation/native'; +import * as Slot from '@rn-primitives/slot'; +import type { SlottableTextProps, TextRef } from '@rn-primitives/types'; +import * as React from 'react'; +import { Platform, Text as RNText, StyleProp, TextStyle } from 'react-native'; +import { type ICustomTheme } from '~/lib/constants'; + +const TextClassContext = React.createContext>(undefined); + +const Text = React.forwardRef( + ({ asChild = false, style, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const contextStyle = React.useContext(TextClassContext); + const Component = asChild ? Slot.Text : RNText; + + return ( + + ); + } +); +Text.displayName = 'Text'; + +export { Text, TextClassContext }; diff --git a/apps/expo-stylesheet/components/ui/toggle-group.tsx b/apps/expo-stylesheet/components/ui/toggle-group.tsx new file mode 100644 index 00000000..d289a485 --- /dev/null +++ b/apps/expo-stylesheet/components/ui/toggle-group.tsx @@ -0,0 +1,102 @@ +import type { LucideIcon } from 'lucide-react-native'; +import * as React from 'react'; +import { useTheme } from '@react-navigation/native'; +import { StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { toggleTextVariants, ToggleProps, toggleVariants } from '~/components/ui/toggle'; +import { TextClassContext } from '~/components/ui/text'; +import * as ToggleGroupPrimitive from '@rn-primitives/toggle-group'; +import { type ICustomTheme } from '~/lib/constants'; + +const ToggleGroupContext = React.createContext(null); + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & ToggleProps +>(({ style, variant, size, children, ...props }, ref) => { + const flattenStyle = StyleSheet.flatten([styles.group, style]); + + return ( + + + {children} + + + ); +}); + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; + +function useToggleGroupContext() { + const context = React.useContext(ToggleGroupContext); + if (context === null) { + throw new Error( + 'ToggleGroup compound components cannot be rendered outside the ToggleGroup component' + ); + } + return context; +} + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + ToggleProps & { + style?: StyleProp; + } +>(({ children, variant, size, style, ...props }, ref) => { + const context = useToggleGroupContext(); + const { value } = ToggleGroupPrimitive.useRootContext(); + const { colors } = useTheme() as ICustomTheme; + + const isSelected = ToggleGroupPrimitive.utils.getIsSelected(value, props.value); + const flattenToggleGroupStyle = StyleSheet.flatten([ + toggleVariants({ + variant: context.variant || variant, + size: context.size || size, + colors, + }), + { + backgroundColor: isSelected ? colors.accent : colors.card, + borderColor: colors.borderMedium, + borderWidth: StyleSheet.hairlineWidth, + }, + props.disabled && { opacity: 0.5 }, + style, + ]); + + return ( + + + {children} + + + ); +}); + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; + +function ToggleGroupIcon({ + icon: Icon, + style, + ...props +}: React.ComponentPropsWithoutRef & { icon: LucideIcon }) { + const textClass = React.useContext(TextClassContext); + const flattenTextStyles = StyleSheet.flatten(textClass); + + return ; +} + +export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem }; + +const styles = StyleSheet.create({ + group: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + }, +}); diff --git a/apps/expo-stylesheet/components/ui/toggle.tsx b/apps/expo-stylesheet/components/ui/toggle.tsx new file mode 100644 index 00000000..503b8e7c --- /dev/null +++ b/apps/expo-stylesheet/components/ui/toggle.tsx @@ -0,0 +1,136 @@ +import * as React from 'react'; +import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; +import type { LucideIcon } from 'lucide-react-native'; +import { useTheme } from '@react-navigation/native'; +import { TextClassContext } from '~/components/ui/text'; +import * as TogglePrimitive from '@rn-primitives/toggle'; +import { type ICustomTheme, type ICustomThemeColor } from '~/lib/constants'; + +type Variant = 'default' | 'outline'; +type Size = 'default' | 'sm' | 'lg'; +type ToggleProps = { variant?: Variant; size?: Size }; + +const toggleVariants = ({ + variant = 'default', + size = 'default', + colors, + style, +}: { + variant?: Variant; + size?: Size; + colors: ICustomThemeColor; + style?: StyleProp; +}): ViewStyle => { + let variantStyle: ViewStyle = {}; + let sizeStyle: ViewStyle = {}; + + // === VARIANT STYLES === + if (variant === 'default') { + variantStyle = { backgroundColor: 'transparent' }; + } else if (variant === 'outline') { + variantStyle = { + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.borderMedium, + backgroundColor: 'transparent', + }; + } + + // === SIZE STYLES === + if (size === 'default') { + sizeStyle = { height: 42, paddingHorizontal: 12 }; + } else if (size === 'sm') { + sizeStyle = { height: 36, paddingHorizontal: 10 }; + } else if (size === 'lg') { + sizeStyle = { height: 44, paddingHorizontal: 20 }; + } + + return StyleSheet.flatten([styles.baseToggle, variantStyle, sizeStyle, style]); +}; + +const toggleTextVariants = ({ + variant = 'default', + size = 'default', + colors, + style, +}: { + variant?: Variant; + size?: Size; + colors: ICustomThemeColor; + style?: StyleProp; +}): TextStyle => { + let variantStyle: TextStyle = {}; + let sizeStyle: TextStyle = {}; + + // === VARIANT STYLES === + if (variant === 'default') variantStyle = {}; + else if (variant === 'outline') variantStyle = {}; + + // === SIZE STYLES === + if (size === 'default') sizeStyle = {}; + else if (size === 'sm') sizeStyle = {}; + else if (size === 'lg') sizeStyle = {}; + + return StyleSheet.flatten([ + styles.baseToggleText, + variantStyle, + sizeStyle, + { color: colors.text }, + style, + ]); +}; + +const Toggle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + ToggleProps & { + style?: StyleProp; + textStyle?: StyleProp; + } +>(({ variant, size, style, textStyle, ...props }, ref) => { + const { colors } = useTheme() as ICustomTheme; + const flattenToggleStyle = StyleSheet.flatten([ + toggleVariants({ variant, size, colors, style }), + props.disabled && { opacity: 0.5 }, + props.pressed && { backgroundColor: colors.accent }, + ]); + + return ( +
+ An open-source UI component library. +