diff --git a/app/components/ui/accordion.tsx b/app/components/ui/accordion.tsx new file mode 100644 index 000000000..bcba47b1f --- /dev/null +++ b/app/components/ui/accordion.tsx @@ -0,0 +1,84 @@ +import { cn } from '@app/lib/utils' +import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react' +import { Accordion as AccordionPrimitive } from 'radix-ui' +import type * as React from 'react' + +function Accordion({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } diff --git a/app/components/ui/alert-dialog.tsx b/app/components/ui/alert-dialog.tsx new file mode 100644 index 000000000..dace474a0 --- /dev/null +++ b/app/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +import { Button } from '@app/components/ui/button' +import { cn } from '@app/lib/utils' +import { AlertDialog as AlertDialogPrimitive } from 'radix-ui' +import type * as React from 'react' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = 'default', + ...props +}: React.ComponentProps & { + size?: 'default' | 'sm' +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + variant = 'default', + size = 'default', + ...props +}: React.ComponentProps & + Pick, 'variant' | 'size'>) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = 'outline', + size = 'default', + ...props +}: React.ComponentProps & + Pick, 'variant' | 'size'>) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/app/components/ui/alert.tsx b/app/components/ui/alert.tsx new file mode 100644 index 000000000..3f1e717f4 --- /dev/null +++ b/app/components/ui/alert.tsx @@ -0,0 +1,75 @@ +import { cn } from '@app/lib/utils' +import { cva, type VariantProps } from 'class-variance-authority' +import type * as React from 'react' + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground', + className, + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertAction, AlertDescription, AlertTitle } diff --git a/app/components/ui/aspect-ratio.tsx b/app/components/ui/aspect-ratio.tsx new file mode 100644 index 000000000..47732b8bf --- /dev/null +++ b/app/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import { AspectRatio as AspectRatioPrimitive } from 'radix-ui' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/app/components/ui/attachment.tsx b/app/components/ui/attachment.tsx new file mode 100644 index 000000000..8726a61bc --- /dev/null +++ b/app/components/ui/attachment.tsx @@ -0,0 +1,203 @@ +import { Button } from '@app/components/ui/button' +import { cn } from '@app/lib/utils' +import { cva, type VariantProps } from 'class-variance-authority' +import { Slot } from 'radix-ui' +import type * as React from 'react' + +const attachmentVariants = cva( + 'group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed', + { + variants: { + size: { + default: + 'gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2', + sm: 'gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5', + xs: 'gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1', + }, + orientation: { + horizontal: 'min-w-40 items-center', + vertical: 'w-24 flex-col has-data-[slot=attachment-content]:w-30', + }, + }, + }, +) + +function Attachment({ + className, + state = 'done', + size = 'default', + orientation = 'horizontal', + ...props +}: React.ComponentProps<'div'> & + VariantProps & { + state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done' + }) { + return ( +
+ ) +} + +const attachmentMediaVariants = cva( + "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5", + { + variants: { + variant: { + icon: '', + image: + 'opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover', + }, + }, + defaultVariants: { + variant: 'icon', + }, + }, +) + +function AttachmentMedia({ + className, + variant = 'icon', + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AttachmentContent({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AttachmentTitle({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ) +} + +function AttachmentDescription({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ) +} + +function AttachmentActions({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AttachmentAction({ + className, + variant, + size = 'icon-xs', + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CarouselNext({ + className, + variant = 'outline', + size = 'icon-sm', + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +} + +export { + Carousel, + type CarouselApi, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + useCarousel, +} diff --git a/app/components/ui/chart.tsx b/app/components/ui/chart.tsx new file mode 100644 index 000000000..7bbcc3e8d --- /dev/null +++ b/app/components/ui/chart.tsx @@ -0,0 +1,397 @@ +'use client' + +import { cn } from '@app/lib/utils' +import * as React from 'react' +import type { TooltipValueType } from 'recharts' +import * as RechartsPrimitive from 'recharts' + +/* + * Upstream scopes the dark half of the generated rules with a .dark class. + * This project keeps exactly one dark mechanism, the prefers-color-scheme + * query in styles.css; a class variant was introduced by shadcn init once + * and removed deliberately. So the dark block is wrapped in that same query + * instead, and a chart series follows the OS setting like everything else. + * + * Format: { THEME_NAME: AT_RULE | null } + */ +const THEMES = { + light: null, + dark: '@media (prefers-color-scheme: dark)', +} as const + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +> + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error('useChart must be used within a ') + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<'div'> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >['children'] + initialDimension?: { + width: number + height: number + } +}) { + const uniqueId = React.useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}` + + return ( + +
+ + + {children} + +
+
+ ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color, + ) + + if (!colorConfig.length) { + return null + } + + /* + * SECURITY. itemConfig.color is interpolated straight into a style element + * with no escaping, so whoever supplies ChartConfig can write arbitrary CSS + * into the page. CSS injection is not harmless: attribute selectors paired + * with background-image URLs exfiltrate values, and absolute positioning + * redresses controls. + * + * This is upstream shadcn source and safe while ChartConfig is a literal + * written by us. It stops being safe the moment a colour reaches it from a + * request, from stored user settings, or from operator config, all of which + * exist in this project. Sanitise at the first caller that does that, or + * validate colours against a strict pattern here. + * + * Note also that noDangerouslySetInnerHtml is switched off for this + * directory, so the lint rule that would otherwise flag this will not. + */ + return ( +