Adding common dependencies before I try to migrate TechTracker to it
This commit is contained in:
@@ -16,16 +16,26 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@acme/backend": "workspace:*",
|
"@acme/backend": "workspace:*",
|
||||||
"@convex-dev/auth": "catalog:convex",
|
"@convex-dev/auth": "catalog:convex",
|
||||||
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@legendapp/list": "^2.0.14",
|
"@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",
|
"convex": "catalog:convex",
|
||||||
"expo": "~54.0.20",
|
"expo": "~54.0.20",
|
||||||
|
"expo-apple-authentication": "~8.0.7",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-dev-client": "~6.0.16",
|
"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-linking": "~8.0.8",
|
||||||
"expo-router": "~6.0.13",
|
"expo-router": "~6.0.13",
|
||||||
"expo-secure-store": "~15.0.7",
|
"expo-secure-store": "~15.0.7",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.10",
|
||||||
"expo-status-bar": "~3.0.8",
|
"expo-status-bar": "~3.0.8",
|
||||||
|
"expo-symbols": "~1.0.7",
|
||||||
"expo-system-ui": "~6.0.8",
|
"expo-system-ui": "~6.0.8",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
"nativewind": "5.0.0-preview.2",
|
"nativewind": "5.0.0-preview.2",
|
||||||
@@ -37,6 +47,7 @@
|
|||||||
"react-native-reanimated": "~4.1.3",
|
"react-native-reanimated": "~4.1.3",
|
||||||
"react-native-safe-area-context": "~5.6.1",
|
"react-native-safe-area-context": "~5.6.1",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-web": "~0.21.2",
|
||||||
"react-native-worklets": "~0.5.1",
|
"react-native-worklets": "~0.5.1",
|
||||||
"superjson": "2.2.3"
|
"superjson": "2.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,30 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
"./avatar": "./src/avatar.tsx",
|
||||||
|
"./based-avatar": "./src/based-avatar.tsx",
|
||||||
|
"./based-progress": "./src/based-progress.ts",
|
||||||
"./button": "./src/button.tsx",
|
"./button": "./src/button.tsx",
|
||||||
|
"./card": "./src/card.tsx",
|
||||||
|
"./checkbox": "./src/checkbox.tsx",
|
||||||
|
"./drawer": "./src/drawer.tsx",
|
||||||
"./dropdown-menu": "./src/dropdown-menu.tsx",
|
"./dropdown-menu": "./src/dropdown-menu.tsx",
|
||||||
"./field": "./src/field.tsx",
|
"./field": "./src/field.tsx",
|
||||||
|
"./form": "./src/form.tsx",
|
||||||
|
"./image-crop": "./src/shadcn-io/image-crop/index.tsx",
|
||||||
"./input": "./src/input.tsx",
|
"./input": "./src/input.tsx",
|
||||||
|
"./input-otp": "./src/input-otp.tsx",
|
||||||
"./label": "./src/label.tsx",
|
"./label": "./src/label.tsx",
|
||||||
|
"./pagination": "./src/pagination.tsx",
|
||||||
|
"./progress": "./src/progress.tsx",
|
||||||
|
"./scroll-area": "./src/scroll-area.tsx",
|
||||||
"./separator": "./src/separator.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",
|
"./theme": "./src/theme.tsx",
|
||||||
"./toast": "./src/toast.tsx"
|
"./toast": "./src/toast.tsx"
|
||||||
},
|
},
|
||||||
@@ -22,11 +40,30 @@
|
|||||||
"ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"
|
"ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-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",
|
"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",
|
"radix-ui": "^1.4.3",
|
||||||
|
"react-hook-form": "^7.65.0",
|
||||||
|
"react-image-crop": "^11.0.10",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@acme/eslint-config": "workspace:*",
|
"@acme/eslint-config": "workspace:*",
|
||||||
|
|||||||
53
packages/ui/src/avatar.tsx
Normal file
53
packages/ui/src/avatar.tsx
Normal file
@@ -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<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot='avatar'
|
||||||
|
className={cn(
|
||||||
|
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot='avatar-image'
|
||||||
|
className={cn('aspect-square size-full', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot='avatar-fallback'
|
||||||
|
className={cn(
|
||||||
|
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
69
packages/ui/src/based-avatar.tsx
Normal file
69
packages/ui/src/based-avatar.tsx
Normal file
@@ -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<typeof AvatarPrimitive.Root> & {
|
||||||
|
src?: string | null;
|
||||||
|
fullName?: string | null;
|
||||||
|
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
|
||||||
|
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
|
||||||
|
userIconProps?: ComponentProps<typeof User>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BasedAvatar = ({
|
||||||
|
src = null,
|
||||||
|
fullName = null,
|
||||||
|
imageProps,
|
||||||
|
fallbackProps,
|
||||||
|
userIconProps = {
|
||||||
|
size: 32,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BasedAvatarProps) => {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot='avatar'
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{src ? (
|
||||||
|
<AvatarImage
|
||||||
|
{...imageProps}
|
||||||
|
src={src}
|
||||||
|
className={imageProps?.className}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
{...fallbackProps}
|
||||||
|
data-slot='avatar-fallback'
|
||||||
|
className={cn(
|
||||||
|
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||||
|
fallbackProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fullName ? (
|
||||||
|
fullName
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
) : (
|
||||||
|
<User
|
||||||
|
{...userIconProps}
|
||||||
|
className={cn('', userIconProps?.className)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AvatarPrimitive.Fallback>
|
||||||
|
)}
|
||||||
|
</AvatarPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BasedAvatar };
|
||||||
53
packages/ui/src/based-progress.tsx
Normal file
53
packages/ui/src/based-progress.tsx
Normal file
@@ -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<number>(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 (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot='progress'
|
||||||
|
className={cn(
|
||||||
|
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot='progress-indicator'
|
||||||
|
className='bg-primary h-full w-full flex-1 transition-all'
|
||||||
|
style={{ transform: `translateX(-${100 - (progress ?? 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BasedProgress };
|
||||||
@@ -1,57 +1,59 @@
|
|||||||
import type { VariantProps } from "class-variance-authority";
|
import * as React from 'react';
|
||||||
import { cva } from "class-variance-authority";
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { Slot as SlotPrimitive } from "radix-ui";
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
import { cn } from "@acme/ui";
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||||
destructive:
|
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:
|
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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
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",
|
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",
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
icon: "size-9",
|
icon: 'size-9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<'button'> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot='button'
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|||||||
92
packages/ui/src/card.tsx
Normal file
92
packages/ui/src/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card'
|
||||||
|
className={cn(
|
||||||
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-header'
|
||||||
|
className={cn(
|
||||||
|
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-title'
|
||||||
|
className={cn('leading-none font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-description'
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-action'
|
||||||
|
className={cn(
|
||||||
|
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-content'
|
||||||
|
className={cn('px-6', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='card-footer'
|
||||||
|
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
32
packages/ui/src/checkbox.tsx
Normal file
32
packages/ui/src/checkbox.tsx
Normal file
@@ -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<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot='checkbox'
|
||||||
|
className={cn(
|
||||||
|
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot='checkbox-indicator'
|
||||||
|
className='flex items-center justify-center text-current transition-none'
|
||||||
|
>
|
||||||
|
<CheckIcon className='size-3.5' />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
135
packages/ui/src/drawer.tsx
Normal file
135
packages/ui/src/drawer.tsx
Normal file
@@ -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<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot='drawer' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot='drawer-close' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot='drawer-overlay'
|
||||||
|
className={cn(
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot='drawer-portal'>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot='drawer-content'
|
||||||
|
className={cn(
|
||||||
|
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||||
|
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||||
|
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||||
|
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||||
|
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className='bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block' />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='drawer-header'
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='drawer-footer'
|
||||||
|
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot='drawer-title'
|
||||||
|
className={cn('text-foreground font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot='drawer-description'
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
};
|
||||||
@@ -1,40 +1,37 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import {
|
import * as React from 'react';
|
||||||
CheckIcon,
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
ChevronRightIcon,
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||||
DotFilledIcon,
|
|
||||||
} from "@radix-ui/react-icons";
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
||||||
|
|
||||||
import { cn } from "@acme/ui";
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuPortal({
|
function DropdownMenuPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuTrigger({
|
function DropdownMenuTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Trigger
|
<DropdownMenuPrimitive.Trigger
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot='dropdown-menu-trigger'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuContent({
|
function DropdownMenuContent({
|
||||||
className,
|
className,
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
@@ -42,10 +39,10 @@ export function DropdownMenuContent({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
data-slot="dropdown-menu-content"
|
data-slot='dropdown-menu-content'
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -54,26 +51,26 @@ export function DropdownMenuContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuGroup({
|
function DropdownMenuGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuItem({
|
function DropdownMenuItem({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive";
|
variant?: 'default' | 'destructive';
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
data-slot="dropdown-menu-item"
|
data-slot='dropdown-menu-item'
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -85,7 +82,7 @@ export function DropdownMenuItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
@@ -93,7 +90,7 @@ export function DropdownMenuCheckboxItem({
|
|||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot='dropdown-menu-checkbox-item'
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
@@ -101,9 +98,9 @@ export function DropdownMenuCheckboxItem({
|
|||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className='size-4' />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -111,34 +108,34 @@ export function DropdownMenuCheckboxItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot='dropdown-menu-radio-group'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuRadioItem({
|
function DropdownMenuRadioItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot='dropdown-menu-radio-item'
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<DotFilledIcon className="size-2 fill-current" />
|
<CircleIcon className='size-2 fill-current' />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -146,7 +143,7 @@ export function DropdownMenuRadioItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuLabel({
|
function DropdownMenuLabel({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
@@ -155,10 +152,10 @@ export function DropdownMenuLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
data-slot="dropdown-menu-label"
|
data-slot='dropdown-menu-label'
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -166,28 +163,28 @@ export function DropdownMenuLabel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuSeparator({
|
function DropdownMenuSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot='dropdown-menu-separator'
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<'span'>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot='dropdown-menu-shortcut'
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -195,13 +192,13 @@ export function DropdownMenuShortcut({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuSub({
|
function DropdownMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuSubTrigger({
|
function DropdownMenuSubTrigger({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
@@ -211,32 +208,50 @@ export function DropdownMenuSubTrigger({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot='dropdown-menu-sub-trigger'
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className='ml-auto size-4' />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenuSubContent({
|
function DropdownMenuSubContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot='dropdown-menu-sub-content'
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
};
|
||||||
|
|||||||
168
packages/ui/src/form.tsx
Normal file
168
packages/ui/src/form.tsx
Normal file
@@ -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<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <FormField>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot='form-item'
|
||||||
|
className={cn('grid gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot='form-label'
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn('data-[error=true]:text-destructive', className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot='form-control'
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot='form-description'
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message ?? '') : props.children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot='form-message'
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn('text-destructive text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
||||||
@@ -2,3 +2,15 @@ import { cx } from "class-variance-authority";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
|
export const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
|
||||||
|
|
||||||
|
export const ccn = ({
|
||||||
|
context,
|
||||||
|
className,
|
||||||
|
on = '',
|
||||||
|
off = '',
|
||||||
|
}: {
|
||||||
|
context: boolean;
|
||||||
|
className: string;
|
||||||
|
on: string;
|
||||||
|
off: string;
|
||||||
|
}) => twMerge(className, context ? on : off);
|
||||||
|
|||||||
77
packages/ui/src/input-otp.tsx
Normal file
77
packages/ui/src/input-otp.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||||
|
import { MinusIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function InputOTP({
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
|
containerClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<OTPInput
|
||||||
|
data-slot='input-otp'
|
||||||
|
containerClassName={cn(
|
||||||
|
'flex items-center gap-2 has-disabled:opacity-50',
|
||||||
|
containerClassName,
|
||||||
|
)}
|
||||||
|
className={cn('disabled:cursor-not-allowed', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='input-otp-group'
|
||||||
|
className={cn('flex items-center', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSlot({
|
||||||
|
index,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'> & {
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='input-otp-slot'
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
{hasFakeCaret && (
|
||||||
|
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
|
||||||
|
<div className='animate-caret-blink bg-foreground h-4 w-px duration-1000' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div data-slot='input-otp-separator' role='separator' {...props}>
|
||||||
|
<MinusIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import { cn } from "@acme/ui";
|
import * as React from 'react';
|
||||||
|
|
||||||
export function Input({
|
import { cn } from '@/lib/utils';
|
||||||
className,
|
|
||||||
type,
|
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot='input'
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
'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",
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { Label as LabelPrimitive } from "radix-ui";
|
import * as React from 'react';
|
||||||
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
|
|
||||||
import { cn } from "@acme/ui";
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export function Label({
|
function Label({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
data-slot="label"
|
data-slot='label'
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
|
|||||||
127
packages/ui/src/pagination.tsx
Normal file
127
packages/ui/src/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role='navigation'
|
||||||
|
aria-label='pagination'
|
||||||
|
data-slot='pagination'
|
||||||
|
className={cn('mx-auto flex w-full justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'ul'>) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot='pagination-content'
|
||||||
|
className={cn('flex flex-row items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||||
|
return <li data-slot='pagination-item' {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
|
React.ComponentProps<'a'>;
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = 'icon',
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
data-slot='pagination-link'
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? 'outline' : 'ghost',
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label='Go to previous page'
|
||||||
|
size='default'
|
||||||
|
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className='hidden sm:block'>Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label='Go to next page'
|
||||||
|
size='default'
|
||||||
|
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className='hidden sm:block'>Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot='pagination-ellipsis'
|
||||||
|
className={cn('flex size-9 items-center justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className='size-4' />
|
||||||
|
<span className='sr-only'>More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
};
|
||||||
31
packages/ui/src/progress.tsx
Normal file
31
packages/ui/src/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot='progress'
|
||||||
|
className={cn(
|
||||||
|
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot='progress-indicator'
|
||||||
|
className='bg-primary h-full w-full flex-1 transition-all'
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
58
packages/ui/src/scroll-area.tsx
Normal file
58
packages/ui/src/scroll-area.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot='scroll-area'
|
||||||
|
className={cn('relative', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot='scroll-area-viewport'
|
||||||
|
className='focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1'
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = 'vertical',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot='scroll-area-scrollbar'
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none p-px transition-colors select-none',
|
||||||
|
orientation === 'vertical' &&
|
||||||
|
'h-full w-2.5 border-l border-l-transparent',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'h-2.5 flex-col border-t border-t-transparent',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot='scroll-area-thumb'
|
||||||
|
className='bg-border relative flex-1 rounded-full'
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { Separator as SeparatorPrimitive } from "radix-ui";
|
import * as React from 'react';
|
||||||
|
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||||
|
|
||||||
import { cn } from "@acme/ui";
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
orientation = "horizontal",
|
orientation = 'horizontal',
|
||||||
decorative = true,
|
decorative = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
data-slot="separator"
|
data-slot='separator'
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
|
|||||||
368
packages/ui/src/shadcn-io/image-crop/index.tsx
Normal file
368
packages/ui/src/shadcn-io/image-crop/index.tsx
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { CropIcon, RotateCcwIcon } from 'lucide-react';
|
||||||
|
import { Slot } from 'radix-ui';
|
||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
type CSSProperties,
|
||||||
|
createContext,
|
||||||
|
type MouseEvent,
|
||||||
|
type ReactNode,
|
||||||
|
type RefObject,
|
||||||
|
type SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import ReactCrop, {
|
||||||
|
centerCrop,
|
||||||
|
makeAspectCrop,
|
||||||
|
type PercentCrop,
|
||||||
|
type PixelCrop,
|
||||||
|
type ReactCropProps,
|
||||||
|
} from 'react-image-crop';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
import 'react-image-crop/dist/ReactCrop.css';
|
||||||
|
|
||||||
|
const centerAspectCrop = (
|
||||||
|
mediaWidth: number,
|
||||||
|
mediaHeight: number,
|
||||||
|
aspect: number | undefined,
|
||||||
|
): PercentCrop =>
|
||||||
|
centerCrop(
|
||||||
|
aspect
|
||||||
|
? makeAspectCrop(
|
||||||
|
{
|
||||||
|
unit: '%',
|
||||||
|
width: 90,
|
||||||
|
},
|
||||||
|
aspect,
|
||||||
|
mediaWidth,
|
||||||
|
mediaHeight,
|
||||||
|
)
|
||||||
|
: { x: 0, y: 0, width: 90, height: 90, unit: '%' },
|
||||||
|
mediaWidth,
|
||||||
|
mediaHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCroppedPngImage = async (
|
||||||
|
imageSrc: HTMLImageElement,
|
||||||
|
scaleFactor: number,
|
||||||
|
pixelCrop: PixelCrop,
|
||||||
|
maxImageSize: number,
|
||||||
|
): Promise<string> => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('Context is null, this should never happen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = imageSrc.naturalWidth / imageSrc.width;
|
||||||
|
const scaleY = imageSrc.naturalHeight / imageSrc.height;
|
||||||
|
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
canvas.width = pixelCrop.width;
|
||||||
|
canvas.height = pixelCrop.height;
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
imageSrc,
|
||||||
|
pixelCrop.x * scaleX,
|
||||||
|
pixelCrop.y * scaleY,
|
||||||
|
pixelCrop.width * scaleX,
|
||||||
|
pixelCrop.height * scaleY,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
const croppedImageUrl = canvas.toDataURL('image/png');
|
||||||
|
const response = await fetch(croppedImageUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
if (blob.size > maxImageSize) {
|
||||||
|
return await getCroppedPngImage(
|
||||||
|
imageSrc,
|
||||||
|
scaleFactor * 0.9,
|
||||||
|
pixelCrop,
|
||||||
|
maxImageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return croppedImageUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageCropContextType = {
|
||||||
|
file: File;
|
||||||
|
maxImageSize: number;
|
||||||
|
imgSrc: string;
|
||||||
|
crop: PercentCrop | undefined;
|
||||||
|
completedCrop: PixelCrop | null;
|
||||||
|
imgRef: RefObject<HTMLImageElement | null>;
|
||||||
|
onCrop?: (croppedImage: string) => void;
|
||||||
|
reactCropProps: Omit<ReactCropProps, 'onChange' | 'onComplete' | 'children'>;
|
||||||
|
handleChange: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void;
|
||||||
|
handleComplete: (
|
||||||
|
pixelCrop: PixelCrop,
|
||||||
|
percentCrop: PercentCrop,
|
||||||
|
) => Promise<void>;
|
||||||
|
onImageLoad: (e: SyntheticEvent<HTMLImageElement>) => void;
|
||||||
|
applyCrop: () => Promise<void>;
|
||||||
|
resetCrop: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageCropContext = createContext<ImageCropContextType | null>(null);
|
||||||
|
|
||||||
|
const useImageCrop = () => {
|
||||||
|
const context = useContext(ImageCropContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('ImageCrop components must be used within ImageCrop');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageCropProps = {
|
||||||
|
file: File;
|
||||||
|
maxImageSize?: number;
|
||||||
|
onCrop?: (croppedImage: string) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
onChange?: ReactCropProps['onChange'];
|
||||||
|
onComplete?: ReactCropProps['onComplete'];
|
||||||
|
} & Omit<ReactCropProps, 'onChange' | 'onComplete' | 'children'>;
|
||||||
|
|
||||||
|
export const ImageCrop = ({
|
||||||
|
file,
|
||||||
|
maxImageSize = 1024 * 1024 * 5,
|
||||||
|
onCrop,
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
onComplete,
|
||||||
|
...reactCropProps
|
||||||
|
}: ImageCropProps) => {
|
||||||
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
const [imgSrc, setImgSrc] = useState<string>('');
|
||||||
|
const [crop, setCrop] = useState<PercentCrop>();
|
||||||
|
const [completedCrop, setCompletedCrop] = useState<PixelCrop | null>(null);
|
||||||
|
const [initialCrop, setInitialCrop] = useState<PercentCrop>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () =>
|
||||||
|
setImgSrc(reader.result?.toString() || ''),
|
||||||
|
);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
const onImageLoad = useCallback(
|
||||||
|
(e: SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
const { width, height } = e.currentTarget;
|
||||||
|
const newCrop = centerAspectCrop(width, height, reactCropProps.aspect);
|
||||||
|
setCrop(newCrop);
|
||||||
|
setInitialCrop(newCrop);
|
||||||
|
},
|
||||||
|
[reactCropProps.aspect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (pixelCrop: PixelCrop, percentCrop: PercentCrop) => {
|
||||||
|
setCrop(percentCrop);
|
||||||
|
onChange?.(pixelCrop, percentCrop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/useAwait: "onComplete is async"
|
||||||
|
const handleComplete = async (
|
||||||
|
pixelCrop: PixelCrop,
|
||||||
|
percentCrop: PercentCrop,
|
||||||
|
) => {
|
||||||
|
setCompletedCrop(pixelCrop);
|
||||||
|
onComplete?.(pixelCrop, percentCrop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyCrop = async () => {
|
||||||
|
if (!(imgRef.current && completedCrop)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const croppedImage = await getCroppedPngImage(
|
||||||
|
imgRef.current,
|
||||||
|
1,
|
||||||
|
completedCrop,
|
||||||
|
maxImageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
onCrop?.(croppedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCrop = () => {
|
||||||
|
if (initialCrop) {
|
||||||
|
setCrop(initialCrop);
|
||||||
|
setCompletedCrop(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: ImageCropContextType = {
|
||||||
|
file,
|
||||||
|
maxImageSize,
|
||||||
|
imgSrc,
|
||||||
|
crop,
|
||||||
|
completedCrop,
|
||||||
|
imgRef,
|
||||||
|
onCrop,
|
||||||
|
reactCropProps,
|
||||||
|
handleChange,
|
||||||
|
handleComplete,
|
||||||
|
onImageLoad,
|
||||||
|
applyCrop,
|
||||||
|
resetCrop,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageCropContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ImageCropContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageCropContentProps = {
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageCropContent = ({
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
}: ImageCropContentProps) => {
|
||||||
|
const {
|
||||||
|
imgSrc,
|
||||||
|
crop,
|
||||||
|
handleChange,
|
||||||
|
handleComplete,
|
||||||
|
onImageLoad,
|
||||||
|
imgRef,
|
||||||
|
reactCropProps,
|
||||||
|
} = useImageCrop();
|
||||||
|
|
||||||
|
const shadcnStyle = {
|
||||||
|
'--rc-border-color': 'var(--color-border)',
|
||||||
|
'--rc-focus-color': 'var(--color-primary)',
|
||||||
|
} as CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactCrop
|
||||||
|
className={cn('max-h-[277px] max-w-full', className)}
|
||||||
|
crop={crop}
|
||||||
|
onChange={handleChange}
|
||||||
|
onComplete={handleComplete}
|
||||||
|
style={{ ...shadcnStyle, ...style }}
|
||||||
|
{...reactCropProps}
|
||||||
|
>
|
||||||
|
{imgSrc && (
|
||||||
|
<img
|
||||||
|
alt='crop'
|
||||||
|
className='size-full'
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
ref={imgRef}
|
||||||
|
src={imgSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ReactCrop>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageCropApplyProps = ComponentProps<'button'> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageCropApply = ({
|
||||||
|
asChild = false,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: ImageCropApplyProps) => {
|
||||||
|
const { applyCrop } = useImageCrop();
|
||||||
|
|
||||||
|
const handleClick = async (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
await applyCrop();
|
||||||
|
onClick?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (asChild) {
|
||||||
|
return (
|
||||||
|
<Slot.Root onClick={handleClick} {...props}>
|
||||||
|
{children}
|
||||||
|
</Slot.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
|
||||||
|
{children ?? <CropIcon className='size-4' />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageCropResetProps = ComponentProps<'button'> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageCropReset = ({
|
||||||
|
asChild = false,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: ImageCropResetProps) => {
|
||||||
|
const { resetCrop } = useImageCrop();
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
resetCrop();
|
||||||
|
onClick?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (asChild) {
|
||||||
|
return (
|
||||||
|
<Slot.Root onClick={handleClick} {...props}>
|
||||||
|
{children}
|
||||||
|
</Slot.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleClick} size='icon' variant='ghost' {...props}>
|
||||||
|
{children ?? <RotateCcwIcon className='size-4' />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep the original Cropper component for backward compatibility
|
||||||
|
export type CropperProps = Omit<ReactCropProps, 'onChange'> & {
|
||||||
|
file: File;
|
||||||
|
maxImageSize?: number;
|
||||||
|
onCrop?: (croppedImage: string) => void;
|
||||||
|
onChange?: ReactCropProps['onChange'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Cropper = ({
|
||||||
|
onChange,
|
||||||
|
onComplete,
|
||||||
|
onCrop,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
file,
|
||||||
|
maxImageSize,
|
||||||
|
...props
|
||||||
|
}: CropperProps) => (
|
||||||
|
<ImageCrop
|
||||||
|
file={file}
|
||||||
|
maxImageSize={maxImageSize}
|
||||||
|
onChange={onChange}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onCrop={onCrop}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ImageCropContent className={className} style={style} />
|
||||||
|
</ImageCrop>
|
||||||
|
);
|
||||||
25
packages/ui/src/sonner.tsx
Normal file
25
packages/ui/src/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { Toaster as Sonner, ToasterProps } from 'sonner';
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = 'system' } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps['theme']}
|
||||||
|
className='toaster group'
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--normal-bg': 'var(--popover)',
|
||||||
|
'--normal-text': 'var(--popover-foreground)',
|
||||||
|
'--normal-border': 'var(--border)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster };
|
||||||
58
packages/ui/src/status-message.tsx
Normal file
58
packages/ui/src/status-message.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Message = { success: string } | { error: string } | { message: string };
|
||||||
|
|
||||||
|
type StatusMessageProps = {
|
||||||
|
message: Message;
|
||||||
|
containerProps?: ComponentProps<'div'>;
|
||||||
|
textProps?: ComponentProps<'div'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusMessage = ({
|
||||||
|
message,
|
||||||
|
containerProps,
|
||||||
|
textProps,
|
||||||
|
}: StatusMessageProps) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center w-full'>
|
||||||
|
{'success' in message && (
|
||||||
|
<div
|
||||||
|
{...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'dark:bg-green-500/20 bg-green-700/20 border-2',
|
||||||
|
'dark:border-green-500/50 border-green-700/50',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.success}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{'error' in message && (
|
||||||
|
<div
|
||||||
|
{...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'bg-destructive/20 border-2 border-destructive/80',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{'message' in message && (
|
||||||
|
<div
|
||||||
|
{...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'bg-accent/20 border-2 border-primary/80',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
51
packages/ui/src/submit-button.tsx
Normal file
51
packages/ui/src/submit-button.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { useFormStatus } from 'react-dom';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type SubmitButtonProps = Omit<
|
||||||
|
ComponentProps<typeof Button>,
|
||||||
|
'type' | 'aria-disabled'
|
||||||
|
> & {
|
||||||
|
pendingText?: string;
|
||||||
|
pendingTextProps?: ComponentProps<'p'>;
|
||||||
|
loaderProps?: ComponentProps<typeof Loader2>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubmitButton = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
pendingText = 'Submitting...',
|
||||||
|
pendingTextProps,
|
||||||
|
loaderProps,
|
||||||
|
...props
|
||||||
|
}: SubmitButtonProps) => {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
aria-disabled={pending}
|
||||||
|
{...props}
|
||||||
|
className={cn('cursor-pointer', className)}
|
||||||
|
>
|
||||||
|
{pending || props.disabled ? (
|
||||||
|
<>
|
||||||
|
<Loader2
|
||||||
|
{...loaderProps}
|
||||||
|
className={cn('mr-2 h-4 w-4 animate-spin', loaderProps?.className)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
{...pendingTextProps}
|
||||||
|
className={cn('text-sm font-medium', pendingTextProps?.className)}
|
||||||
|
>
|
||||||
|
{pendingText}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
packages/ui/src/switch.tsx
Normal file
31
packages/ui/src/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot='switch'
|
||||||
|
className={cn(
|
||||||
|
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot='switch-thumb'
|
||||||
|
className={cn(
|
||||||
|
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
116
packages/ui/src/table.tsx
Normal file
116
packages/ui/src/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot='table-container'
|
||||||
|
className='relative w-full overflow-x-auto'
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot='table'
|
||||||
|
className={cn('w-full caption-bottom text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot='table-header'
|
||||||
|
className={cn('[&_tr]:border-b', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot='table-body'
|
||||||
|
className={cn('[&_tr:last-child]:border-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot='table-footer'
|
||||||
|
className={cn(
|
||||||
|
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot='table-row'
|
||||||
|
className={cn(
|
||||||
|
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot='table-head'
|
||||||
|
className={cn(
|
||||||
|
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot='table-cell'
|
||||||
|
className={cn(
|
||||||
|
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'caption'>) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot='table-caption'
|
||||||
|
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
66
packages/ui/src/tabs.tsx
Normal file
66
packages/ui/src/tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot='tabs'
|
||||||
|
className={cn('flex flex-col gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot='tabs-list'
|
||||||
|
className={cn(
|
||||||
|
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot='tabs-trigger'
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot='tabs-content'
|
||||||
|
className={cn('flex-1 outline-none', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
550
pnpm-lock.yaml
generated
550
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user