fix: pc端修改
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"aliases": {
|
||||
"components": "@workspace/airo-ui/components",
|
||||
"utils": "@workspace/airo-ui/lib/utils",
|
||||
"hooks": "@workspace/airo-ui/hooks",
|
||||
"lib": "@workspace/airo-ui/lib",
|
||||
"ui": "@workspace/airo-ui/components"
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rsc": true,
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"tsx": true
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { config } from '@workspace/eslint-config/react-internal';
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default config;
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "@workspace/airo-ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./postcss.config": "./postcss.config.mjs",
|
||||
"./tailwind.config": "./tailwind.config.ts",
|
||||
"./lib/*": "./src/lib/*.ts",
|
||||
"./lotties/*": "./src/lotties/*.json",
|
||||
"./components/*": "./src/components/*.tsx",
|
||||
"./custom-components/*": "./src/custom-components/*.tsx",
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
"./utils/*": "./src/utils/*.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@iconify-json/flagpack": "^1.2.2",
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/mdi": "^1.2.2",
|
||||
"@iconify-json/simple-icons": "^1.2.20",
|
||||
"@iconify-json/uil": "^1.2.3",
|
||||
"@iconify/react": "^5.2.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-menubar": "^1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"framer-motion": "^11.18.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.473.0",
|
||||
"mathjs": "^14.0.1",
|
||||
"motion": "^11.18.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.15.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"rtl-detect": "^1.1.2",
|
||||
"sonner": "^1.7.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbo/gen": "^2.3.3",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/rtl-detect": "^1.0.3",
|
||||
"@workspace/eslint-config": "workspace:*",
|
||||
"@workspace/typescript-config": "workspace:*",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className='flex'>
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className='text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200' />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className='data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm'
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import * as React from 'react';
|
||||
|
||||
import { buttonVariants } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.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/80',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
);
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div ref={ref} role='alert' className={cn(alertVariants({ variant }), className)} {...props} />
|
||||
));
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
AlertTitle.displayName = 'AlertTitle';
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||
));
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root;
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-muted flex h-full w-full items-center justify-center rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarFallback, AvatarImage };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<'nav'> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label='breadcrumb' {...props} />);
|
||||
Breadcrumb.displayName = 'Breadcrumb';
|
||||
|
||||
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
BreadcrumbList.displayName = 'BreadcrumbList';
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />
|
||||
),
|
||||
);
|
||||
BreadcrumbItem.displayName = 'BreadcrumbItem';
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<'a'> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn('hover:text-foreground transition-colors', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = 'BreadcrumbLink';
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role='link'
|
||||
aria-disabled='true'
|
||||
aria-current='page'
|
||||
className={cn('text-foreground font-normal', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
BreadcrumbPage.displayName = 'BreadcrumbPage';
|
||||
|
||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => (
|
||||
<li
|
||||
role='presentation'
|
||||
aria-hidden='true'
|
||||
className={cn('[&>svg]:h-3.5 [&>svg]:w-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
|
||||
|
||||
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
role='presentation'
|
||||
aria-hidden='true'
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
|
||||
import { buttonVariants } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
||||
month: 'space-y-4',
|
||||
caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-y-1',
|
||||
head_row: 'flex',
|
||||
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: cn(
|
||||
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
|
||||
props.mode === 'range'
|
||||
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
|
||||
: '[&:has([aria-selected])]:rounded-md',
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
|
||||
),
|
||||
day_range_start: 'day-range-start',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn('h-4 w-4', className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn('h-4 w-4', className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = 'Calendar';
|
||||
|
||||
export { Calendar };
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('bg-card text-card-foreground rounded-xl border shadow', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
role='region'
|
||||
aria-roledescription='carousel'
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
});
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className='overflow-hidden'>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
|
||||
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role='group'
|
||||
aria-roledescription='slide'
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
|
||||
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
||||
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-left-12 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className='h-4 w-4' />
|
||||
<span className='sr-only'>Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
|
||||
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
||||
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-right-12 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className='h-4 w-4' />
|
||||
<span className='sr-only'>Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
|
||||
export {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
type CarouselApi,
|
||||
};
|
||||
@@ -0,0 +1,329 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as RechartsPrimitive from 'recharts';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useChart must be used within a <ChartContainer />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = 'Chart';
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join('\n'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<'div'> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: 'line' | 'dot' | 'dashed';
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = 'dot',
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === 'string'
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
|
||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== 'dot';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className='grid gap-1.5'>
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||
indicator === 'dot' && 'items-center',
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
|
||||
{
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div className='grid gap-1.5'>
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className='text-muted-foreground'>
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className='text-foreground font-mono font-medium tabular-nums'>
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = 'ChartTooltip';
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className='h-2 w-2 shrink-0 rounded-[2px]'
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ChartLegendContent.displayName = 'ChartLegend';
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer h-4 w-4 shrink-0 rounded-sm border shadow focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<Check className='h-4 w-4' />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|
||||
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { type DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Dialog, DialogContent } from '@workspace/airo-ui/components/dialog';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className='overflow-hidden p-0'>
|
||||
<Command className='[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
|
||||
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty ref={ref} className='py-6 text-center text-sm' {...props} />
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-border -mx-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = 'CommandShortcut';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className='ml-auto h-4 w-4' />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
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] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
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] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className='h-4 w-4' />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className='h-4 w-4 fill-current' />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('text-foreground px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||
|
||||
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuPortal,
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import * as React from 'react';
|
||||
|
||||
import CloseSvg from '@/components/CustomIcon/icons/close.svg';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import Image from 'next/image';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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/80',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
// 扩展 DialogContentProps 接口
|
||||
interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||
closeIcon?: React.ReactNode;
|
||||
closeClassName?: string;
|
||||
}
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
DialogContentProps
|
||||
>(({ className, closeClassName, closeIcon, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-0 !container fixed left-[50%] top-[50%] z-50 grid h-full w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-0 gap-4 border p-6 px-8 py-8 shadow-lg duration-200 sm:h-auto sm:!rounded-[32px] sm:rounded-lg sm:px-12 sm:py-12',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
'ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-6 top-6 rounded-sm font-bold text-black opacity-100 transition-opacity hover:opacity-100 focus:outline-none focus:ring-0 focus:ring-offset-0 disabled:pointer-events-none',
|
||||
closeClassName,
|
||||
)}
|
||||
>
|
||||
<Image src={CloseSvg} alt={'close'} />
|
||||
<span className='sr-only'>Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
||||
);
|
||||
Drawer.displayName = 'Drawer';
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn('fixed inset-0 z-50 bg-black/80', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className='bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full' />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = 'DrawerContent';
|
||||
|
||||
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)} {...props} />
|
||||
);
|
||||
DrawerHeader.displayName = 'DrawerHeader';
|
||||
|
||||
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />
|
||||
);
|
||||
DrawerFooter.displayName = 'DrawerFooter';
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
DrawerPortal,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className='ml-auto' />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
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] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className='h-4 w-4' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className='h-2 w-2 fill-current' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
|
||||
import { Label } from '@workspace/airo-ui/components/label';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
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, formState } = useFormContext();
|
||||
|
||||
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);
|
||||
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = 'FormLabel';
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = 'FormControl';
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-muted-foreground text-[0.8rem]', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = 'FormDescription';
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-destructive text-[0.8rem] font-medium', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = 'FormMessage';
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type Direction = 'TOP' | 'LEFT' | 'BOTTOM' | 'RIGHT';
|
||||
|
||||
export function HoverBorderGradient({
|
||||
children,
|
||||
containerClassName,
|
||||
className,
|
||||
as: Tag = 'button',
|
||||
duration = 1,
|
||||
clockwise = true,
|
||||
...props
|
||||
}: React.PropsWithChildren<
|
||||
{
|
||||
as?: React.ElementType;
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
clockwise?: boolean;
|
||||
} & React.HTMLAttributes<HTMLElement>
|
||||
>) {
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
const [direction, setDirection] = useState<Direction>('TOP');
|
||||
|
||||
const rotateDirection = (currentDirection: Direction): Direction => {
|
||||
const directions: Direction[] = ['TOP', 'LEFT', 'BOTTOM', 'RIGHT'];
|
||||
const currentIndex = directions.indexOf(currentDirection);
|
||||
const nextIndex = clockwise
|
||||
? (currentIndex - 1 + directions.length) % directions.length
|
||||
: (currentIndex + 1) % directions.length;
|
||||
return directions[nextIndex] as Direction;
|
||||
};
|
||||
|
||||
const movingMap: Record<Direction, string> = {
|
||||
TOP: 'radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
LEFT: 'radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
BOTTOM:
|
||||
'radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
RIGHT:
|
||||
'radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)',
|
||||
};
|
||||
|
||||
const highlight =
|
||||
'radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)';
|
||||
|
||||
useEffect(() => {
|
||||
if (!hovered) {
|
||||
const interval = setInterval(() => {
|
||||
setDirection((prevState) => rotateDirection(prevState));
|
||||
}, duration * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hovered]);
|
||||
return (
|
||||
<Tag
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={cn(
|
||||
'relative flex h-min w-fit flex-col flex-nowrap content-center items-center justify-center gap-10 overflow-visible rounded-full border bg-black/20 decoration-clone p-px transition duration-500 hover:bg-black/10 dark:bg-white/20',
|
||||
containerClassName,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('z-10 w-auto rounded-[inherit] bg-black px-4 py-2 text-white', className)}>
|
||||
{children}
|
||||
</div>
|
||||
<motion.div
|
||||
className={cn('absolute inset-0 z-0 flex-none overflow-hidden rounded-[inherit]')}
|
||||
style={{
|
||||
filter: 'blur(2px)',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
initial={{ background: movingMap[direction] }}
|
||||
animate={{
|
||||
background: hovered ? [movingMap[direction], highlight] : movingMap[direction],
|
||||
}}
|
||||
transition={{ ease: 'linear', duration: duration ?? 1 }}
|
||||
/>
|
||||
<div className='z-1 absolute inset-[2px] flex-none rounded-[100px] bg-black' />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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 w-64 rounded-md border p-4 shadow-md outline-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
||||
@@ -0,0 +1,133 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { AnimatePresence, motion, MotionProps } from 'motion/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type CharacterSet = string[] | readonly string[];
|
||||
|
||||
interface HyperTextProps extends MotionProps {
|
||||
/** The text content to be animated */
|
||||
children: string;
|
||||
/** Optional className for styling */
|
||||
className?: string;
|
||||
/** Duration of the animation in milliseconds */
|
||||
duration?: number;
|
||||
/** Delay before animation starts in milliseconds */
|
||||
delay?: number;
|
||||
/** Component to render as - defaults to div */
|
||||
as?: React.ElementType;
|
||||
/** Whether to start animation when element comes into view */
|
||||
startOnView?: boolean;
|
||||
/** Whether to trigger animation on hover */
|
||||
animateOnHover?: boolean;
|
||||
/** Custom character set for scramble effect. Defaults to uppercase alphabet */
|
||||
characterSet?: CharacterSet;
|
||||
}
|
||||
|
||||
const DEFAULT_CHARACTER_SET = Object.freeze(
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
|
||||
) as readonly string[];
|
||||
|
||||
const getRandomInt = (max: number): number => Math.floor(Math.random() * max);
|
||||
|
||||
export default function HyperText({
|
||||
children,
|
||||
className,
|
||||
duration = 800,
|
||||
delay = 0,
|
||||
as: Component = 'div',
|
||||
startOnView = false,
|
||||
animateOnHover = true,
|
||||
characterSet = DEFAULT_CHARACTER_SET,
|
||||
...props
|
||||
}: HyperTextProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
});
|
||||
|
||||
const [displayText, setDisplayText] = useState<string[]>(() => children.split(''));
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const iterationCount = useRef(0);
|
||||
const elementRef = useRef<HTMLElement>(null);
|
||||
|
||||
const handleAnimationTrigger = () => {
|
||||
if (animateOnHover && !isAnimating) {
|
||||
iterationCount.current = 0;
|
||||
setIsAnimating(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle animation start based on view or delay
|
||||
useEffect(() => {
|
||||
if (!startOnView) {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setIsAnimating(true);
|
||||
}, delay);
|
||||
return () => clearTimeout(startTimeout);
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
setIsAnimating(true);
|
||||
}, delay);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '-30% 0px -30% 0px' },
|
||||
);
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [delay, startOnView]);
|
||||
|
||||
// Handle scramble animation
|
||||
useEffect(() => {
|
||||
if (!isAnimating) return;
|
||||
|
||||
const intervalDuration = duration / (children.length * 10);
|
||||
const maxIterations = children.length;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (iterationCount.current < maxIterations) {
|
||||
setDisplayText((currentText) =>
|
||||
currentText.map((letter, index) =>
|
||||
letter === ' '
|
||||
? letter
|
||||
: index <= iterationCount.current
|
||||
? (children[index] ?? ' ')
|
||||
: (characterSet[getRandomInt(characterSet.length)] ?? ' '),
|
||||
),
|
||||
);
|
||||
iterationCount.current = iterationCount.current + 0.1;
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, intervalDuration);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [children, duration, isAnimating, characterSet]);
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn('overflow-hidden py-2 text-4xl font-bold', className)}
|
||||
onMouseEnter={handleAnimationTrigger}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{displayText.map((letter, index) => (
|
||||
<motion.span key={index} className={cn('font-mono', letter === ' ' ? 'w-3' : '')}>
|
||||
{letter.toUpperCase()}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</MotionComponent>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { Minus } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = 'InputOTP';
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = 'InputOTPGroup';
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'ring-ring z-10 ring-1',
|
||||
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>
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = 'InputOTPSlot';
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role='separator' {...props}>
|
||||
<Minus />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = 'InputOTPSeparator';
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background flex h-9 items-center space-x-1 rounded-md border p-1 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className='ml-auto h-4 w-4' />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
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] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in 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-[12rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
));
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className='h-4 w-4' />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className='h-4 w-4 fill-current' />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = 'MenubarShortcut';
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarCheckboxItem,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarItem,
|
||||
MenubarLabel,
|
||||
MenubarMenu,
|
||||
MenubarPortal,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative z-10 flex max-w-max flex-1 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('group flex flex-1 list-none items-center justify-center space-x-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{' '}
|
||||
<ChevronDown
|
||||
className='relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full md:absolute md:w-auto',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn('absolute left-0 top-full flex justify-center')}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import React from 'react';
|
||||
|
||||
export interface OrbitingCirclesProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
reverse?: boolean;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
radius?: number;
|
||||
path?: boolean;
|
||||
iconSize?: number;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export function OrbitingCircles({
|
||||
className,
|
||||
children,
|
||||
reverse,
|
||||
duration = 20,
|
||||
radius = 160,
|
||||
path = true,
|
||||
iconSize = 30,
|
||||
speed = 1,
|
||||
...props
|
||||
}: OrbitingCirclesProps) {
|
||||
const calculatedDuration = duration / speed;
|
||||
return (
|
||||
<>
|
||||
{path && (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
version='1.1'
|
||||
className='pointer-events-none absolute inset-0 size-full'
|
||||
>
|
||||
<circle
|
||||
className='stroke-black/10 stroke-1 dark:stroke-white/10'
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
r={radius}
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{React.Children.map(children, (child, index) => {
|
||||
const angle = (360 / React.Children.count(children)) * index;
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--duration': calculatedDuration,
|
||||
'--radius': radius,
|
||||
'--angle': angle,
|
||||
'--icon-size': `${iconSize}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
`animate-orbit absolute flex size-[var(--icon-size)] transform-gpu items-center justify-center rounded-full`,
|
||||
{ '[animation-direction:reverse]': reverse },
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ButtonProps, buttonVariants } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role='navigation'
|
||||
aria-label='pagination'
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = 'Pagination';
|
||||
|
||||
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
|
||||
),
|
||||
);
|
||||
PaginationContent.displayName = 'PaginationContent';
|
||||
|
||||
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
|
||||
({ className, ...props }, ref) => <li ref={ref} className={cn('', className)} {...props} />,
|
||||
);
|
||||
PaginationItem.displayName = 'PaginationItem';
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
PaginationLink.displayName = 'PaginationLink';
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label='Go to previous page'
|
||||
size='default'
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className='h-4 w-4' />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
||||
|
||||
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label='Go to next page'
|
||||
size='default'
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className='h-4 w-4' />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = 'PaginationNext';
|
||||
|
||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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 w-72 rounded-md border p-4 shadow-md outline-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('bg-primary/20 relative h-2 w-full overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className='bg-primary h-full w-full flex-1 transition-all'
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import { Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />;
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
|
||||
<Circle className='fill-primary h-3.5 w-3.5' />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel;
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border'>
|
||||
<GripVertical className='h-2.5 w-2.5' />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
|
||||
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className='h-full w-full rounded-[inherit]'>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className='bg-border relative flex-1 rounded-full' />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className='h-4 w-4 opacity-50' />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className='h-4 w-4' />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className='h-4 w-4' />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
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 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className='absolute right-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className='h-4 w-4' />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.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/80',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
<SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none'>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
);
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-foreground text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
@@ -0,0 +1,741 @@
|
||||
'use client';
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { PanelLeft } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { Separator } from '@workspace/airo-ui/components/separator';
|
||||
import { Sheet, SheetContent } from '@workspace/airo-ui/components/sheet';
|
||||
import { Skeleton } from '@workspace/airo-ui/components/skeleton';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@workspace/airo-ui/components/tooltip';
|
||||
import { useIsMobile } from '@workspace/airo-ui/hooks/use-mobile';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
type SidebarContext = {
|
||||
state: 'expanded' | 'collapsed';
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === 'function' ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? 'expanded' : 'collapsed';
|
||||
|
||||
const contextValue = React.useMemo<SidebarContext>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = 'SidebarProvider';
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === 'none') {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar='sidebar'
|
||||
data-mobile='true'
|
||||
className='bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden'
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<div className='flex h-full w-full flex-col'>{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className='text-sidebar-foreground group peer hidden md:block'
|
||||
data-state={state}
|
||||
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar='sidebar'
|
||||
className='bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow'
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = 'Sidebar';
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar='trigger'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className={cn('h-7 w-7', className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className='sr-only'>Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = 'SidebarTrigger';
|
||||
|
||||
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar='rail'
|
||||
aria-label='Toggle Sidebar'
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title='Toggle Sidebar'
|
||||
className={cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarRail.displayName = 'SidebarRail';
|
||||
|
||||
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background relative flex min-h-svh flex-1 flex-col',
|
||||
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarInset.displayName = 'SidebarInset';
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar='input'
|
||||
className={cn(
|
||||
'bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = 'SidebarInput';
|
||||
|
||||
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='header'
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarHeader.displayName = 'SidebarHeader';
|
||||
|
||||
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='footer'
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarFooter.displayName = 'SidebarFooter';
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar='separator'
|
||||
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = 'SidebarSeparator';
|
||||
|
||||
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='content'
|
||||
className={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarContent.displayName = 'SidebarContent';
|
||||
|
||||
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='group'
|
||||
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarGroup.displayName = 'SidebarGroup';
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'div';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar='group-label'
|
||||
className={cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar='group-action'
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 after:md:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = 'SidebarGroupAction';
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='group-content'
|
||||
className={cn('w-full text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
SidebarGroupContent.displayName = 'SidebarGroupContent';
|
||||
|
||||
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar='menu'
|
||||
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
SidebarMenu.displayName = 'SidebarMenu';
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar='menu-item'
|
||||
className={cn('group/menu-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
SidebarMenuItem.displayName = 'SidebarMenuItem';
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-sm',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar='menu-button'
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === 'string') {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='right'
|
||||
align='center'
|
||||
hidden={state !== 'collapsed' || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = 'SidebarMenuButton';
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar='menu-action'
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 after:md:hidden',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
showOnHover &&
|
||||
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = 'SidebarMenuAction';
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='menu-badge'
|
||||
className={cn(
|
||||
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar='menu-skeleton'
|
||||
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && <Skeleton className='size-4 rounded-md' data-sidebar='menu-skeleton-icon' />}
|
||||
<Skeleton
|
||||
className='h-4 max-w-[--skeleton-width] flex-1'
|
||||
data-sidebar='menu-skeleton-text'
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar='menu-sub'
|
||||
className={cn(
|
||||
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
SidebarMenuSub.displayName = 'SidebarMenuSub';
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
|
||||
({ ...props }, ref) => <li ref={ref} {...props} />,
|
||||
);
|
||||
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<'a'> & {
|
||||
asChild?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar='menu-sub-button'
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||
size === 'sm' && 'text-xs',
|
||||
size === 'md' && 'text-sm',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('bg-primary/10 animate-pulse rounded-md', className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className='bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full'>
|
||||
<SliderPrimitive.Range className='bg-primary absolute h-full' />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className='border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50' />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Toaster as Sonner } from 'sonner';
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className='toaster group'
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||
description: 'group-[.toast]:text-muted-foreground',
|
||||
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
interface SwitchProps extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> {
|
||||
thumbClassName?: string;
|
||||
}
|
||||
|
||||
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, SwitchProps>(
|
||||
({ className, thumbClassName, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
||||
thumbClassName,
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
),
|
||||
);
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className='relative w-full overflow-auto'>
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
</div>
|
||||
),
|
||||
);
|
||||
Table.displayName = 'Table';
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
TableRow.displayName = 'TableRow';
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = 'TableHead';
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = 'TableCell';
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
||||
));
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { motion, stagger, useAnimate } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const TextGenerateEffect = ({
|
||||
words,
|
||||
className,
|
||||
filter = true,
|
||||
duration = 0.5,
|
||||
}: {
|
||||
words: string;
|
||||
className?: string;
|
||||
filter?: boolean;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const [scope, animate] = useAnimate();
|
||||
const wordsArray = words.split(' ');
|
||||
useEffect(() => {
|
||||
animate(
|
||||
'span',
|
||||
{
|
||||
opacity: 1,
|
||||
filter: filter ? 'blur(0px)' : 'none',
|
||||
},
|
||||
{
|
||||
duration: duration ? duration : 1,
|
||||
delay: stagger(0.2),
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scope.current]);
|
||||
|
||||
const renderWords = () => {
|
||||
return (
|
||||
<motion.div ref={scope}>
|
||||
{wordsArray.map((word, idx) => {
|
||||
return (
|
||||
<motion.span
|
||||
key={word + idx}
|
||||
className='text-black opacity-0 dark:text-white'
|
||||
style={{
|
||||
filter: filter ? 'blur(10px)' : 'none',
|
||||
}}
|
||||
>
|
||||
{word}{' '}
|
||||
</motion.span>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('font-bold', className)}>
|
||||
<div className='mt-4'>
|
||||
<div className='text-2xl leading-snug tracking-wide text-black dark:text-white'>
|
||||
{renderWords()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface TimelineEntry {
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Timeline = ({ data }: { data: TimelineEntry[] }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
setHeight(rect.height);
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ['start 10%', 'end 50%'],
|
||||
});
|
||||
|
||||
const heightTransform = useTransform(scrollYProgress, [0, 1], [0, height]);
|
||||
const opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);
|
||||
|
||||
return (
|
||||
<div className='w-full font-sans md:px-10' ref={containerRef}>
|
||||
<div className='mx-auto max-w-7xl px-4 py-10 md:px-8 lg:px-10'>
|
||||
{/* <h2 className='mb-4 max-w-4xl text-lg text-black md:text-4xl dark:text-white'>
|
||||
Changelog from my journey
|
||||
</h2>
|
||||
<p className='max-w-sm text-sm text-neutral-700 md:text-base dark:text-neutral-300'>
|
||||
I've been working on Aceternity for the past 2 years. Here's a timeline of my
|
||||
journey.
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
<div ref={ref} className='relative mx-auto max-w-7xl pb-20'>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className='flex justify-start pt-10 md:gap-10 md:pt-40'>
|
||||
<div className='sticky top-40 z-40 flex max-w-xs flex-col items-center self-start whitespace-nowrap md:w-auto md:flex-row lg:max-w-sm'>
|
||||
<div className='absolute left-3 flex h-10 w-10 items-center justify-center rounded-full bg-white md:left-3 dark:bg-black'>
|
||||
<div className='h-4 w-4 rounded-full border border-neutral-300 bg-neutral-200 p-2 dark:border-neutral-700 dark:bg-neutral-800' />
|
||||
</div>
|
||||
<h3 className='hidden text-xl font-bold text-neutral-500 md:block md:pl-20 md:text-xl dark:text-neutral-500'>
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className='relative w-full pl-20 pr-4 md:pl-4'>
|
||||
<h3 className='mb-4 block text-left text-2xl font-bold text-neutral-500 md:hidden dark:text-neutral-500'>
|
||||
{item.title}
|
||||
</h3>
|
||||
{item.content}{' '}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
height: height + 'px',
|
||||
}}
|
||||
className='absolute left-8 top-0 w-[2px] overflow-hidden bg-[linear-gradient(to_bottom,var(--tw-gradient-stops))] from-transparent from-[0%] via-neutral-200 to-transparent to-[99%] [mask-image:linear-gradient(to_bottom,transparent_0%,black_10%,black_90%,transparent_100%)] md:left-8 dark:via-neutral-700'
|
||||
>
|
||||
<motion.div
|
||||
style={{
|
||||
height: heightTransform,
|
||||
opacity: opacityTransform,
|
||||
}}
|
||||
className='absolute inset-x-0 top-0 w-[2px] rounded-full bg-gradient-to-t from-purple-500 from-[0%] via-blue-500 via-[10%] to-transparent'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import * as ToastPrimitives from '@radix-ui/react-toast';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-1 disabled:pointer-events-none disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-foreground/50 hover:text-foreground absolute right-1 top-1 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
className,
|
||||
)}
|
||||
toast-close=''
|
||||
{...props}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
Toast,
|
||||
ToastAction,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
type ToastActionElement,
|
||||
type ToastProps,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from '@workspace/airo-ui/components/toast';
|
||||
import { useToast } from '@workspace/airo-ui/hooks/use-toast';
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className='grid gap-1'>
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
|
||||
import { type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { toggleVariants } from '@workspace/airo-ui/components/toggle';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('flex items-center justify-center gap-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const toggleVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
outline:
|
||||
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-2 min-w-9',
|
||||
sm: 'h-8 px-1.5 min-w-8',
|
||||
lg: 'h-10 px-2.5 min-w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 overflow-hidden rounded-md px-3 py-1.5 text-xs',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@workspace/airo-ui/components/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover';
|
||||
import { Icon } from '@workspace/airo-ui/custom-components/icon';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { countries, type ICountry } from '@workspace/airo-ui/utils/countries';
|
||||
import { BoxIcon, Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface AreaCodeSelectProps {
|
||||
value?: string;
|
||||
onChange?: (value: ICountry) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
simple?: boolean;
|
||||
whitelist?: string[];
|
||||
}
|
||||
|
||||
const filterItems = (whitelist?: string[]) => {
|
||||
const baseItems = countries
|
||||
.filter((item) => !!item.phone)
|
||||
.map((item) => {
|
||||
const phones = item.phone!.split(',');
|
||||
if (phones.length > 1) {
|
||||
return [...phones].map((phone) => ({
|
||||
...item,
|
||||
phone,
|
||||
}));
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.flat();
|
||||
|
||||
if (!whitelist?.length) return baseItems;
|
||||
return baseItems.filter((item) => whitelist.includes(item.phone!));
|
||||
};
|
||||
|
||||
export const AreaCodeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
placeholder = 'Select Area Code',
|
||||
simple = false,
|
||||
whitelist,
|
||||
}: AreaCodeSelectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<ICountry | undefined>();
|
||||
const items = filterItems(whitelist);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== selectedItem?.phone) {
|
||||
const found = items.find((item) => item.phone === value);
|
||||
setSelectedItem(found);
|
||||
}
|
||||
}, [selectedItem?.phone, value, items]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className={cn('justify-between', className)}
|
||||
>
|
||||
{selectedItem ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon icon={`flagpack:${selectedItem.alpha2.toLowerCase()}`} className='!size-5' />+
|
||||
{selectedItem.phone}
|
||||
{!simple && `(${selectedItem.name})`}
|
||||
</div>
|
||||
) : (
|
||||
placeholder
|
||||
)}
|
||||
<ChevronsUpDown className='ml-2 h-4 w-4 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search area code...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<BoxIcon className='inline-block text-slate-500' />
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={`${item.alpha2}-${item.phone}`}
|
||||
value={`${item.phone}-${item.name}`}
|
||||
onSelect={() => {
|
||||
setSelectedItem(item);
|
||||
onChange?.(item);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon icon={`flagpack:${item.alpha2.toLowerCase()}`} className='!size-5' />+
|
||||
{item.phone} ({item.name})
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
selectedItem?.phone === item.phone ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@workspace/airo-ui/components/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { BoxIcon, CheckIcon, ChevronsUpDownIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export type Option<T = string> = {
|
||||
value: T;
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
// Conditional types to determine the value type for onChange
|
||||
type OnChangeType<T, M extends boolean> = M extends true ? T[] : T;
|
||||
|
||||
type ComboboxProps<T = string, M extends boolean = false> = {
|
||||
multiple?: M;
|
||||
options?: Option<T>[];
|
||||
placeholder?: string;
|
||||
value?: OnChangeType<T, M>;
|
||||
onChange: (value: OnChangeType<T, M>) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Combobox<T, M extends boolean = false>({
|
||||
multiple = false as M,
|
||||
options = [],
|
||||
placeholder = 'Select...',
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: ComboboxProps<T, M>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleSelect = (selectedValue: T) => {
|
||||
if (multiple) {
|
||||
const newValue = Array.isArray(value) ? [...value] : [];
|
||||
|
||||
if (newValue.includes(selectedValue)) {
|
||||
newValue.splice(newValue.indexOf(selectedValue), 1);
|
||||
onChange(newValue as OnChangeType<T, M>);
|
||||
} else {
|
||||
onChange([...newValue, selectedValue] as OnChangeType<T, M>);
|
||||
}
|
||||
} else {
|
||||
const newValue = selectedValue === value ? ('' as T) : selectedValue;
|
||||
onChange(newValue as OnChangeType<T, M>);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonLabel = () => {
|
||||
if (multiple && Array.isArray(value) && value.length > 0) {
|
||||
const selectedLabels = options
|
||||
.filter((option) => value.includes(option.value))
|
||||
.map((option) => option.label)
|
||||
.join(', ');
|
||||
|
||||
return selectedLabels;
|
||||
} else if (!multiple) {
|
||||
const selectedOption = options.find((option) => option.value === value);
|
||||
|
||||
return selectedOption ? selectedOption.children || selectedOption.label : placeholder;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'w-full items-center justify-between rounded-full bg-[#EAEAEA] text-[#848484]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className='truncate'>{renderButtonLabel()}</span>
|
||||
<ChevronsUpDownIcon className='ml-2 size-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-fit p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search...' className='h-9' />
|
||||
<CommandEmpty>
|
||||
<BoxIcon className='inline-block text-slate-500' />
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandList>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={String(option.label + option.value)}
|
||||
value={option.label + option.value}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
>
|
||||
{option.children || option.label}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
multiple
|
||||
? Array.isArray(value) && value.includes(option.value)
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
: value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@workspace/airo-ui/components/alert-dialog';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
interface ConfirmationButtonProps {
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
export const ConfirmButton: React.FC<ConfirmationButtonProps> = ({
|
||||
trigger,
|
||||
title,
|
||||
description,
|
||||
onConfirm,
|
||||
cancelText = 'Cancel',
|
||||
confirmText = 'Confirm',
|
||||
}) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>{confirmText}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { Calendar, CalendarProps } from '@workspace/airo-ui/components/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { intlFormat } from 'date-fns';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export function DatePicker({
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}: CalendarProps & {
|
||||
placeholder?: string;
|
||||
value?: number;
|
||||
onChange?: (value?: number) => void;
|
||||
}) {
|
||||
const [date, setDate] = React.useState<Date | undefined>(value ? new Date(value) : undefined);
|
||||
|
||||
const handleSelect = (selectedDate: Date | undefined) => {
|
||||
setDate(selectedDate);
|
||||
if (onChange) {
|
||||
onChange(selectedDate?.getTime() || 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
className={cn('w-full justify-between font-normal', !value && 'text-muted-foreground')}
|
||||
>
|
||||
{value ? intlFormat(value) : <span>{placeholder}</span>}
|
||||
<CalendarIcon className='size-4' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0' align='start'>
|
||||
<Calendar {...props} mode='single' selected={date} onSelect={handleSelect} initialFocus />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { Label } from '@workspace/airo-ui/components/label';
|
||||
import { Switch } from '@workspace/airo-ui/components/switch';
|
||||
import { Combobox } from '@workspace/airo-ui/custom-components/combobox';
|
||||
import {
|
||||
EnhancedInput,
|
||||
EnhancedInputProps,
|
||||
} from '@workspace/airo-ui/custom-components/enhanced-input';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { CircleMinusIcon, CirclePlusIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface FieldConfig extends Omit<EnhancedInputProps, 'type'> {
|
||||
name: string;
|
||||
type: 'text' | 'number' | 'select' | 'time' | 'boolean';
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
interface ObjectInputProps<T> {
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
fields: FieldConfig[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function ObjectInput<T extends Record<string, any>>({
|
||||
value,
|
||||
onChange,
|
||||
fields,
|
||||
className,
|
||||
}: ObjectInputProps<T>) {
|
||||
const [internalState, setInternalState] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalState(value);
|
||||
}, [value]);
|
||||
|
||||
const updateField = (key: keyof T, fieldValue: string | number | boolean) => {
|
||||
const updatedInternalState = { ...internalState, [key]: fieldValue };
|
||||
setInternalState(updatedInternalState);
|
||||
onChange(updatedInternalState);
|
||||
};
|
||||
const renderField = (field: FieldConfig) => {
|
||||
switch (field.type) {
|
||||
case 'select':
|
||||
return (
|
||||
field.options && (
|
||||
<Combobox<string, false>
|
||||
placeholder={field.placeholder}
|
||||
options={field.options}
|
||||
value={internalState[field.name]}
|
||||
onChange={(fieldValue) => updateField(field.name, fieldValue)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<Switch
|
||||
checked={internalState[field.name] as boolean}
|
||||
onCheckedChange={(fieldValue) => updateField(field.name, fieldValue)}
|
||||
/>
|
||||
{field.placeholder && <Label>{field.placeholder}</Label>}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<EnhancedInput
|
||||
value={internalState[field.name]}
|
||||
onValueChange={(fieldValue) => updateField(field.name, fieldValue)}
|
||||
{...field}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-wrap gap-4', className)}>
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className={cn('flex-1', field.className)}>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface ArrayInputProps<T> {
|
||||
value?: T[];
|
||||
onChange: (value: T[]) => void;
|
||||
fields: FieldConfig[];
|
||||
isReverse?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function ArrayInput<T extends Record<string, any>>({
|
||||
value = [],
|
||||
onChange,
|
||||
fields,
|
||||
isReverse = false,
|
||||
className,
|
||||
}: ArrayInputProps<T>) {
|
||||
const initializeDefaultItem = (): T =>
|
||||
fields.reduce((acc, field) => {
|
||||
acc[field.name as keyof T] = undefined as T[keyof T];
|
||||
return acc;
|
||||
}, {} as T);
|
||||
|
||||
const [displayItems, setDisplayItems] = useState<T[]>(() => {
|
||||
return value.length > 0 ? value : [initializeDefaultItem()];
|
||||
});
|
||||
|
||||
const isItemModified = (item: T): boolean =>
|
||||
fields.some((field) => {
|
||||
const val = item[field.name];
|
||||
return val !== undefined && val !== null && val !== '';
|
||||
});
|
||||
|
||||
const handleItemChange = (index: number, updatedItem: T) => {
|
||||
const newDisplayItems = [...displayItems];
|
||||
newDisplayItems[index] = updatedItem;
|
||||
setDisplayItems(newDisplayItems);
|
||||
|
||||
const modifiedItems = newDisplayItems.filter(isItemModified);
|
||||
onChange(modifiedItems);
|
||||
};
|
||||
|
||||
const createField = () => {
|
||||
if (isReverse) {
|
||||
setDisplayItems([initializeDefaultItem(), ...displayItems]);
|
||||
} else {
|
||||
setDisplayItems([...displayItems, initializeDefaultItem()]);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteField = (index: number) => {
|
||||
const newDisplayItems = displayItems.filter((_, i) => i !== index);
|
||||
setDisplayItems(newDisplayItems);
|
||||
|
||||
const modifiedItems = newDisplayItems.filter(isItemModified);
|
||||
onChange(modifiedItems);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value.length > 0) {
|
||||
setDisplayItems(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{displayItems.map((item, index) => (
|
||||
<div key={index} className='flex items-center gap-4'>
|
||||
<ObjectInput
|
||||
value={item}
|
||||
onChange={(updatedItem) => handleItemChange(index, updatedItem)}
|
||||
fields={fields}
|
||||
className={className}
|
||||
/>
|
||||
<div className='flex min-w-20 items-center'>
|
||||
{displayItems.length > 1 && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
type='button'
|
||||
className='text-destructive p-0 text-lg'
|
||||
onClick={() => deleteField(index)}
|
||||
>
|
||||
<CircleMinusIcon />
|
||||
</Button>
|
||||
)}
|
||||
{(isReverse ? index === 0 : index === displayItems.length - 1) && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
type='button'
|
||||
className='text-primary p-0 text-lg'
|
||||
onClick={createField}
|
||||
>
|
||||
<CirclePlusIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
MonacoEditorProps,
|
||||
} from '@workspace/airo-ui/custom-components/editor/monaco-editor';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function HTMLEditor(props: MonacoEditorProps) {
|
||||
return (
|
||||
<MonacoEditor
|
||||
title='HTML Editor'
|
||||
description='Support HTML'
|
||||
{...props}
|
||||
language='markdown'
|
||||
render={(value) => <HTMLPreview value={value} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface HTMLPreviewProps {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
function HTMLPreview({ value }: HTMLPreviewProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const iframeDocument = iframeRef.current?.contentDocument;
|
||||
if (iframeDocument) {
|
||||
iframeDocument.open();
|
||||
iframeDocument.write(value || '');
|
||||
iframeDocument.close();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return <iframe ref={iframeRef} title='HTML Preview' className='h-full w-full border-0' />;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { HTMLEditor } from '@workspace/airo-ui/custom-components/editor/html';
|
||||
export { JSONEditor } from '@workspace/airo-ui/custom-components/editor/json';
|
||||
export { MarkdownEditor } from '@workspace/airo-ui/custom-components/editor/markdown';
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
MonacoEditorProps,
|
||||
} from '@workspace/airo-ui/custom-components/editor/monaco-editor';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface JSONEditorProps extends Omit<MonacoEditorProps, 'placeholder' | 'value' | 'onChange'> {
|
||||
schema?: Record<string, unknown>;
|
||||
placeholder?: Record<string, unknown>;
|
||||
value?: Record<string, unknown> | string;
|
||||
onChange?: (value: Record<string, unknown> | string | undefined) => void;
|
||||
}
|
||||
|
||||
export function JSONEditor(props: JSONEditorProps) {
|
||||
const { schema, placeholder = {}, ...rest } = props;
|
||||
|
||||
const editorKey = useMemo(() => JSON.stringify({ schema, placeholder }), [schema, placeholder]);
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
key={editorKey}
|
||||
title='Edit JSON'
|
||||
{...rest}
|
||||
value={
|
||||
typeof props.value === 'string'
|
||||
? props.value
|
||||
: props.value
|
||||
? JSON.stringify(props.value, null, 2)
|
||||
: ''
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (props.onChange && typeof value === 'string') {
|
||||
try {
|
||||
props.onChange(
|
||||
props.value && typeof props.value === 'string' ? value : JSON.parse(value),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('Invalid JSON input:', e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder ? JSON.stringify(placeholder, null, 2) : ''}
|
||||
language='json'
|
||||
onMount={(editor, monaco) => {
|
||||
if (props.onMount) props.onMount(editor, monaco);
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: '',
|
||||
fileMatch: ['*'],
|
||||
schema: schema || {
|
||||
type: 'object',
|
||||
properties: generateSchema(placeholder),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const generateSchema = (obj: Record<string, unknown>): Record<string, SchemaProperty> => {
|
||||
const properties: Record<string, SchemaProperty> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
properties[key] = {
|
||||
type: 'array',
|
||||
items: value.length > 0 ? generateSchema({ item: value[0] }).item : { type: 'null' },
|
||||
};
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
properties[key] = {
|
||||
type: 'object',
|
||||
properties: generateSchema(value as Record<string, unknown>),
|
||||
};
|
||||
} else {
|
||||
properties[key] = { type: typeof value as SchemaType };
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
type SchemaType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
|
||||
interface SchemaProperty {
|
||||
type: SchemaType;
|
||||
items?: SchemaProperty;
|
||||
properties?: Record<string, SchemaProperty>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
MonacoEditor,
|
||||
MonacoEditorProps,
|
||||
} from '@workspace/airo-ui/custom-components/editor/monaco-editor';
|
||||
import { Markdown } from '@workspace/airo-ui/custom-components/markdown';
|
||||
|
||||
export function MarkdownEditor(props: MonacoEditorProps) {
|
||||
return (
|
||||
<MonacoEditor
|
||||
title='Markdown Editor'
|
||||
description='Support markdwon and html syntax'
|
||||
{...props}
|
||||
language='markdown'
|
||||
render={(value) => <Markdown>{value || ''}</Markdown>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
'use client';
|
||||
|
||||
import { Editor, type Monaco, type OnMount } from '@monaco-editor/react';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { useSize } from 'ahooks';
|
||||
import { EyeIcon, EyeOff, FullscreenIcon, MinimizeIcon } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface MonacoEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string | undefined) => void;
|
||||
onBlur?: (value: string | undefined) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
render?: (value?: string) => React.ReactNode;
|
||||
onMount?: OnMount;
|
||||
language?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function debounce<T extends (...args: any[]) => void>(func: T, delay: number) {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function (...args: Parameters<T>) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
export function MonacoEditor({
|
||||
value: propValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
title = 'Editor Title',
|
||||
description,
|
||||
placeholder = 'Start typing...',
|
||||
render,
|
||||
onMount,
|
||||
language = 'markdown',
|
||||
className,
|
||||
}: MonacoEditorProps) {
|
||||
const [internalValue, setInternalValue] = useState<string | undefined>(propValue);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isPreviewVisible, setIsPreviewVisible] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(ref);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
const debouncedOnChange = useRef(
|
||||
debounce((newValue: string | undefined) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
}
|
||||
}, 300),
|
||||
).current;
|
||||
|
||||
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
||||
if (onMount) onMount(editor, monaco);
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const newValue = editor.getValue();
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
});
|
||||
|
||||
editor.onDidBlurEditorWidget(() => {
|
||||
if (onBlur) {
|
||||
onBlur(editor.getValue());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => setIsFullscreen(!isFullscreen);
|
||||
const togglePreview = () => setIsPreviewVisible(!isPreviewVisible);
|
||||
|
||||
return (
|
||||
<div ref={ref} className='size-full'>
|
||||
<div style={size}>
|
||||
<div
|
||||
className={cn('flex size-full min-h-96 flex-col rounded-md border', className, {
|
||||
'bg-background fixed inset-0 z-50 !mt-0 h-screen': isFullscreen,
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center justify-between border-b p-2'>
|
||||
<div>
|
||||
<h1 className='text-left text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='text-muted-foreground text-[0.8rem]'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-2'>
|
||||
{render && (
|
||||
<Button variant='outline' size='icon' type='button' onClick={togglePreview}>
|
||||
{isPreviewVisible ? <EyeOff /> : <EyeIcon />}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant='outline' size='icon' type='button' onClick={toggleFullscreen}>
|
||||
{isFullscreen ? <MinimizeIcon /> : <FullscreenIcon />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn('relative flex flex-1 overflow-hidden')}>
|
||||
<div
|
||||
className={cn('flex-1 overflow-hidden p-4 invert dark:invert-0', {
|
||||
'w-1/2': isPreviewVisible,
|
||||
})}
|
||||
>
|
||||
<Editor
|
||||
language={language}
|
||||
value={internalValue}
|
||||
onChange={(newValue) => {
|
||||
setInternalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
className=''
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
contextmenu: false,
|
||||
folding: false,
|
||||
fontSize: 14,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'off',
|
||||
minimap: { enabled: false },
|
||||
overviewRulerLanes: 0,
|
||||
renderLineHighlight: 'none',
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
useShadows: false,
|
||||
vertical: 'hidden',
|
||||
},
|
||||
tabSize: 2,
|
||||
wordWrap: 'off',
|
||||
}}
|
||||
theme='transparentTheme'
|
||||
beforeMount={(monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('transparentTheme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!internalValue?.trim() && placeholder && (
|
||||
<pre
|
||||
className='text-muted-foreground pointer-events-none absolute left-7 top-4 text-sm'
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{placeholder}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
{render && isPreviewVisible && (
|
||||
<div className='w-1/2 flex-1 overflow-auto border-l p-4'>{render(internalValue)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export default function Empty({ description }: { description?: React.ReactNode }) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center p-6 py-16'>
|
||||
<svg
|
||||
width='64'
|
||||
height='41'
|
||||
viewBox='0 0 64 41'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='currentColor'
|
||||
stroke='currentColor'
|
||||
className='text-background'
|
||||
>
|
||||
<g transform='translate(0 1)' fill='none' fillRule='evenodd'>
|
||||
<ellipse cx='32' cy='33' rx='32' ry='7' fill='currentColor' opacity={0.8}></ellipse>
|
||||
<g fillRule='nonzero' stroke='#d9d9d9'>
|
||||
<path d='M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z'></path>
|
||||
<path
|
||||
d='M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z'
|
||||
fill='currentColor'
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<p className='mt-6 text-center text-gray-500'>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
export interface EnhancedInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
prefix?: string | ReactNode;
|
||||
suffix?: string | ReactNode;
|
||||
formatInput?: (value: string | number) => string | number;
|
||||
formatOutput?: (value: string | number) => string | number;
|
||||
onValueChange?: (value: string | number) => void;
|
||||
onValueBlur?: (value: string | number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function EnhancedInput({
|
||||
suffix,
|
||||
prefix,
|
||||
formatInput,
|
||||
formatOutput,
|
||||
value: initialValue,
|
||||
className,
|
||||
onValueChange,
|
||||
onValueBlur,
|
||||
...props
|
||||
}: EnhancedInputProps) {
|
||||
const getProcessedValue = (inputValue: unknown) => {
|
||||
if (inputValue === '' || inputValue === 0 || inputValue === '0') return '';
|
||||
const newValue = String(inputValue ?? '');
|
||||
return formatInput ? formatInput(newValue) : newValue;
|
||||
};
|
||||
|
||||
const [value, setValue] = useState<string | number>(() => getProcessedValue(initialValue));
|
||||
// @ts-expect-error - This is a controlled component
|
||||
const [internalValue, setInternalValue] = useState<string | number>(initialValue ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue !== internalValue) {
|
||||
const newValue = getProcessedValue(initialValue);
|
||||
if (value !== newValue) {
|
||||
setValue(newValue);
|
||||
// @ts-expect-error - This is a controlled component
|
||||
setInternalValue(initialValue ?? '');
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialValue, formatInput]);
|
||||
|
||||
const processValue = (inputValue: string | number) => {
|
||||
let processedValue: number | string = inputValue?.toString().trim();
|
||||
|
||||
if (processedValue === '0' && props.type === 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (processedValue && props.type === 'number') processedValue = Number(processedValue);
|
||||
return formatOutput ? formatOutput(processedValue) : processedValue;
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
let inputValue = e.target.value;
|
||||
|
||||
if (props.type === 'number') {
|
||||
if (inputValue === '0') {
|
||||
setValue('');
|
||||
setInternalValue(0);
|
||||
onValueChange?.(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^-?\d*\.?\d*$/.test(inputValue) || inputValue === '-' || inputValue === '.') {
|
||||
const numericValue = Number(inputValue);
|
||||
if (!isNaN(numericValue) && inputValue !== '-' && inputValue !== '.') {
|
||||
const min = Number.isFinite(props.min) ? props.min : -Infinity;
|
||||
const max = Number.isFinite(props.max) ? props.max : Infinity;
|
||||
const constrainedValue = Math.max(min!, Math.min(max!, numericValue));
|
||||
inputValue = String(constrainedValue);
|
||||
setInternalValue(constrainedValue);
|
||||
} else {
|
||||
setInternalValue(inputValue);
|
||||
}
|
||||
setValue(inputValue === '0' ? '' : inputValue);
|
||||
}
|
||||
} else {
|
||||
setValue(inputValue);
|
||||
setInternalValue(inputValue);
|
||||
}
|
||||
|
||||
const outputValue = processValue(inputValue);
|
||||
onValueChange?.(outputValue);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (props.type === 'number' && value) {
|
||||
if (value === '-' || value === '.') {
|
||||
setValue('');
|
||||
setInternalValue('');
|
||||
onValueBlur?.('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保0值显示为空
|
||||
if (value === '0') {
|
||||
setValue('');
|
||||
onValueBlur?.(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const outputValue = processValue(value);
|
||||
if ((initialValue || '') !== outputValue) {
|
||||
onValueBlur?.(outputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPrefix = () => {
|
||||
return typeof prefix === 'string' ? (
|
||||
<div className='bg-muted relative mr-px flex h-9 items-center text-nowrap px-3'>{prefix}</div>
|
||||
) : (
|
||||
prefix
|
||||
);
|
||||
};
|
||||
|
||||
const renderSuffix = () => {
|
||||
return typeof suffix === 'string' ? (
|
||||
<div className='bg-muted relative ml-px flex h-9 items-center text-nowrap px-3'>{suffix}</div>
|
||||
) : (
|
||||
suffix
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-input flex w-full items-center overflow-hidden rounded-md border',
|
||||
className,
|
||||
)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{renderPrefix()}
|
||||
<Input
|
||||
step={0.01}
|
||||
{...props}
|
||||
value={value}
|
||||
className='block rounded-none border-none'
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{renderSuffix()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
// import { icons as FlagPack } from '@iconify-json/flagpack';
|
||||
// import { icons as Logos } from '@iconify-json/logos';
|
||||
// import { icons as Mdi } from '@iconify-json/mdi';
|
||||
// import { icons as Simple } from '@iconify-json/simple-icons';
|
||||
// import { icons as Uil } from '@iconify-json/uil';
|
||||
|
||||
import { Icon as Iconify, IconProps } from '@iconify/react';
|
||||
|
||||
// addCollection(FlagPack);
|
||||
// addCollection(Mdi);
|
||||
// addCollection(Uil);
|
||||
// addCollection(Simple);
|
||||
// addCollection(Logos);
|
||||
|
||||
export function Icon(props: IconProps) {
|
||||
return <Iconify {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import ReactMarkdown, { Components } from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkToc from 'remark-toc';
|
||||
|
||||
interface CodeBlockProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function CodeBlock({ className, children, ...props }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const match = className?.startsWith('language-') ? /language-(\w+)/.exec(className) : null;
|
||||
|
||||
const handleCopy = useCallback((text: string) => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Failed to copy text. Please try again.');
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (match) {
|
||||
return (
|
||||
<div className='group relative my-4 w-full overflow-hidden rounded-lg'>
|
||||
<div className='bg-muted flex items-center justify-between gap-4 px-4 py-2 text-sm font-semibold'>
|
||||
<span className='lowercase [&>span]:text-xs'>{match[1]}</span>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => handleCopy(String(children).replace(/\n$/, ''))}
|
||||
className='absolute right-2 top-0 z-20 p-0.5 opacity-0 transition-opacity duration-200 group-hover:opacity-100'
|
||||
>
|
||||
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
PreTag='div'
|
||||
language={match[1]}
|
||||
style={oneDark}
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code {...props} className={cn(className, 'rounded border font-semibold')}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
components?: Components;
|
||||
}
|
||||
|
||||
export function Markdown({ children, components }: MarkdownProps) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className='prose dark:prose-invert w-full max-w-[unset] break-words'
|
||||
remarkPlugins={[remarkGfm, remarkToc, remarkMath]}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h1: ({ node, className, ...props }) => (
|
||||
<h1
|
||||
className={cn(
|
||||
'mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h2: ({ node, className, ...props }) => (
|
||||
<h2
|
||||
className={cn(
|
||||
'mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h3: ({ node, className, ...props }) => (
|
||||
<h3
|
||||
className={cn(
|
||||
'mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h4: ({ node, className, ...props }) => (
|
||||
<h4
|
||||
className={cn(
|
||||
'mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h5: ({ node, className, ...props }) => (
|
||||
<h5
|
||||
className={cn('my-4 text-lg font-semibold first:mt-0 last:mb-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
h6: ({ node, className, ...props }) => (
|
||||
<h6 className={cn('my-4 font-semibold first:mt-0 last:mb-0', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node, className, ...props }) => (
|
||||
<p className={cn('mb-5 mt-5 leading-7 first:mt-0 last:mb-0', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
a: ({ node, className, ...props }) => (
|
||||
<a
|
||||
target='_blank'
|
||||
className={cn('text-primary font-medium underline underline-offset-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
blockquote: ({ node, className, ...props }) => (
|
||||
<blockquote className={cn('border-l-2 pl-6 italic', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ul: ({ node, className, ...props }) => (
|
||||
<ul className={cn('my-5 ml-6 list-disc [&>li]:mt-2', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ol: ({ node, className, ...props }) => (
|
||||
<ol className={cn('my-5 ml-6 list-decimal [&>li]:mt-2', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
hr: ({ node, className, ...props }) => (
|
||||
<hr className={cn('my-5 border-b', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
table: ({ node, className, ...props }) => (
|
||||
<table
|
||||
className={cn(
|
||||
'my-5 w-full border-separate border-spacing-0 overflow-y-auto',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
th: ({ node, className, ...props }) => (
|
||||
<th
|
||||
className={cn(
|
||||
'bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
td: ({ node, className, ...props }) => (
|
||||
<td
|
||||
className={cn(
|
||||
'border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
tr: ({ node, className, ...props }) => (
|
||||
<tr
|
||||
className={cn(
|
||||
'm-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
sup: ({ node, className, ...props }) => (
|
||||
<sup className={cn('[&>a]:text-xs [&>a]:no-underline', className)} {...props} />
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
pre: ({ node, className, ...props }) => (
|
||||
<pre className={cn('overflow-x-auto rounded-b-lg p-0', className)} {...props} />
|
||||
),
|
||||
code(props) {
|
||||
return <CodeBlock {...(props as CodeBlockProps)} />;
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { Combobox } from '@workspace/airo-ui/custom-components/combobox';
|
||||
|
||||
export interface IParams {
|
||||
key: string;
|
||||
placeholder?: string;
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
interface ColumnFilterProps<TData> {
|
||||
table: Table<TData>;
|
||||
params: IParams[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
filters?: any;
|
||||
}
|
||||
|
||||
export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProps<TData>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const updateFilter = (key: string, value: any) => {
|
||||
table.setColumnFilters((prev) => {
|
||||
const newFilters = prev.filter((filter) => filter.id !== key);
|
||||
if (value) {
|
||||
newFilters.push({ id: key, value });
|
||||
}
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
{params.map((param) => {
|
||||
if (param.options) {
|
||||
return (
|
||||
<Combobox
|
||||
key={param.key}
|
||||
className='w-32'
|
||||
placeholder={param.placeholder || 'Choose...'}
|
||||
value={filters[param.key] || ''}
|
||||
onChange={(value) => {
|
||||
updateFilter(param.key, value);
|
||||
}}
|
||||
options={param.options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
key={param.key}
|
||||
className='w-64 rounded-[20px] text-xl shadow-[inset_0_0_7.6px_0_#00000040]'
|
||||
placeholder={param.placeholder || 'Search...'}
|
||||
value={filters[param.key] || ''}
|
||||
onChange={(event) => updateFilter(param.key, event.target.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
ProList,
|
||||
type ProListActions,
|
||||
type ProListProps,
|
||||
} from '@workspace/airo-ui/custom-components/pro-list/pro-list';
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/airo-ui/components/select';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PaginationText {
|
||||
textRowsPerPage?: string;
|
||||
textPageOf?: (pageIndex: number, pageCount: number) => string;
|
||||
}
|
||||
|
||||
interface PaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
text?: PaginationText;
|
||||
}
|
||||
|
||||
export function Pagination<TData>({ table, text }: PaginationProps<TData>) {
|
||||
return (
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<div className='text-muted-foreground flex-1 whitespace-nowrap text-center sm:text-left'>
|
||||
{text?.textPageOf?.(table.getState().pagination.pageIndex + 1, table.getPageCount()) ||
|
||||
`Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`}
|
||||
</div>
|
||||
<div className='flex flex-grow items-center justify-center gap-2 sm:justify-end'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<p className='font-medium'>{text?.textRowsPerPage || 'Rows per page'}</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[70px]'>
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side='top'>
|
||||
{[10, 20, 50, 100, 200].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='hidden lg:flex'
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to first page</span>
|
||||
<ChevronsLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to previous page</span>
|
||||
<ChevronLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageIndex + 1}`}
|
||||
onValueChange={(value) => table.setPageIndex(Number(value) - 1)}
|
||||
>
|
||||
<SelectTrigger className='w-[70px]'>
|
||||
<SelectValue placeholder='Select page number' />
|
||||
</SelectTrigger>
|
||||
<SelectContent className='w-12'>
|
||||
{Array.from({ length: table.getPageCount() }, (_, i) => (
|
||||
<SelectItem key={i} value={`${i + 1}`}>
|
||||
{i + 1}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to next page</span>
|
||||
<ChevronRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='hidden lg:flex'
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to last page</span>
|
||||
<ChevronsRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnFiltersState,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@workspace/airo-ui/components/alert';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { Checkbox } from '@workspace/airo-ui/components/checkbox';
|
||||
import Empty from '@workspace/airo-ui/custom-components/empty';
|
||||
import { ColumnFilter, IParams } from '@workspace/airo-ui/custom-components/pro-list/column-filter';
|
||||
import { Pagination } from '@workspace/airo-ui/custom-components/pro-list/pagination';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { Loader, RefreshCcw } from 'lucide-react';
|
||||
import React, { useEffect, useImperativeHandle, useState } from 'react';
|
||||
|
||||
export interface ProListProps<TData, TValue> {
|
||||
request: (
|
||||
pagination: {
|
||||
page: number;
|
||||
size: number;
|
||||
},
|
||||
filter: TValue,
|
||||
) => Promise<{ list: TData[]; total: number }>;
|
||||
params?: IParams[];
|
||||
header?: {
|
||||
title?: React.ReactNode;
|
||||
toolbar?: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
batchRender?: (rows: TData[]) => React.ReactNode[];
|
||||
renderItem: (item: TData, checkbox: React.ReactNode) => React.ReactNode;
|
||||
action?: React.Ref<ProListActions | undefined>;
|
||||
texts?: Partial<{
|
||||
textRowsPerPage: string;
|
||||
textPageOf: (current: number, total: number) => string;
|
||||
selectedRowsText: (total: number) => string;
|
||||
}>;
|
||||
empty?: React.ReactNode;
|
||||
}
|
||||
export interface ProListActions {
|
||||
refresh: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function ProList<TData, TValue extends Record<string, unknown>>({
|
||||
request,
|
||||
params,
|
||||
header,
|
||||
batchRender,
|
||||
renderItem,
|
||||
action,
|
||||
texts,
|
||||
empty,
|
||||
}: ProListProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<{ [key: number]: boolean }>({});
|
||||
const [data, setData] = useState<TData[]>([]);
|
||||
const [rowCount, setRowCount] = useState<number>(0);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: [],
|
||||
onPaginationChange: setPagination,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
columnFilters,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFiltering: true,
|
||||
rowCount: rowCount,
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await request(
|
||||
{
|
||||
page: pagination.pageIndex + 1,
|
||||
size: pagination.pageSize,
|
||||
},
|
||||
Object.fromEntries(columnFilters.map((item) => [item.id, item.value])) as TValue,
|
||||
);
|
||||
setData(response.list);
|
||||
setRowCount(response.total);
|
||||
} catch (error) {
|
||||
console.log('Fetch data error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reset = async () => {
|
||||
table.resetColumnFilters();
|
||||
table.resetGlobalFilter(true);
|
||||
table.resetColumnVisibility();
|
||||
setRowSelection({});
|
||||
table.resetPagination();
|
||||
};
|
||||
|
||||
useImperativeHandle(action, () => ({
|
||||
refresh: fetchData,
|
||||
reset,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pagination.pageIndex, pagination.pageSize, columnFilters]);
|
||||
|
||||
const handleSelectionChange = (index: number, isSelected: boolean) => {
|
||||
setRowSelection((prevSelection) => ({
|
||||
...prevSelection,
|
||||
[index]: isSelected,
|
||||
}));
|
||||
};
|
||||
|
||||
const selectedRows = data.filter((_, index) => rowSelection[index]);
|
||||
const selectedCount = selectedRows.length;
|
||||
|
||||
return (
|
||||
<div className='flex max-w-full flex-col gap-4'>
|
||||
<div className='flex flex-wrap-reverse items-center justify-between gap-4'>
|
||||
<div>
|
||||
{params ? (
|
||||
<ColumnFilter
|
||||
table={table}
|
||||
params={params}
|
||||
filters={Object.fromEntries(columnFilters.map((item) => [item.id, item.value]))}
|
||||
/>
|
||||
) : (
|
||||
header?.title
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-1 items-center justify-end gap-2'>
|
||||
{params && params?.length > 0 && (
|
||||
<>
|
||||
<Button variant='outline' className='h-8 w-8 bg-[#D9D9D9] p-2' onClick={fetchData}>
|
||||
<RefreshCcw className='h-4 w-4 text-[#848484]' />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{header?.toolbar}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedCount > 0 && batchRender && (
|
||||
<Alert className='flex items-center justify-between'>
|
||||
<AlertTitle className='m-0'>
|
||||
{texts?.selectedRowsText?.(selectedCount) || `Selected ${selectedCount} rows`}
|
||||
</AlertTitle>
|
||||
<AlertDescription className='flex gap-2'>{batchRender(selectedRows)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn('relative overflow-x-auto', {
|
||||
'rounded-xl border': data.length === 0,
|
||||
})}
|
||||
>
|
||||
<div className='grid grid-cols-1 gap-4'>
|
||||
{data.length ? (
|
||||
data.map((item, index) => {
|
||||
const isSelected = !!rowSelection[index];
|
||||
|
||||
const checkbox = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(value) => handleSelectionChange(index, !!value)}
|
||||
aria-label='Select row'
|
||||
/>
|
||||
);
|
||||
|
||||
return <div key={index}>{renderItem(item, checkbox)}</div>;
|
||||
})
|
||||
) : (
|
||||
<div className='flex items-center justify-center py-24'>{empty || <Empty />}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className='bg-muted/80 absolute top-0 z-20 flex h-full w-full items-center justify-center'>
|
||||
<Loader className='h-4 w-4 animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{rowCount > 0 && (
|
||||
<Pagination
|
||||
table={table}
|
||||
text={{
|
||||
textRowsPerPage: texts?.textRowsPerPage,
|
||||
textPageOf: texts?.textPageOf,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { Combobox } from '@workspace/airo-ui/custom-components/combobox';
|
||||
|
||||
export interface IParams {
|
||||
key: string;
|
||||
placeholder?: string;
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
interface ColumnFilterProps<TData> {
|
||||
table: Table<TData>;
|
||||
params: IParams[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
filters?: any;
|
||||
}
|
||||
|
||||
export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProps<TData>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const updateFilter = (key: string, value: any) => {
|
||||
table.setColumnFilters((prev) => {
|
||||
const newFilters = prev.filter((filter) => filter.id !== key);
|
||||
if (value) {
|
||||
newFilters.push({ id: key, value });
|
||||
}
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
{params.map((param) => {
|
||||
if (param.options) {
|
||||
return (
|
||||
<Combobox
|
||||
key={param.key}
|
||||
className='min-w-32'
|
||||
placeholder={param.placeholder || 'Choose...'}
|
||||
value={filters[param.key] || ''}
|
||||
onChange={(value) => {
|
||||
updateFilter(param.key, value);
|
||||
}}
|
||||
options={param.options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
key={param.key}
|
||||
className='min-w-32'
|
||||
placeholder={param.placeholder || 'Search...'}
|
||||
value={filters[param.key] || ''}
|
||||
onChange={(event) => updateFilter(param.key, event.target.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { flexRender, Header } from '@tanstack/react-table';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@workspace/airo-ui/components/dropdown-menu';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { ArrowDownIcon, ArrowDownUpIcon, ArrowUpIcon, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
interface ColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
header: Header<TData, TValue>;
|
||||
text?: Partial<{
|
||||
asc: string;
|
||||
desc: string;
|
||||
hide: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ColumnHeader<TData, TValue>({
|
||||
header,
|
||||
className,
|
||||
text,
|
||||
}: ColumnHeaderProps<TData, TValue>) {
|
||||
const column = header.column;
|
||||
const title = header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext());
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn(className)}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full items-center', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='flex h-8 w-full justify-start !bg-transparent p-0 text-sm'
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === 'desc' ? (
|
||||
<ArrowDownIcon className='ml-2 h-4 w-4' />
|
||||
) : column.getIsSorted() === 'asc' ? (
|
||||
<ArrowUpIcon className='ml-2 h-4 w-4' />
|
||||
) : (
|
||||
<ArrowDownUpIcon className='ml-2 h-4 w-4' />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start'>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUpIcon className='text-muted-foreground/70 mr-2 h-3.5 w-3.5' />
|
||||
{text?.asc || 'ASC'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDownIcon className='text-muted-foreground/70 mr-2 h-3.5 w-3.5' />
|
||||
{text?.desc || 'DESC'}
|
||||
</DropdownMenuItem>
|
||||
{column.getCanHide() && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeOffIcon className='text-muted-foreground/70 mr-2 h-3.5 w-3.5' />
|
||||
{text?.hide || 'Hide'}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@workspace/airo-ui/components/dropdown-menu';
|
||||
import { ListTodoIcon } from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ColumnToggleProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function ColumnToggle<TData>({ table }: ColumnToggleProps<TData>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline' size='icon'>
|
||||
<ListTodoIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[150px]'>
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide())
|
||||
.map((column) => {
|
||||
const columns = table.getAllColumns().filter((item) => item.getIsVisible());
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className='capitalize'
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
disabled={columns.length === 1 && columns?.[0]?.id === column.id}
|
||||
>
|
||||
{column.columnDef.header as ReactNode}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
ProTable,
|
||||
type ProTableActions,
|
||||
type ProTableProps,
|
||||
} from '@workspace/airo-ui/custom-components/pro-table/pro-table';
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/airo-ui/components/select';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PaginationText {
|
||||
textRowsPerPage?: string;
|
||||
textPageOf?: (pageIndex: number, pageCount: number) => string;
|
||||
}
|
||||
|
||||
interface PaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
text?: PaginationText;
|
||||
}
|
||||
|
||||
export function Pagination<TData>({ table, text }: PaginationProps<TData>) {
|
||||
return (
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<div className='text-muted-foreground flex-1 whitespace-nowrap text-center sm:text-left'>
|
||||
{text?.textPageOf?.(table.getState().pagination.pageIndex + 1, table.getPageCount()) ||
|
||||
`Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`}
|
||||
</div>
|
||||
<div className='flex flex-grow items-center justify-center gap-2 sm:justify-end'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<p className='font-medium'>{text?.textRowsPerPage || 'Rows per page'}</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='h-8 w-[70px]'>
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side='top'>
|
||||
{[10, 20, 50, 100, 200].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='hidden lg:flex'
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to first page</span>
|
||||
<ChevronsLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to previous page</span>
|
||||
<ChevronLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageIndex + 1}`}
|
||||
onValueChange={(value) => table.setPageIndex(Number(value) - 1)}
|
||||
>
|
||||
<SelectTrigger className='w-[70px]'>
|
||||
<SelectValue placeholder='Select page number' />
|
||||
</SelectTrigger>
|
||||
<SelectContent className='w-12'>
|
||||
{Array.from({ length: table.getPageCount() }, (_, i) => (
|
||||
<SelectItem key={i} value={`${i + 1}`}>
|
||||
{i + 1}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to next page</span>
|
||||
<ChevronRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
className='hidden lg:flex'
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to last page</span>
|
||||
<ChevronsRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@workspace/airo-ui/components/alert';
|
||||
import { Button } from '@workspace/airo-ui/components/button';
|
||||
import { Checkbox } from '@workspace/airo-ui/components/checkbox';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@workspace/airo-ui/components/table';
|
||||
import Empty from '@workspace/airo-ui/custom-components/empty';
|
||||
import {
|
||||
ColumnFilter,
|
||||
IParams,
|
||||
} from '@workspace/airo-ui/custom-components/pro-table/column-filter';
|
||||
import { ColumnHeader } from '@workspace/airo-ui/custom-components/pro-table/column-header';
|
||||
import { ColumnToggle } from '@workspace/airo-ui/custom-components/pro-table/column-toggle';
|
||||
import { Pagination } from '@workspace/airo-ui/custom-components/pro-table/pagination';
|
||||
import { SortableRow } from '@workspace/airo-ui/custom-components/pro-table/sortable-row';
|
||||
import { ProTableWrapper } from '@workspace/airo-ui/custom-components/pro-table/wrapper';
|
||||
import { useSize } from 'ahooks';
|
||||
import { GripVertical, ListRestart, Loader, RefreshCcw } from 'lucide-react';
|
||||
import React, { Fragment, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
export interface ProTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
request: (
|
||||
pagination: {
|
||||
page: number;
|
||||
size: number;
|
||||
},
|
||||
filter: TValue,
|
||||
) => Promise<{ list: TData[]; total: number }>;
|
||||
params?: IParams[];
|
||||
header?: {
|
||||
title?: React.ReactNode;
|
||||
toolbar?: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
actions?: {
|
||||
render?: (row: TData) => React.ReactNode[];
|
||||
batchRender?: (rows: TData[]) => React.ReactNode[];
|
||||
};
|
||||
action?: React.Ref<ProTableActions | undefined>;
|
||||
texts?: Partial<{
|
||||
actions: string;
|
||||
asc: string;
|
||||
desc: string;
|
||||
hide: string;
|
||||
textRowsPerPage: string;
|
||||
textPageOf: (current: number, total: number) => string;
|
||||
selectedRowsText: (total: number) => string;
|
||||
}>;
|
||||
empty?: React.ReactNode;
|
||||
onSort?: (
|
||||
sourceId: string | number,
|
||||
targetId: string | number | null,
|
||||
items: TData[],
|
||||
) => Promise<TData[]>;
|
||||
}
|
||||
|
||||
export interface ProTableActions {
|
||||
refresh: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function ProTable<
|
||||
TData extends Record<string, unknown> & { id?: string },
|
||||
TValue extends Record<string, unknown>,
|
||||
>({
|
||||
columns,
|
||||
request,
|
||||
params,
|
||||
header,
|
||||
actions,
|
||||
action,
|
||||
texts,
|
||||
empty,
|
||||
onSort,
|
||||
}: ProTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [data, setData] = useState<TData[]>([]);
|
||||
const [rowCount, setRowCount] = useState<number>(0);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: [
|
||||
...(onSort
|
||||
? [
|
||||
{
|
||||
id: 'sortable',
|
||||
header: (
|
||||
<GripVertical className='h-4 w-4 cursor-move text-gray-500 hover:text-gray-700' />
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(actions?.batchRender ? [createSelectColumn<TData, TValue>()] : []),
|
||||
...columns.map(
|
||||
(column) =>
|
||||
({
|
||||
enableSorting: false,
|
||||
...column,
|
||||
}) as ColumnDef<TData, TValue>,
|
||||
),
|
||||
...(actions?.render
|
||||
? ([
|
||||
{
|
||||
id: 'actions',
|
||||
header: texts?.actions,
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center justify-end gap-2'>
|
||||
{actions?.render?.(row.original).map((item, index) => (
|
||||
<Fragment key={index}>{item}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
] as ColumnDef<TData, TValue>[])
|
||||
: []),
|
||||
] as ColumnDef<TData, TValue>[],
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFiltering: true,
|
||||
rowCount: rowCount,
|
||||
manualSorting: true,
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await request(
|
||||
{
|
||||
page: pagination.pageIndex + 1,
|
||||
size: pagination.pageSize,
|
||||
},
|
||||
Object.fromEntries(columnFilters.map((item) => [item.id, item.value])) as TValue,
|
||||
);
|
||||
setData(response.list);
|
||||
setRowCount(response.total);
|
||||
} catch (error) {
|
||||
console.log('Fetch data error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const reset = async () => {
|
||||
table.resetSorting();
|
||||
table.resetColumnFilters();
|
||||
table.resetGlobalFilter(true);
|
||||
table.resetColumnVisibility();
|
||||
table.resetRowSelection();
|
||||
table.resetPagination();
|
||||
};
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(ref);
|
||||
|
||||
useImperativeHandle(action, () => ({
|
||||
refresh: fetchData,
|
||||
reset,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pagination.pageIndex, pagination.pageSize, columnFilters]);
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
|
||||
const selectedCount = selectedRows.length;
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4' ref={ref}>
|
||||
<div className='flex flex-wrap-reverse items-center justify-between gap-4'>
|
||||
<div>
|
||||
{params ? (
|
||||
<ColumnFilter
|
||||
table={table}
|
||||
params={params}
|
||||
filters={Object.fromEntries(columnFilters.map((item) => [item.id, item.value]))}
|
||||
/>
|
||||
) : (
|
||||
header?.title
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-1 items-center justify-end gap-2'>
|
||||
<Button variant='outline' size='icon' onClick={fetchData}>
|
||||
<RefreshCcw />
|
||||
</Button>
|
||||
<ColumnToggle table={table} />
|
||||
<Button variant='outline' size='icon' onClick={reset}>
|
||||
<ListRestart />
|
||||
</Button>
|
||||
{header?.toolbar}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedCount > 0 && actions?.batchRender && (
|
||||
<Alert className='flex items-center justify-between'>
|
||||
<AlertTitle className='m-0'>
|
||||
{texts?.selectedRowsText?.(selectedCount) || `Selected ${selectedCount} rows`}
|
||||
</AlertTitle>
|
||||
<AlertDescription className='flex gap-2'>
|
||||
{actions.batchRender(selectedRows)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div
|
||||
className='relative w-auto overflow-x-auto rounded-md border'
|
||||
style={{
|
||||
width: size?.width,
|
||||
}}
|
||||
>
|
||||
<ProTableWrapper data={data} setData={setData} onSort={onSort}>
|
||||
<Table className='w-full'>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} className={getTableHeaderClass(header.column.id)}>
|
||||
<ColumnHeader
|
||||
header={header}
|
||||
text={{
|
||||
asc: texts?.asc,
|
||||
desc: texts?.desc,
|
||||
hide: texts?.hide,
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel()?.rows?.length ? (
|
||||
onSort ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<SortableRow
|
||||
key={row.original.id ? String(row.original.id) : String(row.index)}
|
||||
id={row.original.id ? String(row.original.id) : String(row.index)}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
isSortable
|
||||
>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.filter((cell) => {
|
||||
return cell.column.id !== 'sortable';
|
||||
})
|
||||
.map((cell) => (
|
||||
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</SortableRow>
|
||||
))
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className={getTableCellClass(cell.column.id)}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length + 2} className='py-24'>
|
||||
{empty || <Empty />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ProTableWrapper>
|
||||
|
||||
{loading && (
|
||||
<div className='bg-muted/80 absolute top-0 z-20 flex h-full w-full items-center justify-center'>
|
||||
<Loader className='h-4 w-4 animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{rowCount > 0 && (
|
||||
<Pagination
|
||||
table={table}
|
||||
text={{
|
||||
textRowsPerPage: texts?.textRowsPerPage,
|
||||
textPageOf: texts?.textPageOf,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createSelectColumn<TData, TValue>(): ColumnDef<TData, TValue> {
|
||||
return {
|
||||
id: 'selected',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label='Select all'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label='Select row'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
};
|
||||
}
|
||||
|
||||
function getTableHeaderClass(columnId: string) {
|
||||
if (['sortable', 'selected'].includes(columnId)) {
|
||||
return 'sticky left-0 z-10 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] [&:has([role=checkbox])]:pr-2';
|
||||
} else if (columnId === 'actions') {
|
||||
return 'sticky right-0 z-10 text-right bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]';
|
||||
}
|
||||
return 'truncate';
|
||||
}
|
||||
|
||||
function getTableCellClass(columnId: string) {
|
||||
if (['sortable', 'selected'].includes(columnId)) {
|
||||
return 'sticky left-0 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]';
|
||||
} else if (columnId === 'actions') {
|
||||
return 'sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]';
|
||||
}
|
||||
return 'truncate';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { TableCell, TableRow } from '@workspace/airo-ui/components/table';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
interface SortableRowProps {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
isSortable: boolean;
|
||||
}
|
||||
|
||||
export function SortableRow({ id, children, isSortable }: SortableRowProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id,
|
||||
disabled: !isSortable,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString({
|
||||
x: 0,
|
||||
y: transform?.y || 0,
|
||||
scaleX: transform?.scaleX || 1,
|
||||
scaleY: transform?.scaleY || 1,
|
||||
}),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style}>
|
||||
{isSortable ? (
|
||||
<TableCell className='cursor-move' {...listeners} {...attributes}>
|
||||
<GripVertical className='h-4 w-4 cursor-move text-gray-500 hover:text-gray-700' />
|
||||
</TableCell>
|
||||
) : null}
|
||||
{children}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
|
||||
export function ProTableWrapper<TData extends { id?: string }>({
|
||||
children,
|
||||
onSort,
|
||||
data,
|
||||
setData,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onSort?: (
|
||||
sourceId: string | number,
|
||||
targetId: string | number | null,
|
||||
items: TData[],
|
||||
) => Promise<TData[]>;
|
||||
data: TData[];
|
||||
setData: React.Dispatch<React.SetStateAction<TData[]>>;
|
||||
}) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (onSort) {
|
||||
const updatedData = await onSort(active.id, over?.id || null, data);
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
if (!onSort) return children;
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext
|
||||
items={data.map((item) => String(item.id))}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{children}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Badge } from '@workspace/airo-ui/components/badge';
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { X } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface TagInputProps {
|
||||
value?: string[];
|
||||
onChange?: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
separator?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TagInput({
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder,
|
||||
separator = ',',
|
||||
className,
|
||||
}: TagInputProps) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [tags, setTags] = useState<string[]>(value);
|
||||
|
||||
useEffect(() => {
|
||||
setTags(value.map((tag) => tag.trim()).filter((tag) => tag));
|
||||
}, [value]);
|
||||
|
||||
function normalizeInput(input: string) {
|
||||
return input.replace(/,/g, ',');
|
||||
}
|
||||
|
||||
function addTag() {
|
||||
const normalizedInput = normalizeInput(inputValue);
|
||||
const newTags = normalizedInput
|
||||
.split(separator)
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag && !tags.includes(tag));
|
||||
|
||||
if (newTags.length > 0) {
|
||||
const updatedTags = [...tags, ...newTags];
|
||||
updateTags(updatedTags);
|
||||
}
|
||||
setInputValue('');
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter' || event.key === separator || event.key === ',') {
|
||||
event.preventDefault();
|
||||
addTag();
|
||||
} else if (event.key === 'Backspace' && inputValue === '') {
|
||||
event.preventDefault();
|
||||
handleRemoveTag(tags.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveTag(index: number) {
|
||||
const newTags = tags.filter((_, i) => i !== index);
|
||||
updateTags(newTags);
|
||||
}
|
||||
|
||||
function updateTags(newTags: string[]) {
|
||||
setTags(newTags);
|
||||
onChange?.(newTags);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-input focus-within:ring-primary flex min-h-9 w-full flex-wrap items-center gap-2 rounded-md border bg-transparent p-2 shadow-sm transition-colors focus-within:ring-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{tags.map((tag, index) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant='outline'
|
||||
className='border-primary bg-primary/10 flex items-center gap-1 px-1'
|
||||
>
|
||||
{tag}
|
||||
<X className='size-4 cursor-pointer' onClick={() => handleRemoveTag(index)} />
|
||||
</Badge>
|
||||
))}
|
||||
<Input
|
||||
className='h-full min-w-48 flex-1 border-none bg-transparent p-0 shadow-none !ring-0'
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={addTag}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagInput;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import { Label } from '@workspace/airo-ui/components/label';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type ReturnType = 'base64' | 'file';
|
||||
|
||||
interface UploadImageProps {
|
||||
onChange: (value: string | File) => void;
|
||||
returnType?: ReturnType;
|
||||
id?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
maxSize?: number; // Maximum file size in MB
|
||||
}
|
||||
|
||||
export const UploadImage = ({
|
||||
onChange,
|
||||
returnType = 'base64',
|
||||
id = 'image-upload',
|
||||
children,
|
||||
className,
|
||||
maxSize = 1,
|
||||
}: UploadImageProps) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const toBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
const validateFileSize = (file: File): boolean => {
|
||||
const maxSizeInBytes = maxSize * 1024 * 1024;
|
||||
if (file.size > maxSizeInBytes) {
|
||||
alert(`File size exceeds the limit (${maxSize}MB)`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
if (!validateFileSize(file)) return;
|
||||
|
||||
if (returnType === 'base64') {
|
||||
const base64 = await toBase64(file);
|
||||
onChange(base64);
|
||||
} else {
|
||||
onChange(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
if (!validateFileSize(file)) return;
|
||||
|
||||
if (returnType === 'base64') {
|
||||
const base64 = await toBase64(file);
|
||||
onChange(base64);
|
||||
} else {
|
||||
onChange(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input type='file' accept='image/*' className='hidden' id={id} onChange={handleImageUpload} />
|
||||
<Label
|
||||
htmlFor={id}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
!children && 'flex items-center justify-center rounded-lg border-2 border-dashed p-4',
|
||||
isDragging && 'border-primary bg-muted/50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children || <Upload />}
|
||||
</Label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import rtlDetect from 'rtl-detect';
|
||||
|
||||
export function getLangDir(locale: string) {
|
||||
return rtlDetect.getLangDir(locale);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener('change', onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export const useOutsideClick = (
|
||||
ref: React.RefObject<HTMLDivElement>,
|
||||
callback: (event: MouseEvent | TouchEvent) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', listener);
|
||||
document.addEventListener('touchstart', listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', listener);
|
||||
document.removeEventListener('touchstart', listener);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ToastActionElement, ToastProps } from '@workspace/airo-ui/components/toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
};
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, 'id'>;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { toast, useToast };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user