From 1709233f7b4bd1ede421cd12d23d24f710ee5673 Mon Sep 17 00:00:00 2001 From: gibbyb Date: Tue, 28 Oct 2025 12:52:45 -0500 Subject: [PATCH] Adding common dependencies before I try to migrate TechTracker to it --- apps/expo/package.json | 11 + packages/ui/package.json | 39 +- packages/ui/src/avatar.tsx | 53 ++ packages/ui/src/based-avatar.tsx | 69 +++ packages/ui/src/based-progress.tsx | 53 ++ packages/ui/src/button.tsx | 46 +- packages/ui/src/card.tsx | 92 +++ packages/ui/src/checkbox.tsx | 32 + packages/ui/src/drawer.tsx | 135 +++++ packages/ui/src/dropdown-menu.tsx | 119 ++-- packages/ui/src/form.tsx | 168 ++++++ packages/ui/src/index.ts | 12 + packages/ui/src/input-otp.tsx | 77 +++ packages/ui/src/input.tsx | 20 +- packages/ui/src/label.tsx | 15 +- packages/ui/src/pagination.tsx | 127 ++++ packages/ui/src/progress.tsx | 31 + packages/ui/src/scroll-area.tsx | 58 ++ packages/ui/src/separator.tsx | 17 +- .../ui/src/shadcn-io/image-crop/index.tsx | 368 ++++++++++++ packages/ui/src/sonner.tsx | 25 + packages/ui/src/status-message.tsx | 58 ++ packages/ui/src/submit-button.tsx | 51 ++ packages/ui/src/switch.tsx | 31 + packages/ui/src/table.tsx | 116 ++++ packages/ui/src/tabs.tsx | 66 +++ pnpm-lock.yaml | 550 +++++++++++++++++- 27 files changed, 2324 insertions(+), 115 deletions(-) create mode 100644 packages/ui/src/avatar.tsx create mode 100644 packages/ui/src/based-avatar.tsx create mode 100644 packages/ui/src/based-progress.tsx create mode 100644 packages/ui/src/card.tsx create mode 100644 packages/ui/src/checkbox.tsx create mode 100644 packages/ui/src/drawer.tsx create mode 100644 packages/ui/src/form.tsx create mode 100644 packages/ui/src/input-otp.tsx create mode 100644 packages/ui/src/pagination.tsx create mode 100644 packages/ui/src/progress.tsx create mode 100644 packages/ui/src/scroll-area.tsx create mode 100644 packages/ui/src/shadcn-io/image-crop/index.tsx create mode 100644 packages/ui/src/sonner.tsx create mode 100644 packages/ui/src/status-message.tsx create mode 100644 packages/ui/src/submit-button.tsx create mode 100644 packages/ui/src/switch.tsx create mode 100644 packages/ui/src/table.tsx create mode 100644 packages/ui/src/tabs.tsx diff --git a/apps/expo/package.json b/apps/expo/package.json index 27a9d01..a10e3cd 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -16,16 +16,26 @@ "dependencies": { "@acme/backend": "workspace:*", "@convex-dev/auth": "catalog:convex", + "@expo/vector-icons": "^15.0.3", "@legendapp/list": "^2.0.14", + "@react-navigation/bottom-tabs": "^7.6.0", + "@react-navigation/elements": "^2.7.1", + "@react-navigation/native": "^7.1.19", + "@sentry/react-native": "^7.4.0", "convex": "catalog:convex", "expo": "~54.0.20", + "expo-apple-authentication": "~8.0.7", "expo-constants": "~18.0.10", "expo-dev-client": "~6.0.16", + "expo-font": "~14.0.9", + "expo-haptics": "~15.0.7", + "expo-image": "~3.0.10", "expo-linking": "~8.0.8", "expo-router": "~6.0.13", "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", + "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.8", "expo-web-browser": "~15.0.8", "nativewind": "5.0.0-preview.2", @@ -37,6 +47,7 @@ "react-native-reanimated": "~4.1.3", "react-native-safe-area-context": "~5.6.1", "react-native-screens": "~4.16.0", + "react-native-web": "~0.21.2", "react-native-worklets": "~0.5.1", "superjson": "2.2.3" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index ae4aba9..845bb51 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,12 +4,30 @@ "type": "module", "exports": { ".": "./src/index.ts", + "./avatar": "./src/avatar.tsx", + "./based-avatar": "./src/based-avatar.tsx", + "./based-progress": "./src/based-progress.ts", "./button": "./src/button.tsx", + "./card": "./src/card.tsx", + "./checkbox": "./src/checkbox.tsx", + "./drawer": "./src/drawer.tsx", "./dropdown-menu": "./src/dropdown-menu.tsx", "./field": "./src/field.tsx", + "./form": "./src/form.tsx", + "./image-crop": "./src/shadcn-io/image-crop/index.tsx", "./input": "./src/input.tsx", + "./input-otp": "./src/input-otp.tsx", "./label": "./src/label.tsx", + "./pagination": "./src/pagination.tsx", + "./progress": "./src/progress.tsx", + "./scroll-area": "./src/scroll-area.tsx", "./separator": "./src/separator.tsx", + "./sonner": "./src/sonner.tsx", + "./status-message": "./src/status-message.ts", + "./submit-button": "./src/submit-button.tsx", + "./switch": "./src/switch.tsx", + "./table": "./src/table.tsx", + "./tabs": "./src/tabs.tsx", "./theme": "./src/theme.tsx", "./toast": "./src/toast.tsx" }, @@ -22,11 +40,30 @@ "ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "input-otp": "^1.4.2", + "lucide-react": "^0.542.0", + "next-themes": "^0.4.6", "radix-ui": "^1.4.3", + "react-hook-form": "^7.65.0", + "react-image-crop": "^11.0.10", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "vaul": "^1.1.2" }, "devDependencies": { "@acme/eslint-config": "workspace:*", diff --git a/packages/ui/src/avatar.tsx b/packages/ui/src/avatar.tsx new file mode 100644 index 0000000..52e6be0 --- /dev/null +++ b/packages/ui/src/avatar.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/ui/src/based-avatar.tsx b/packages/ui/src/based-avatar.tsx new file mode 100644 index 0000000..ec0fcee --- /dev/null +++ b/packages/ui/src/based-avatar.tsx @@ -0,0 +1,69 @@ +'use client'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { User } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { AvatarImage } from '@/components/ui/avatar'; +import { type ComponentProps } from 'react'; + +type BasedAvatarProps = ComponentProps & { + src?: string | null; + fullName?: string | null; + imageProps?: Omit, 'data-slot'>; + fallbackProps?: ComponentProps; + userIconProps?: ComponentProps; +}; + +const BasedAvatar = ({ + src = null, + fullName = null, + imageProps, + fallbackProps, + userIconProps = { + size: 32, + }, + className, + ...props +}: BasedAvatarProps) => { + return ( + + {src ? ( + + ) : ( + + {fullName ? ( + fullName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + ) : ( + + )} + + )} + + ); +}; + +export { BasedAvatar }; diff --git a/packages/ui/src/based-progress.tsx b/packages/ui/src/based-progress.tsx new file mode 100644 index 0000000..c960965 --- /dev/null +++ b/packages/ui/src/based-progress.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as ProgressPrimitive from '@radix-ui/react-progress'; +import { cn } from '@/lib/utils'; + +type BasedProgressProps = React.ComponentProps< + typeof ProgressPrimitive.Root +> & { + /** how many ms between updates */ + intervalMs?: number; + /** fraction of the remaining distance to add each tick */ + alpha?: number; +}; + +const BasedProgress = ({ + intervalMs = 50, + alpha = 0.1, + className, + value = 0, + ...props +}: BasedProgressProps) => { + const [progress, setProgress] = React.useState(value ?? 0); + + React.useEffect(() => { + const id = window.setInterval(() => { + setProgress((prev) => { + const next = prev + (100 - prev) * alpha; + return Math.min(100, Math.round(next * 10) / 10); + }); + }, intervalMs); + return () => window.clearInterval(id); + }, [intervalMs, alpha]); + + return ( + + + + ); +}; + +export { BasedProgress }; diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx index 4f9c928..657477e 100644 --- a/packages/ui/src/button.tsx +++ b/packages/ui/src/button.tsx @@ -1,57 +1,59 @@ -import type { VariantProps } from "class-variance-authority"; -import { cva } from "class-variance-authority"; -import { Slot as SlotPrimitive } from "radix-ui"; +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@acme/ui"; +import { cn } from '@/lib/utils'; -export const buttonVariants = cva( - "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 shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: - "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs", + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: - "bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs", + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', outline: - "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs", + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs", + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, }, ); -export function Button({ +function Button({ className, variant, size, asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean; }) { - const Comp = asChild ? SlotPrimitive.Slot : "button"; + const Comp = asChild ? Slot : 'button'; return ( ); } + +export { Button, buttonVariants }; diff --git a/packages/ui/src/card.tsx b/packages/ui/src/card.tsx new file mode 100644 index 0000000..32a06b1 --- /dev/null +++ b/packages/ui/src/card.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/packages/ui/src/checkbox.tsx b/packages/ui/src/checkbox.tsx new file mode 100644 index 0000000..fde2498 --- /dev/null +++ b/packages/ui/src/checkbox.tsx @@ -0,0 +1,32 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/packages/ui/src/drawer.tsx b/packages/ui/src/drawer.tsx new file mode 100644 index 0000000..0e2eb3c --- /dev/null +++ b/packages/ui/src/drawer.tsx @@ -0,0 +1,135 @@ +'use client'; + +import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; + +import { cn } from '@/lib/utils'; + +function Drawer({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ); +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/packages/ui/src/dropdown-menu.tsx b/packages/ui/src/dropdown-menu.tsx index 11ef322..7a8804e 100644 --- a/packages/ui/src/dropdown-menu.tsx +++ b/packages/ui/src/dropdown-menu.tsx @@ -1,40 +1,37 @@ -"use client"; +'use client'; -import { - CheckIcon, - ChevronRightIcon, - DotFilledIcon, -} from "@radix-ui/react-icons"; -import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; -import { cn } from "@acme/ui"; +import { cn } from '@/lib/utils'; -export function DropdownMenu({ +function DropdownMenu({ ...props }: React.ComponentProps) { - return ; + return ; } -export function DropdownMenuPortal({ +function DropdownMenuPortal({ ...props }: React.ComponentProps) { return ( - + ); } -export function DropdownMenuTrigger({ +function DropdownMenuTrigger({ ...props }: React.ComponentProps) { return ( ); } -export function DropdownMenuContent({ +function DropdownMenuContent({ className, sideOffset = 4, ...props @@ -42,10 +39,10 @@ export function DropdownMenuContent({ return ( ) { return ( - + ); } -export function DropdownMenuItem({ +function DropdownMenuItem({ className, inset, - variant = "default", + variant = 'default', ...props }: React.ComponentProps & { inset?: boolean; - variant?: "default" | "destructive"; + variant?: 'default' | 'destructive'; }) { return ( ) { return ( - + - + {children} @@ -111,34 +108,34 @@ export function DropdownMenuCheckboxItem({ ); } -export function DropdownMenuRadioGroup({ +function DropdownMenuRadioGroup({ ...props }: React.ComponentProps) { return ( ); } -export function DropdownMenuRadioItem({ +function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps) { return ( - + - + {children} @@ -146,7 +143,7 @@ export function DropdownMenuRadioItem({ ); } -export function DropdownMenuLabel({ +function DropdownMenuLabel({ className, inset, ...props @@ -155,10 +152,10 @@ export function DropdownMenuLabel({ }) { return ( ) { return ( ); } -export function DropdownMenuShortcut({ +function DropdownMenuShortcut({ className, ...props -}: React.ComponentProps<"span">) { +}: React.ComponentProps<'span'>) { return ( ) { - return ; + return ; } -export function DropdownMenuSubTrigger({ +function DropdownMenuSubTrigger({ className, inset, children, @@ -211,32 +208,50 @@ export function DropdownMenuSubTrigger({ }) { return ( {children} - + ); } -export function DropdownMenuSubContent({ +function DropdownMenuSubContent({ className, ...props }: React.ComponentProps) { return ( ); } + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/packages/ui/src/form.tsx b/packages/ui/src/form.tsx new file mode 100644 index 0000000..5dd2131 --- /dev/null +++ b/packages/ui/src/form.tsx @@ -0,0 +1,168 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId(); + + return ( + +
+ + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( +