diff --git a/.mcp.json b/.mcp.json index 03b7976cc3f..80268502752 100644 --- a/.mcp.json +++ b/.mcp.json @@ -9,6 +9,14 @@ "tokenUrl": "https://api.superset.sh/api/auth/mcp/token", "scopes": ["openid", "profile", "email"] } + }, + "expo-mcp": { + "type": "http", + "url": "https://mcp.expo.dev/mcp" + }, + "maestro": { + "command": "maestro", + "args": ["mcp"] } } } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/FilesView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/FilesView.tsx index 4ce396eb63a..a7f288e1075 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/FilesView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/FilesView.tsx @@ -39,8 +39,8 @@ export function FilesView() { const [searchTerm, setSearchTerm] = useState(""); const projectId = workspace?.project?.id; - const showHiddenFiles = useFileExplorerStore( - (s) => (projectId ? (s.showHiddenFiles[projectId] ?? false) : false), + const showHiddenFiles = useFileExplorerStore((s) => + projectId ? (s.showHiddenFiles[projectId] ?? false) : false, ); const toggleHiddenFiles = useFileExplorerStore((s) => s.toggleHiddenFiles); diff --git a/apps/mobile/components/ui/accordion.tsx b/apps/mobile/components/ui/accordion.tsx new file mode 100644 index 00000000000..d4a8263e65c --- /dev/null +++ b/apps/mobile/components/ui/accordion.tsx @@ -0,0 +1,155 @@ +import * as AccordionPrimitive from "@rn-primitives/accordion"; +import { ChevronDown } from "lucide-react-native"; +import { Platform, Pressable, View } from "react-native"; +import Animated, { + FadeOutUp, + LayoutAnimationConfig, + LinearTransition, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from "react-native-reanimated"; +import { Icon } from "@/components/ui/icon"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +function Accordion({ + children, + ...props +}: Omit & + React.RefAttributes) { + return ( + + + + {children} + + + + ); +} + +function AccordionItem({ + children, + className, + value, + ...props +}: AccordionPrimitive.ItemProps & + React.RefAttributes) { + return ( + + + {children} + + + ); +} + +const Trigger = Platform.OS === "web" ? View : Pressable; + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.TriggerProps & { + children?: React.ReactNode; +} & React.RefAttributes) { + const { isExpanded } = AccordionPrimitive.useItemContext(); + + const progress = useDerivedValue( + () => + isExpanded + ? withTiming(1, { duration: 250 }) + : withTiming(0, { duration: 200 }), + [isExpanded], + ); + const chevronStyle = useAnimatedStyle( + () => ({ + transform: [{ rotate: `${progress.value * 180}deg` }], + }), + [progress], + ); + + return ( + + + + svg]:rotate-180", + }), + className, + )} + > + {children} + + + + + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.ContentProps & + React.RefAttributes) { + const { isExpanded } = AccordionPrimitive.useItemContext(); + return ( + + + + {children} + + + + ); +} + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/apps/mobile/components/ui/alert-dialog.tsx b/apps/mobile/components/ui/alert-dialog.tsx new file mode 100644 index 00000000000..e143a46b991 --- /dev/null +++ b/apps/mobile/components/ui/alert-dialog.tsx @@ -0,0 +1,167 @@ +import * as AlertDialogPrimitive from "@rn-primitives/alert-dialog"; +import * as React from "react"; +import { Platform, View, type ViewProps } from "react-native"; +import { FadeIn, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { buttonTextVariants, buttonVariants } from "@/components/ui/button"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function AlertDialogOverlay({ + className, + children, + ...props +}: Omit & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + {children} + + + + ); +} + +function AlertDialogContent({ + className, + portalHost, + ...props +}: AlertDialogPrimitive.ContentProps & + React.RefAttributes & { + portalHost?: string; + }) { + return ( + + + + + + ); +} + +function AlertDialogHeader({ className, ...props }: ViewProps) { + return ( + + + + ); +} + +function AlertDialogFooter({ className, ...props }: ViewProps) { + return ( + + ); +} + +function AlertDialogTitle({ + className, + ...props +}: AlertDialogPrimitive.TitleProps & + React.RefAttributes) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: AlertDialogPrimitive.DescriptionProps & + React.RefAttributes) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: AlertDialogPrimitive.ActionProps & + React.RefAttributes) { + return ( + + + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: AlertDialogPrimitive.CancelProps & + React.RefAttributes) { + return ( + + + + ); +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +}; diff --git a/apps/mobile/components/ui/alert.tsx b/apps/mobile/components/ui/alert.tsx new file mode 100644 index 00000000000..854b06a7815 --- /dev/null +++ b/apps/mobile/components/ui/alert.tsx @@ -0,0 +1,85 @@ +import type { LucideIcon } from "lucide-react-native"; +import * as React from "react"; +import { View, type ViewProps } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { Text, TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +function Alert({ + className, + variant, + children, + icon, + iconClassName, + ...props +}: ViewProps & + React.RefAttributes & { + icon: LucideIcon; + variant?: "default" | "destructive"; + iconClassName?: string; + }) { + return ( + + + + + + {children} + + + ); +} + +function AlertTitle({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + return ( + + ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + const textClass = React.useContext(TextClassContext); + return ( + + ); +} + +export { Alert, AlertDescription, AlertTitle }; diff --git a/apps/mobile/components/ui/aspect-ratio.tsx b/apps/mobile/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000000..0edd624e023 --- /dev/null +++ b/apps/mobile/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/mobile/components/ui/avatar.tsx b/apps/mobile/components/ui/avatar.tsx new file mode 100644 index 00000000000..1725edea7de --- /dev/null +++ b/apps/mobile/components/ui/avatar.tsx @@ -0,0 +1,47 @@ +import * as AvatarPrimitive from "@rn-primitives/avatar"; +import { cn } from "@/lib/utils"; + +function Avatar({ + className, + ...props +}: AvatarPrimitive.RootProps & React.RefAttributes) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: AvatarPrimitive.ImageProps & React.RefAttributes) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.FallbackProps & + React.RefAttributes) { + return ( + + ); +} + +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/apps/mobile/components/ui/badge.tsx b/apps/mobile/components/ui/badge.tsx new file mode 100644 index 00000000000..5583e5683bc --- /dev/null +++ b/apps/mobile/components/ui/badge.tsx @@ -0,0 +1,72 @@ +import * as Slot from "@rn-primitives/slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Platform, View, type ViewProps } from "react-native"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + cn( + "border-border group shrink-0 flex-row items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5", + Platform.select({ + web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive w-fit whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3", + }), + ), + { + variants: { + variant: { + default: cn( + "bg-primary border-transparent", + Platform.select({ web: "[a&]:hover:bg-primary/90" }), + ), + secondary: cn( + "bg-secondary border-transparent", + Platform.select({ web: "[a&]:hover:bg-secondary/90" }), + ), + destructive: cn( + "bg-destructive border-transparent", + Platform.select({ web: "[a&]:hover:bg-destructive/90" }), + ), + outline: Platform.select({ + web: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }), + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const badgeTextVariants = cva("text-xs font-medium", { + variants: { + variant: { + default: "text-primary-foreground", + secondary: "text-secondary-foreground", + destructive: "text-white", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +type BadgeProps = ViewProps & + React.RefAttributes & { + asChild?: boolean; + } & VariantProps; + +function Badge({ className, variant, asChild, ...props }: BadgeProps) { + const Component = asChild ? Slot.View : View; + return ( + + + + ); +} + +export { Badge, badgeTextVariants, badgeVariants }; +export type { BadgeProps }; diff --git a/apps/mobile/components/ui/button.tsx b/apps/mobile/components/ui/button.tsx index dff5292b0a9..3bb70e8c08c 100644 --- a/apps/mobile/components/ui/button.tsx +++ b/apps/mobile/components/ui/button.tsx @@ -3,6 +3,8 @@ import { Platform, Pressable } from "react-native"; import { TextClassContext } from "@/components/ui/text"; import { cn } from "@/lib/utils"; +// NOTE: group-* is not supported yet by Uniwind + const buttonVariants = cva( cn( "group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none", diff --git a/apps/mobile/components/ui/checkbox.tsx b/apps/mobile/components/ui/checkbox.tsx new file mode 100644 index 00000000000..e7a1e177621 --- /dev/null +++ b/apps/mobile/components/ui/checkbox.tsx @@ -0,0 +1,53 @@ +import * as CheckboxPrimitive from "@rn-primitives/checkbox"; +import { Check } from "lucide-react-native"; +import { Platform } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { cn } from "@/lib/utils"; + +const DEFAULT_HIT_SLOP = 24; + +function Checkbox({ + className, + checkedClassName, + indicatorClassName, + iconClassName, + ...props +}: CheckboxPrimitive.RootProps & + React.RefAttributes & { + checkedClassName?: string; + indicatorClassName?: string; + iconClassName?: string; + }) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/apps/mobile/components/ui/collapsible.tsx b/apps/mobile/components/ui/collapsible.tsx new file mode 100644 index 00000000000..613bf3eeb23 --- /dev/null +++ b/apps/mobile/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/mobile/components/ui/context-menu.tsx b/apps/mobile/components/ui/context-menu.tsx new file mode 100644 index 00000000000..d9a1ffb892a --- /dev/null +++ b/apps/mobile/components/ui/context-menu.tsx @@ -0,0 +1,324 @@ +import * as ContextMenuPrimitive from "@rn-primitives/context-menu"; +import { + Check, + ChevronDown, + ChevronRight, + ChevronUp, +} from "lucide-react-native"; +import * as React from "react"; +import { + Platform, + type StyleProp, + StyleSheet, + Text, + type TextProps, + View, + type ViewStyle, +} from "react-native"; +import { FadeIn } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { Icon } from "@/components/ui/icon"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const ContextMenu = ContextMenuPrimitive.Root; +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; +const ContextMenuGroup = ContextMenuPrimitive.Group; +const ContextMenuSub = ContextMenuPrimitive.Sub; +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +function ContextMenuSubTrigger({ + className, + inset, + children, + iconClassName, + ...props +}: ContextMenuPrimitive.SubTriggerProps & + React.RefAttributes & { + children?: React.ReactNode; + iconClassName?: string; + inset?: boolean; + }) { + const { open } = ContextMenuPrimitive.useSubContext(); + const icon = + Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown; + return ( + + + {children} + + + + ); +} + +function ContextMenuSubContent({ + className, + ...props +}: ContextMenuPrimitive.SubContentProps & + React.RefAttributes) { + return ( + + + + ); +} + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function ContextMenuContent({ + className, + overlayClassName, + overlayStyle, + portalHost, + ...props +}: ContextMenuPrimitive.ContentProps & + React.RefAttributes & { + overlayStyle?: StyleProp; + overlayClassName?: string; + portalHost?: string; + }) { + return ( + + + + + + + + + + + + ); +} + +function ContextMenuItem({ + className, + inset, + variant, + ...props +}: ContextMenuPrimitive.ItemProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + variant?: "default" | "destructive"; + }) { + return ( + + + + ); +} + +function ContextMenuCheckboxItem({ + className, + children, + ...props +}: ContextMenuPrimitive.CheckboxItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: ContextMenuPrimitive.RadioItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: ContextMenuPrimitive.LabelProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + }) { + return ( + + ); +} + +function ContextMenuSeparator({ + className, + ...props +}: ContextMenuPrimitive.SeparatorProps & + React.RefAttributes) { + return ( + + ); +} + +function ContextMenuShortcut({ + className, + ...props +}: TextProps & React.RefAttributes) { + return ( + + ); +} + +export { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuGroup, + ContextMenuItem, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +}; diff --git a/apps/mobile/components/ui/dialog.tsx b/apps/mobile/components/ui/dialog.tsx new file mode 100644 index 00000000000..42da9c3ec56 --- /dev/null +++ b/apps/mobile/components/ui/dialog.tsx @@ -0,0 +1,164 @@ +import * as DialogPrimitive from "@rn-primitives/dialog"; +import { X } from "lucide-react-native"; +import * as React from "react"; +import { Platform, Text, View, type ViewProps } from "react-native"; +import { FadeIn, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { Icon } from "@/components/ui/icon"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function DialogOverlay({ + className, + children, + ...props +}: Omit & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + *]:cursor-auto", + }), + className, + )} + {...props} + asChild={Platform.OS !== "web"} + > + + + {children} + + + + + ); +} +function DialogContent({ + className, + portalHost, + children, + ...props +}: DialogPrimitive.ContentProps & + React.RefAttributes & { + portalHost?: string; + }) { + return ( + + + + {children} + + + Close + + + + + ); +} + +function DialogHeader({ className, ...props }: ViewProps) { + return ( + + ); +} + +function DialogFooter({ className, ...props }: ViewProps) { + return ( + + ); +} + +function DialogTitle({ + className, + ...props +}: DialogPrimitive.TitleProps & React.RefAttributes) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.DescriptionProps & + React.RefAttributes) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/apps/mobile/components/ui/dropdown-menu.tsx b/apps/mobile/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000000..e0c0de0ac60 --- /dev/null +++ b/apps/mobile/components/ui/dropdown-menu.tsx @@ -0,0 +1,331 @@ +import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu"; +import { + Check, + ChevronDown, + ChevronRight, + ChevronUp, +} from "lucide-react-native"; +import * as React from "react"; +import { + Platform, + type StyleProp, + StyleSheet, + Text, + type TextProps, + View, + type ViewStyle, +} from "react-native"; +import { FadeIn } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { Icon } from "@/components/ui/icon"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +function DropdownMenuSubTrigger({ + className, + inset, + children, + iconClassName, + ...props +}: DropdownMenuPrimitive.SubTriggerProps & + React.RefAttributes & { + children?: React.ReactNode; + iconClassName?: string; + inset?: boolean; + }) { + const { open } = DropdownMenuPrimitive.useSubContext(); + const icon = + Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown; + return ( + + + {children} + + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: DropdownMenuPrimitive.SubContentProps & + React.RefAttributes) { + return ( + + + + ); +} + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function DropdownMenuContent({ + className, + overlayClassName, + overlayStyle, + portalHost, + ...props +}: DropdownMenuPrimitive.ContentProps & + React.RefAttributes & { + overlayStyle?: StyleProp; + overlayClassName?: string; + portalHost?: string; + }) { + return ( + + + + + + + + + + + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant, + ...props +}: DropdownMenuPrimitive.ItemProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + variant?: "default" | "destructive"; + }) { + return ( + + + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + ...props +}: DropdownMenuPrimitive.CheckboxItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: DropdownMenuPrimitive.RadioItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: DropdownMenuPrimitive.LabelProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + }) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: DropdownMenuPrimitive.SeparatorProps & + React.RefAttributes) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: TextProps & React.RefAttributes) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/apps/mobile/components/ui/hover-card.tsx b/apps/mobile/components/ui/hover-card.tsx new file mode 100644 index 00000000000..4d88a7c75a7 --- /dev/null +++ b/apps/mobile/components/ui/hover-card.tsx @@ -0,0 +1,56 @@ +import * as HoverCardPrimitive from "@rn-primitives/hover-card"; +import * as React from "react"; +import { Platform, StyleSheet } from "react-native"; +import { FadeIn, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const HoverCard = HoverCardPrimitive.Root; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function HoverCardContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: HoverCardPrimitive.ContentProps & + React.RefAttributes) { + return ( + + + + + + *]:cursor-auto", + props.side === "bottom" && "slide-in-from-top-2", + props.side === "top" && "slide-in-from-bottom-2", + ), + }), + className, + )} + {...props} + /> + + + + + + ); +} + +export { HoverCard, HoverCardContent, HoverCardTrigger }; diff --git a/apps/mobile/components/ui/icon.tsx b/apps/mobile/components/ui/icon.tsx new file mode 100644 index 00000000000..94d2523a80c --- /dev/null +++ b/apps/mobile/components/ui/icon.tsx @@ -0,0 +1,54 @@ +import type { LucideIcon, LucideProps } from "lucide-react-native"; +import { withUniwind } from "uniwind"; +import { cn } from "@/lib/utils"; + +type IconProps = LucideProps & { + as: LucideIcon; +}; + +function IconImpl({ as: IconComponent, ...props }: IconProps) { + return ; +} + +const StyledIcon = withUniwind(IconImpl, { + size: { + fromClassName: "className", + styleProperty: "width", + }, + color: { + fromClassName: "className", + styleProperty: "color", + }, +}); + +/** + * A wrapper component for Lucide icons with Uniwind `className` support via `withUniwind`. + * + * This component allows you to render any Lucide icon while applying utility classes + * using `uniwind`. It avoids the need to wrap or configure each icon individually. + * + * @component + * @example + * ```tsx + * import { ArrowRight } from 'lucide-react-native'; + * import { Icon } from '@/registry/uniwind/registry/components/ui/icon'; + * + * + * ``` + * + * @param {LucideIcon} as - The Lucide icon component to render. + * @param {string} className - Utility classes to style the icon using Uniwind. + * @param {number} size - Icon size (overrides the size class). + * @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon. + */ +function Icon({ as: IconComponent, className, ...props }: IconProps) { + return ( + + ); +} + +export { Icon }; diff --git a/apps/mobile/components/ui/label.tsx b/apps/mobile/components/ui/label.tsx new file mode 100644 index 00000000000..bca5ff9e1a8 --- /dev/null +++ b/apps/mobile/components/ui/label.tsx @@ -0,0 +1,41 @@ +import * as LabelPrimitive from "@rn-primitives/label"; +import { Platform } from "react-native"; +import { cn } from "@/lib/utils"; + +function Label({ + className, + onPress, + onLongPress, + onPressIn, + onPressOut, + disabled, + ...props +}: LabelPrimitive.TextProps & React.RefAttributes) { + return ( + + + + ); +} + +export { Label }; diff --git a/apps/mobile/components/ui/menubar.tsx b/apps/mobile/components/ui/menubar.tsx new file mode 100644 index 00000000000..3c208d23c14 --- /dev/null +++ b/apps/mobile/components/ui/menubar.tsx @@ -0,0 +1,396 @@ +import * as MenubarPrimitive from "@rn-primitives/menubar"; +import { Portal } from "@rn-primitives/portal"; +import { + Check, + ChevronDown, + ChevronRight, + ChevronUp, +} from "lucide-react-native"; +import * as React from "react"; +import { + Platform, + Pressable, + type StyleProp, + StyleSheet, + Text, + type TextProps, + View, + type ViewStyle, +} from "react-native"; +import { FadeIn } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { Icon } from "@/components/ui/icon"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const MenubarMenu = MenubarPrimitive.Menu; + +const MenubarGroup = MenubarPrimitive.Group; + +const MenubarPortal = MenubarPrimitive.Portal; + +const MenubarSub = MenubarPrimitive.Sub; + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function Menubar({ + className, + value: valueProp, + onValueChange: onValueChangeProp, + ...props +}: MenubarPrimitive.RootProps & React.RefAttributes) { + const id = React.useId(); + const [value, setValue] = React.useState(undefined); + + function closeMenu() { + if (onValueChangeProp) { + onValueChangeProp(undefined); + return; + } + setValue(undefined); + } + + return ( + <> + {Platform.OS !== "web" && (value || valueProp) ? ( + + + + ) : null} + + + ); +} + +function MenubarTrigger({ + className, + ...props +}: MenubarPrimitive.TriggerProps & + React.RefAttributes) { + const { value } = MenubarPrimitive.useRootContext(); + const { value: itemValue } = MenubarPrimitive.useMenuContext(); + + return ( + + + + ); +} + +function MenubarSubTrigger({ + className, + inset, + children, + iconClassName, + ...props +}: MenubarPrimitive.SubTriggerProps & + React.RefAttributes & { + children?: React.ReactNode; + iconClassName?: string; + inset?: boolean; + }) { + const { open } = MenubarPrimitive.useSubContext(); + const icon = + Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown; + return ( + + + {children} + + + + ); +} + +function MenubarSubContent({ + className, + ...props +}: MenubarPrimitive.SubContentProps & + React.RefAttributes) { + return ( + + + + ); +} + +function MenubarContent({ + className, + overlayClassName, + overlayStyle, + portalHost, + align = "start", + alignOffset = -4, + sideOffset = 8, + ...props +}: MenubarPrimitive.ContentProps & + React.RefAttributes & { + overlayStyle?: StyleProp; + overlayClassName?: string; + portalHost?: string; + }) { + return ( + + + + + + + + + + ); +} + +function MenubarItem({ + className, + inset, + variant, + ...props +}: MenubarPrimitive.ItemProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + variant?: "default" | "destructive"; + }) { + return ( + + + + ); +} + +function MenubarCheckboxItem({ + className, + children, + ...props +}: MenubarPrimitive.CheckboxItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function MenubarRadioItem({ + className, + children, + ...props +}: MenubarPrimitive.RadioItemProps & + React.RefAttributes & { + children?: React.ReactNode; + }) { + return ( + + + + + + + + {children} + + + ); +} + +function MenubarLabel({ + className, + inset, + ...props +}: MenubarPrimitive.LabelProps & + React.RefAttributes & { + className?: string; + inset?: boolean; + }) { + return ( + + ); +} + +function MenubarSeparator({ + className, + ...props +}: MenubarPrimitive.SeparatorProps & + React.RefAttributes) { + return ( + + ); +} + +function MenubarShortcut({ + className, + ...props +}: TextProps & React.RefAttributes) { + return ( + + ); +} + +export { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarGroup, + MenubarItem, + MenubarLabel, + MenubarMenu, + MenubarPortal, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +}; diff --git a/apps/mobile/components/ui/native-only-animated-view.tsx b/apps/mobile/components/ui/native-only-animated-view.tsx new file mode 100644 index 00000000000..78e707fb895 --- /dev/null +++ b/apps/mobile/components/ui/native-only-animated-view.tsx @@ -0,0 +1,24 @@ +import { Platform } from "react-native"; +import Animated from "react-native-reanimated"; + +/** + * This component is used to wrap animated views that should only be animated on native. + * @param props - The props for the animated view. + * @returns The animated view if the platform is native, otherwise the children. + * @example + * + * I am only animated on native + * + */ +function NativeOnlyAnimatedView( + props: React.ComponentProps & + React.RefAttributes, +) { + if (Platform.OS === "web") { + return <>{props.children as React.ReactNode}; + } else { + return ; + } +} + +export { NativeOnlyAnimatedView }; diff --git a/apps/mobile/components/ui/popover.tsx b/apps/mobile/components/ui/popover.tsx new file mode 100644 index 00000000000..18129009ded --- /dev/null +++ b/apps/mobile/components/ui/popover.tsx @@ -0,0 +1,62 @@ +import * as PopoverPrimitive from "@rn-primitives/popover"; +import * as React from "react"; +import { Platform, StyleSheet } from "react-native"; +import { FadeIn, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + portalHost, + ...props +}: PopoverPrimitive.ContentProps & + React.RefAttributes & { + portalHost?: string; + }) { + return ( + + + + + + + + + + + + ); +} + +export { Popover, PopoverContent, PopoverTrigger }; diff --git a/apps/mobile/components/ui/progress.tsx b/apps/mobile/components/ui/progress.tsx new file mode 100644 index 00000000000..5e5a88592be --- /dev/null +++ b/apps/mobile/components/ui/progress.tsx @@ -0,0 +1,93 @@ +import * as ProgressPrimitive from "@rn-primitives/progress"; +import { Platform, View } from "react-native"; +import Animated, { + Extrapolation, + interpolate, + useAnimatedStyle, + useDerivedValue, + withSpring, +} from "react-native-reanimated"; +import { cn } from "@/lib/utils"; + +function Progress({ + className, + value, + indicatorClassName, + ...props +}: ProgressPrimitive.RootProps & + React.RefAttributes & { + indicatorClassName?: string; + }) { + return ( + + + + ); +} + +export { Progress }; + +const Indicator = Platform.select({ + web: WebIndicator, + native: NativeIndicator, + default: NullIndicator, +}); + +type IndicatorProps = { + value: number | undefined | null; + className?: string; +}; + +function WebIndicator({ value, className }: IndicatorProps) { + if (Platform.OS !== "web") { + return null; + } + + return ( + + + + ); +} + +function NativeIndicator({ value, className }: IndicatorProps) { + const progress = useDerivedValue(() => value ?? 0); + + const indicator = useAnimatedStyle(() => { + return { + width: withSpring( + `${interpolate(progress.value, [0, 100], [1, 100], Extrapolation.CLAMP)}%`, + { overshootClamping: true }, + ), + }; + }, [value]); + + if (Platform.OS === "web") { + return null; + } + + return ( + + + + ); +} + +function NullIndicator(_props: IndicatorProps) { + return null; +} diff --git a/apps/mobile/components/ui/radio-group.tsx b/apps/mobile/components/ui/radio-group.tsx new file mode 100644 index 00000000000..4c8a0f5a72d --- /dev/null +++ b/apps/mobile/components/ui/radio-group.tsx @@ -0,0 +1,37 @@ +import * as RadioGroupPrimitive from "@rn-primitives/radio-group"; +import { Platform } from "react-native"; +import { cn } from "@/lib/utils"; + +function RadioGroup({ + className, + ...props +}: RadioGroupPrimitive.RootProps & + React.RefAttributes) { + return ( + + ); +} + +function RadioGroupItem({ + className, + ...props +}: RadioGroupPrimitive.ItemProps & + React.RefAttributes) { + return ( + + + + ); +} + +export { RadioGroup, RadioGroupItem }; diff --git a/apps/mobile/components/ui/select.tsx b/apps/mobile/components/ui/select.tsx new file mode 100644 index 00000000000..1ba86c88386 --- /dev/null +++ b/apps/mobile/components/ui/select.tsx @@ -0,0 +1,288 @@ +import * as SelectPrimitive from "@rn-primitives/select"; +import { + Check, + ChevronDown, + ChevronDownIcon, + ChevronUpIcon, +} from "lucide-react-native"; +import * as React from "react"; +import { Platform, ScrollView, StyleSheet, View } from "react-native"; +import { FadeIn, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { Icon } from "@/components/ui/icon"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +type Option = SelectPrimitive.Option; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +function SelectValue({ + ref, + className, + ...props +}: SelectPrimitive.ValueProps & + React.RefAttributes & { + className?: string; + }) { + const { value } = SelectPrimitive.useRootContext(); + return ( + + ); +} + +function SelectTrigger({ + ref, + className, + children, + size = "default", + ...props +}: SelectPrimitive.TriggerProps & + React.RefAttributes & { + children?: React.ReactNode; + size?: "default" | "sm"; + }) { + return ( + + {children} + + + ); +} + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function SelectContent({ + className, + children, + position = "popper", + portalHost, + ...props +}: SelectPrimitive.ContentProps & + React.RefAttributes & { + className?: string; + portalHost?: string; + }) { + return ( + + + + + + + + + {children} + + + + + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.LabelProps & React.RefAttributes) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.ItemProps & React.RefAttributes) { + return ( + + + + + + + + + ); +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.SeparatorProps & + React.RefAttributes) { + return ( + + ); +} + +/** + * @platform Web only + * Returns null on native platforms + */ +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS !== "web") { + return null; + } + return ( + + + + ); +} + +/** + * @platform Web only + * Returns null on native platforms + */ +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS !== "web") { + return null; + } + return ( + + + + ); +} + +/** + * @platform Native only + * Returns the children on the web + */ +function NativeSelectScrollView({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS === "web") { + return <>{props.children}; + } + return ; +} + +export { + NativeSelectScrollView, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, + type Option, +}; diff --git a/apps/mobile/components/ui/separator.tsx b/apps/mobile/components/ui/separator.tsx new file mode 100644 index 00000000000..c91fef4f192 --- /dev/null +++ b/apps/mobile/components/ui/separator.tsx @@ -0,0 +1,25 @@ +import * as SeparatorPrimitive from "@rn-primitives/separator"; +import { cn } from "@/lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: SeparatorPrimitive.RootProps & + React.RefAttributes) { + return ( + + ); +} + +export { Separator }; diff --git a/apps/mobile/components/ui/skeleton.tsx b/apps/mobile/components/ui/skeleton.tsx new file mode 100644 index 00000000000..e5d083c2d28 --- /dev/null +++ b/apps/mobile/components/ui/skeleton.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import type { View } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from "react-native-reanimated"; +import { cn } from "@/lib/utils"; + +const duration = 1000; + +function Skeleton({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + const sv = useSharedValue(1); + + React.useEffect(() => { + sv.value = withRepeat(withTiming(0.5, { duration }), -1, true); + }, [sv]); + + const style = useAnimatedStyle( + () => ({ + opacity: sv.value, + }), + [sv], + ); + return ( + + ); +} + +export { Skeleton }; diff --git a/apps/mobile/components/ui/tabs.tsx b/apps/mobile/components/ui/tabs.tsx new file mode 100644 index 00000000000..ab03b290be3 --- /dev/null +++ b/apps/mobile/components/ui/tabs.tsx @@ -0,0 +1,75 @@ +import * as TabsPrimitive from "@rn-primitives/tabs"; +import { Platform } from "react-native"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +function Tabs({ + className, + ...props +}: TabsPrimitive.RootProps & React.RefAttributes) { + return ( + + ); +} + +function TabsList({ + className, + ...props +}: TabsPrimitive.ListProps & React.RefAttributes) { + return ( + + ); +} + +function TabsTrigger({ + className, + ...props +}: TabsPrimitive.TriggerProps & React.RefAttributes) { + const { value } = TabsPrimitive.useRootContext(); + return ( + + + + ); +} + +function TabsContent({ + className, + ...props +}: TabsPrimitive.ContentProps & React.RefAttributes) { + return ( + + ); +} + +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/apps/mobile/components/ui/textarea.tsx b/apps/mobile/components/ui/textarea.tsx new file mode 100644 index 00000000000..16ba411eee7 --- /dev/null +++ b/apps/mobile/components/ui/textarea.tsx @@ -0,0 +1,33 @@ +import { Platform, TextInput, type TextInputProps } from "react-native"; +import { cn } from "@/lib/utils"; + +function Textarea({ + className, + multiline = true, + numberOfLines = Platform.select({ web: 2, native: 8 }), // On web, numberOfLines also determines initial height. On native, it determines the maximum height. + placeholderTextColorClassName, + ...props +}: TextInputProps & React.RefAttributes) { + return ( + + ); +} + +export { Textarea }; diff --git a/apps/mobile/components/ui/toggle-group.tsx b/apps/mobile/components/ui/toggle-group.tsx new file mode 100644 index 00000000000..f0c8b1e4e1e --- /dev/null +++ b/apps/mobile/components/ui/toggle-group.tsx @@ -0,0 +1,116 @@ +import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group"; +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { Platform } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { TextClassContext } from "@/components/ui/text"; +import { toggleVariants } from "@/components/ui/toggle"; +import { cn } from "@/lib/utils"; + +const ToggleGroupContext = React.createContext | null>(null); + +function ToggleGroup({ + className, + variant, + size, + children, + ...props +}: ToggleGroupPrimitive.RootProps & + VariantProps & + React.RefAttributes) { + return ( + + + {children} + + + ); +} + +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; +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + isFirst, + isLast, + ...props +}: ToggleGroupPrimitive.ItemProps & + VariantProps & + React.RefAttributes & { + isFirst?: boolean; + isLast?: boolean; + }) { + const context = useToggleGroupContext(); + const { value } = ToggleGroupPrimitive.useRootContext(); + + return ( + + + {children} + + + ); +} + +function ToggleGroupIcon({ + className, + ...props +}: React.ComponentProps) { + const textClass = React.useContext(TextClassContext); + return ( + + ); +} + +export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem }; diff --git a/apps/mobile/components/ui/toggle.tsx b/apps/mobile/components/ui/toggle.tsx new file mode 100644 index 00000000000..baebaa8b30a --- /dev/null +++ b/apps/mobile/components/ui/toggle.tsx @@ -0,0 +1,81 @@ +import * as TogglePrimitive from "@rn-primitives/toggle"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { Platform } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const toggleVariants = cva( + cn( + "active:bg-muted group flex flex-row items-center justify-center gap-2 rounded-md", + Platform.select({ + web: "hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex cursor-default whitespace-nowrap outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:pointer-events-none [&_svg]:pointer-events-none", + }), + ), + { + variants: { + variant: { + default: "bg-transparent", + outline: cn( + "border-input active:bg-accent border bg-transparent shadow-sm shadow-black/5", + Platform.select({ + web: "hover:bg-accent hover:text-accent-foreground", + }), + ), + }, + size: { + default: "h-10 min-w-10 px-2.5 sm:h-9 sm:min-w-9 sm:px-2", + sm: "h-9 min-w-9 px-2 sm:h-8 sm:min-w-8 sm:px-1.5", + lg: "h-11 min-w-11 px-3 sm:h-10 sm:min-w-10 sm:px-2.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Toggle({ + className, + variant, + size, + ...props +}: TogglePrimitive.RootProps & + VariantProps & + React.RefAttributes) { + return ( + + + + ); +} + +function ToggleIcon({ + className, + ...props +}: React.ComponentProps) { + const textClass = React.useContext(TextClassContext); + return ( + + ); +} + +export { Toggle, ToggleIcon, toggleVariants }; diff --git a/apps/mobile/components/ui/tooltip.tsx b/apps/mobile/components/ui/tooltip.tsx new file mode 100644 index 00000000000..7bd02c3f1a3 --- /dev/null +++ b/apps/mobile/components/ui/tooltip.tsx @@ -0,0 +1,72 @@ +import * as TooltipPrimitive from "@rn-primitives/tooltip"; +import * as React from "react"; +import { Platform, StyleSheet } from "react-native"; +import { FadeInDown, FadeInUp, FadeOut } from "react-native-reanimated"; +import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"; +import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const FullWindowOverlay = + Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment; + +function TooltipContent({ + className, + sideOffset = 4, + portalHost, + side = "top", + ...props +}: TooltipPrimitive.ContentProps & + React.RefAttributes & { + portalHost?: string; + }) { + return ( + + + + + + + + + + + + ); +} + +export { Tooltip, TooltipContent, TooltipTrigger }; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 03cb5f14f20..9ce221cdb8a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -16,9 +16,30 @@ "@electric-sql/client": "https://pkg.pr.new/@electric-sql/client@3724", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.1.28", + "@rn-primitives/accordion": "^1.2.0", + "@rn-primitives/alert-dialog": "^1.2.0", + "@rn-primitives/aspect-ratio": "^1.2.0", + "@rn-primitives/avatar": "^1.2.0", + "@rn-primitives/checkbox": "^1.2.0", + "@rn-primitives/collapsible": "^1.2.0", + "@rn-primitives/context-menu": "^1.2.0", + "@rn-primitives/dialog": "^1.2.0", + "@rn-primitives/dropdown-menu": "^1.2.0", + "@rn-primitives/hover-card": "^1.2.0", + "@rn-primitives/label": "^1.2.0", + "@rn-primitives/menubar": "^1.2.0", + "@rn-primitives/popover": "^1.2.0", "@rn-primitives/portal": "^1.3.0", + "@rn-primitives/progress": "^1.2.0", + "@rn-primitives/radio-group": "^1.2.0", + "@rn-primitives/select": "^1.2.0", + "@rn-primitives/separator": "^1.2.0", "@rn-primitives/slot": "^1.2.0", "@rn-primitives/switch": "^1.2.0", + "@rn-primitives/tabs": "^1.2.0", + "@rn-primitives/toggle": "^1.2.0", + "@rn-primitives/toggle-group": "^1.2.0", + "@rn-primitives/tooltip": "^1.2.0", "@superset/db": "workspace:*", "@superset/trpc": "workspace:*", "@tanstack/db": "0.5.22", diff --git a/apps/mobile/screens/(authenticated)/demo/DemoScreen.tsx b/apps/mobile/screens/(authenticated)/demo/DemoScreen.tsx index e245a66bfc1..635d655031e 100644 --- a/apps/mobile/screens/(authenticated)/demo/DemoScreen.tsx +++ b/apps/mobile/screens/(authenticated)/demo/DemoScreen.tsx @@ -1,7 +1,41 @@ import { eq, isNull } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; -import { ScrollView, Text, View } from "react-native"; +import { + AlertCircle, + Bold, + ChevronRight, + Info, + Italic, + Mail, + Star, + Underline, + User, +} from "lucide-react-native"; +import * as React from "react"; +import { Pressable, ScrollView, View } from "react-native"; import { OrganizationSwitcher } from "@/components/OrganizationSwitcher"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { AspectRatio } from "@/components/ui/aspect-ratio"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -9,11 +43,61 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Icon } from "@/components/ui/icon"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Progress } from "@/components/ui/progress"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Text } from "@/components/ui/text"; +import { Textarea } from "@/components/ui/textarea"; +import { Toggle, ToggleIcon } from "@/components/ui/toggle"; +import { + ToggleGroup, + ToggleGroupIcon, + ToggleGroupItem, +} from "@/components/ui/toggle-group"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { useCollections } from "@/providers/CollectionsProvider"; export function DemoScreen() { const collections = useCollections(); + // Live queries const { data: organizations } = useLiveQuery( (q) => q.from({ organizations: collections.organizations }), [collections], @@ -73,18 +157,620 @@ export function DemoScreen() { [collections], ); + // Component state + const [checkboxChecked, setCheckboxChecked] = React.useState(false); + const [switchChecked, setSwitchChecked] = React.useState(false); + const [radioValue, setRadioValue] = React.useState("option-1"); + const [selectValue, setSelectValue] = React.useState< + { value: string; label: string } | undefined + >(undefined); + const [progressValue, setProgressValue] = React.useState(33); + const [togglePressed, setTogglePressed] = React.useState(false); + const [toggleGroupValue, setToggleGroupValue] = React.useState([]); + const [collapsibleOpen, setCollapsibleOpen] = React.useState(false); + const [tabValue, setTabValue] = React.useState("tab1"); + const [inputValue, setInputValue] = React.useState(""); + const [textareaValue, setTextareaValue] = React.useState(""); + return ( + {/* Header */} - Electric Collections Demo + Component Demo - Real-time synced data from Electric SQL + All UI components + real-time synced data + + + {/* ── Buttons ── */} + + + Button + All button variants and sizes + + + + + + + + + + + + + + + + + + + + {/* ── Badge ── */} + + + Badge + Status indicators + + + + + Default + + + Secondary + + + Destructive + + + Outline + + + + + + {/* ── Alert ── */} + + + Alert + Informational messages + + + + Heads up! + + This is a default alert component. + + + + Error + + Something went wrong. Please try again. + + + + + + {/* ── Avatar ── */} + + + Avatar + User profile images with fallback + + + + + + SP + + + + + KH + + + + + + + + + + + + {/* ── Input & Textarea ── */} + + + Input & Textarea + Text input fields + + + + + + + + +