Remove unused files

This commit is contained in:
Joshen Lim committed 2025-06-10 16:53:36 +08:00
1 parent 8be344a8e8
commit 85fc2cf73b
163 files changed
+93 -12235

No files matched your search

@@ -1,57 +0,0 @@
'use client'
import * as React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import { cn } from 'ui'
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 font-medium transition-transform hover:underline [&[data-state=open]>svg]:rotate-180',
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 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={cn(
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -1,103 +0,0 @@
'use client'
import { Button } from 'ui'
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
import { composeRefs } from 'components/interfaces/DataTableDemo/lib/compose-refs'
import { cn } from 'ui'
import { cva, VariantProps } from 'class-variance-authority'
import { Check, Copy, Plus } from 'lucide-react'
import * as React from 'react'
const containerVariants = cva(
'peer whitespace-pre-wrap break-all rounded-md border p-2 font-mono text-sm',
{
variants: {
variant: {
default: 'border-border/50 bg-border/30',
destructive: 'border-destructive/50 bg-destructive/30',
},
defaultVariants: {
variant: 'default',
},
},
}
)
export interface CopyToClipboardContainerProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof containerVariants> {
/**
* If set and the content exceeds the maximum height,
* a "Show content" button will collapse the full content
*/
maxHeight?: number
}
export const CopyToClipboardContainer = React.forwardRef<
HTMLDivElement,
CopyToClipboardContainerProps
>(({ children, variant, maxHeight, className, ...props }, ref) => {
const [open, setOpen] = React.useState(false)
const [collapsible, setCollapsible] = React.useState(!!maxHeight)
const innerRef = React.useRef<HTMLDivElement>(null)
const { copy, isCopied } = useCopyToClipboard()
React.useLayoutEffect(() => {
if (innerRef.current && maxHeight) {
// REMINDER: scrollHeight will keep the max possible height of the content
// if maxHeight is set, we show the button by default and remove it if height is too narrow.
// That way, we avoid having a layout shift of first showing the full height and then collapsible.
if (innerRef.current.scrollHeight <= maxHeight) {
setCollapsible(false)
}
}
}, [innerRef.current, maxHeight])
return (
<div
className="group relative text-left"
style={
{
'--max-height': `${maxHeight}px`,
} as React.CSSProperties
}
>
<div
ref={composeRefs(ref, innerRef)}
className={cn(
containerVariants({ variant }),
collapsible && !open ? 'max-h-[var(--max-height)] overflow-hidden' : undefined,
className
)}
{...props}
>
{children}
</div>
<Button
variant="outline"
size="icon"
className="absolute right-2 top-2 h-6 w-6 opacity-0 focus:opacity-100 group-hover:opacity-100 peer-focus:opacity-100"
onClick={() => {
const content = innerRef.current?.textContent
if (content) copy(content)
}}
>
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
</Button>
{collapsible && !open ? (
<div className="absolute inset-x-px bottom-px flex items-center justify-center rounded-b-md bg-gradient-to-b from-background/0 to-background/100">
<Button
variant="outline"
size="sm"
className="my-1 rounded-full"
onClick={() => setOpen(true)}
>
<Plus className="mr-2 h-4 w-4" /> Show content
</Button>
</div>
) : null}
</div>
)
})
CopyToClipboardContainer.displayName = 'CopyToClipboardContainer'
@@ -1,40 +0,0 @@
import * as React from 'react'
import { cn } from 'ui'
export interface InputWithAddonsProps extends React.InputHTMLAttributes<HTMLInputElement> {
leading?: React.ReactNode
trailing?: React.ReactNode
containerClassName?: string
}
const InputWithAddons = React.forwardRef<HTMLInputElement, InputWithAddonsProps>(
({ leading, trailing, containerClassName, className, ...props }, ref) => {
return (
<div
className={cn(
'border-input ring-offset-background focus-within:ring-ring group flex h-10 w-full rounded-md border bg-transparent text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 overflow-hidden',
containerClassName
)}
>
{leading ? (
<div className="border-input bg-muted/50 border-r px-3 py-2">{leading}</div>
) : null}
<input
className={cn(
'placeholder:text-muted-foreground bg-background w-full rounded-md px-3 py-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
{trailing ? (
<div className="border-input bg-muted/50 border-l px-3 py-2">{trailing}</div>
) : null}
</div>
)
}
)
InputWithAddons.displayName = 'InputWithAddons'
export { InputWithAddons }
@@ -1,51 +0,0 @@
// Copy Pasta from: https://github.com/sadmann7/shadcn-table/blob/main/src/components/kbd.tsx#L54
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from 'ui'
export const kbdVariants = cva(
'select-none rounded border px-1.5 py-px font-mono text-[0.7rem] font-normal font-mono shadow-sm disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-accent text-accent-foreground',
outline: 'bg-background text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
)
export interface KbdProps
extends React.ComponentPropsWithoutRef<'kbd'>,
VariantProps<typeof kbdVariants> {
/**
* The title of the `abbr` element inside the `kbd` element.
* @default undefined
* @type string | undefined
* @example title="Command"
*/
abbrTitle?: string
}
const Kbd = React.forwardRef<HTMLUnknownElement, KbdProps>(
({ abbrTitle, children, className, variant, ...props }, ref) => {
return (
<kbd className={cn(kbdVariants({ variant, className }))} ref={ref} {...props}>
{abbrTitle ? (
<abbr title={abbrTitle} className="no-underline">
{children}
</abbr>
) : (
children
)}
</kbd>
)
}
)
Kbd.displayName = 'Kbd'
export { Kbd }
@@ -1,49 +0,0 @@
import {
Table,
TableBody,
TableCell,
TableRow,
} from 'components/interfaces/DataTableDemo/components/custom/table'
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
import { Check, Copy } from 'lucide-react'
interface KVTableProps {
data: Record<string, string | number | boolean>
}
export function KVTable({ data }: KVTableProps) {
return (
<div className="mx-auto max-w-lg">
<div className="overflow-hidden rounded-lg border border-border bg-background">
<Table>
<TableBody>
{Object.entries(data).map(([key, value]) => {
return <RowAction key={key} label={key} value={value} />
})}
</TableBody>
</Table>
</div>
</div>
)
}
function RowAction({ label, value }: { label: string; value: string | number | boolean }) {
const { copy, isCopied } = useCopyToClipboard()
return (
<TableRow
className="group text-left *:border-border hover:bg-transparent [&>:not(:last-child)]:border-r"
onClick={(e) => {
e.stopPropagation()
copy(value.toString())
}}
>
<TableCell className="bg-muted/50 py-1 font-mono font-medium">{label}</TableCell>
<TableCell className="relative py-1 font-mono">
{value}
<div className="invisible absolute right-1.5 top-1.5 rounded-sm border border-border bg-background p-0.5 backdrop-blur-sm group-hover:visible">
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
</div>
</TableCell>
</TableRow>
)
}
@@ -1,46 +0,0 @@
import {
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { Braces, TableProperties } from 'lucide-react'
import { CopyToClipboardContainer } from './copy-to-clipboard-container'
import { KVTable } from './kv-table'
interface KVTabsProps {
data: Record<string, string>
className?: string
}
export function KVTabs({ data, className }: KVTabsProps) {
return (
<Tabs defaultValue="table" className={className}>
<div className="flex items-center justify-end">
<TabsList className="h-auto gap-1 bg-background px-0 py-0">
<TabsTrigger
value="table"
className="px-0 py-0 text-muted-foreground/70 data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
<TableProperties className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger
value="raw"
className="px-0 py-0 text-muted-foreground/70 data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
<Braces className="h-4 w-4" />
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="table" className="rounded-md">
<KVTable data={data} />
</TabsContent>
<TabsContent value="raw" asChild>
{/* REMINDER: either `overflow-auto whitespace-pre` or `whitespace-pre-wrap` - depends if we want to wrap the text or not */}
<CopyToClipboardContainer variant="default" className="overflow-auto whitespace-pre">
{JSON.stringify(data, null, 2)}
</CopyToClipboardContainer>
</TabsContent>
</Tabs>
)
}
@@ -1,40 +0,0 @@
import NextLink, { type LinkProps as NextLinkProps } from 'next/link'
import React from 'react'
import { cn } from 'ui'
import { ArrowUpRight } from 'lucide-react'
export interface LinkProps extends NextLinkProps {
className?: string
children?: React.ReactNode
hideArrow?: boolean
}
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
({ className, href, children, hideArrow, ...props }, ref) => {
const isInternal = href?.toString().startsWith('/') || href?.toString().startsWith('#')
const externalLinkProps = !isInternal ? { target: '_blank', rel: 'noreferrer' } : undefined
return (
<NextLink
className={cn(
'group text-foreground underline underline-offset-4 decoration-border hover:decoration-foreground',
'ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-md',
className
)}
ref={ref}
href={href}
{...externalLinkProps}
{...props}
>
{children}
{!isInternal && !hideArrow ? (
<ArrowUpRight className="text-muted-foreground w-4 h-4 inline-block ml-0.5 group-hover:text-foreground group-hover:-translate-y-px group-hover:translate-x-px" />
) : null}
</NextLink>
)
}
)
Link.displayName = 'Link'
export { Link }
@@ -1,128 +0,0 @@
'use client'
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from 'ui'
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(
'fixed inset-0 z-50 bg-background/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
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> {
hideClose?: boolean
}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, hideClose, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
{!hideClose ? (
<SheetPrimitive.Close
autoFocus={true}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
) : null}
</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-lg font-semibold text-foreground', 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-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
@@ -1,35 +0,0 @@
// Props to https://github.com/shadcn-ui/ui/issues/885#issuecomment-2059600641
'use client'
import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import { cn } from 'ui'
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => {
const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max]
return (
<SliderPrimitive.Root
ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{initialValue.map((_, index) => (
<React.Fragment key={index}>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</React.Fragment>
))}
</SliderPrimitive.Root>
)
})
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
@@ -1,333 +0,0 @@
'use client'
import * as React from 'react'
import type {
DndContextProps,
DraggableSyntheticListeners,
DropAnimation,
UniqueIdentifier,
} from '@dnd-kit/core'
import {
closestCenter,
defaultDropAnimationSideEffects,
DndContext,
DragOverlay,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
restrictToHorizontalAxis,
restrictToParentElement,
restrictToVerticalAxis,
} from '@dnd-kit/modifiers'
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
useSortable,
verticalListSortingStrategy,
type SortableContextProps,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Slot, type SlotProps } from '@radix-ui/react-slot'
import { composeRefs } from 'components/interfaces/DataTableDemo/lib/compose-refs'
import { cn } from 'ui'
import { Button, type ButtonProps } from 'ui'
import { createPortal } from 'react-dom'
const orientationConfig = {
vertical: {
modifiers: [restrictToVerticalAxis, restrictToParentElement],
strategy: verticalListSortingStrategy,
},
horizontal: {
modifiers: [restrictToHorizontalAxis, restrictToParentElement],
strategy: horizontalListSortingStrategy,
},
mixed: {
modifiers: [restrictToParentElement],
strategy: undefined,
},
}
interface SortableProps<TData extends { id: UniqueIdentifier }> extends DndContextProps {
/**
* An array of data items that the sortable component will render.
* @example
* value={[
* { id: 1, name: 'Item 1' },
* { id: 2, name: 'Item 2' },
* ]}
*/
value: TData[]
/**
* An optional callback function that is called when the order of the data items changes.
* It receives the new array of items as its argument.
* @example
* onValueChange={(items) => console.log(items)}
*/
onValueChange?: (items: TData[]) => void
/**
* An optional callback function that is called when an item is moved.
* It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item.
* This will override the default behavior of updating the order of the data items.
* @type (event: { activeIndex: number; overIndex: number }) => void
* @example
* onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)}
*/
onMove?: (event: { activeIndex: number; overIndex: number }) => void
/**
* A collision detection strategy that will be used to determine the closest sortable item.
* @default closestCenter
* @type DndContextProps["collisionDetection"]
*/
collisionDetection?: DndContextProps['collisionDetection']
/**
* An array of modifiers that will be used to modify the behavior of the sortable component.
* @default
* [restrictToVerticalAxis, restrictToParentElement]
* @type Modifier[]
*/
modifiers?: DndContextProps['modifiers']
/**
* A sorting strategy that will be used to determine the new order of the data items.
* @default verticalListSortingStrategy
* @type SortableContextProps["strategy"]
*/
strategy?: SortableContextProps['strategy']
/**
* Specifies the axis for the drag-and-drop operation. It can be "vertical", "horizontal", or "both".
* @default "vertical"
* @type "vertical" | "horizontal" | "mixed"
*/
orientation?: 'vertical' | 'horizontal' | 'mixed'
/**
* An optional React node that is rendered on top of the sortable component.
* It can be used to display additional information or controls.
* @default null
* @type React.ReactNode | null
* @example
* overlay={<Skeleton className="w-full h-8" />}
*/
overlay?: React.ReactNode | null
}
function Sortable<TData extends { id: UniqueIdentifier }>({
value,
onValueChange,
onDragStart,
onDragEnd,
onDragCancel,
collisionDetection = closestCenter,
modifiers,
strategy,
onMove,
orientation = 'vertical',
overlay,
children,
...props
}: SortableProps<TData>) {
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null)
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor)
)
const config = orientationConfig[orientation]
return (
<DndContext
modifiers={modifiers ?? config.modifiers}
sensors={sensors}
onDragStart={(event) => {
setActiveId(event.active.id)
onDragStart?.(event)
}}
onDragEnd={(event) => {
const { active, over } = event
if (over && active.id !== over?.id) {
const activeIndex = value.findIndex((item) => item.id === active.id)
const overIndex = value.findIndex((item) => item.id === over.id)
if (onMove) {
onMove({ activeIndex, overIndex })
} else {
onValueChange?.(arrayMove(value, activeIndex, overIndex))
}
}
setActiveId(null)
onDragEnd?.(event)
}}
onDragCancel={(event) => {
setActiveId?.(null)
onDragCancel?.(event)
}}
collisionDetection={collisionDetection}
{...props}
>
<SortableContext items={value} strategy={strategy ?? config.strategy}>
{children}
</SortableContext>
{overlay
? // https://docs.dndkit.com/api-documentation/draggable/drag-overlay#portals
createPortal(
<SortableOverlay activeId={activeId}>{overlay}</SortableOverlay>,
document.body
)
: null}
</DndContext>
)
}
const dropAnimationOpts: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.4',
},
},
}),
}
interface SortableOverlayProps extends React.ComponentPropsWithRef<typeof DragOverlay> {
activeId?: UniqueIdentifier | null
}
const SortableOverlay = React.forwardRef<HTMLDivElement, SortableOverlayProps>(
({ activeId, dropAnimation = dropAnimationOpts, children, ...props }, ref) => {
return (
<DragOverlay dropAnimation={dropAnimation} {...props}>
{activeId ? (
<SortableItem ref={ref} value={activeId} className="cursor-grabbing" asChild>
{children}
</SortableItem>
) : null}
</DragOverlay>
)
}
)
SortableOverlay.displayName = 'SortableOverlay'
interface SortableItemContextProps {
attributes: React.HTMLAttributes<HTMLElement>
listeners: DraggableSyntheticListeners | undefined
isDragging?: boolean
}
const SortableItemContext = React.createContext<SortableItemContextProps>({
attributes: {},
listeners: undefined,
isDragging: false,
})
function useSortableItem() {
const context = React.useContext(SortableItemContext)
if (!context) {
throw new Error('useSortableItem must be used within a SortableItem')
}
return context
}
interface SortableItemProps extends SlotProps {
/**
* The unique identifier of the item.
* @example "item-1"
* @type UniqueIdentifier
*/
value: UniqueIdentifier
/**
* Specifies whether the item should act as a trigger for the drag-and-drop action.
* @default false
* @type boolean | undefined
*/
asTrigger?: boolean
/**
* Merges the item's props into its immediate child.
* @default false
* @type boolean | undefined
*/
asChild?: boolean
}
const SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>(
({ value, asTrigger, asChild, className, ...props }, ref) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: value,
})
const context = React.useMemo<SortableItemContextProps>(
() => ({
attributes,
listeners,
isDragging,
}),
[attributes, listeners, isDragging]
)
const style: React.CSSProperties = {
opacity: isDragging ? 0.5 : 1,
transform: CSS.Translate.toString(transform),
transition,
}
const Comp = asChild ? Slot : 'div'
return (
<SortableItemContext.Provider value={context}>
<Comp
data-state={isDragging ? 'dragging' : undefined}
className={cn(
'data-[state=dragging]:cursor-grabbing',
{ 'cursor-grab': !isDragging && asTrigger },
className
)}
ref={composeRefs(ref, setNodeRef as React.Ref<HTMLDivElement>)}
style={style}
{...(asTrigger ? attributes : {})}
{...(asTrigger ? listeners : {})}
{...props}
/>
</SortableItemContext.Provider>
)
}
)
SortableItem.displayName = 'SortableItem'
interface SortableDragHandleProps extends ButtonProps {
withHandle?: boolean
}
const SortableDragHandle = React.forwardRef<HTMLButtonElement, SortableDragHandleProps>(
({ className, ...props }, ref) => {
const { attributes, listeners, isDragging } = useSortableItem()
return (
<Button
ref={composeRefs(ref)}
data-state={isDragging ? 'dragging' : undefined}
className={cn('cursor-grab data-[state=dragging]:cursor-grabbing', className)}
{...attributes}
{...listeners}
{...props}
/>
)
}
)
SortableDragHandle.displayName = 'SortableDragHandle'
export { Sortable, SortableDragHandle, SortableItem, SortableOverlay }
@@ -1,102 +0,0 @@
import * as React from 'react'
import { cn } from 'ui'
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
containerClassName?: string
}
const Table = React.forwardRef<HTMLTableElement, TableProps>(
({ className, containerClassName, onScroll, ...props }, ref) => (
<div
className={cn('w-full overflow-auto', containerClassName)}
// REMINDER: we are not scrolling the table, but the container
{...{ onScroll }}
>
<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-primary text-primary-foreground font-medium', 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, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
@@ -1,54 +0,0 @@
import React, { useState, useRef, useEffect } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TooltipPortal } from 'ui'
import { cn } from 'ui'
interface TextWithTooltipProps {
text: string | number
className?: string
}
export function TextWithTooltip({ text, className }: TextWithTooltipProps) {
const [isTruncated, setIsTruncated] = useState<boolean>(false)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const checkTruncation = () => {
if (textRef.current) {
const { scrollWidth, clientWidth } = textRef.current
setIsTruncated(scrollWidth > clientWidth)
}
}
const resizeObserver = new ResizeObserver(() => {
checkTruncation()
})
if (textRef.current) {
resizeObserver.observe(textRef.current)
}
checkTruncation()
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<TooltipProvider delayDuration={100} disableHoverableContent>
<Tooltip>
<TooltipTrigger disabled={!isTruncated} asChild>
<div
ref={textRef}
className={cn('truncate', !isTruncated && 'pointer-events-none', className)}
>
{text}
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{text}</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
)
}
@@ -1,56 +0,0 @@
import type { Column } from '@tanstack/react-table'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { Button, type ButtonProps } from 'ui'
import { cn } from 'ui'
interface DataTableColumnHeaderProps<TData, TValue> extends ButtonProps {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
...props
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<Button
type="text"
size="small"
onClick={() => {
column.toggleSorting(undefined)
}}
className={cn(
'text-xs',
'py-0 px-0 h-7 hover:bg-transparent flex gap-2 items-center justify-between w-full',
className
)}
iconRight={
<span className="flex flex-col">
<ChevronUp
className={cn(
'-mb-1 hover:text-foreground-lighter',
column.getIsSorted() === 'asc' ? 'text-foreground' : 'text-foreground-muted'
)}
/>
<ChevronDown
className={cn(
'-mt-1 hover:text-foreground-lighter',
column.getIsSorted() === 'desc' ? 'text-foreground' : 'text-foreground-muted'
)}
/>
</span>
}
{...props}
>
<span>{title}</span>
</Button>
)
}
@@ -1,32 +0,0 @@
Reusable Column Components
This folder is dedicated for components that can be used as within the `columns.tsx` structure.
Components:
- `DataTableColumnTimestamp`: A hover card with the _Unix timestamp_, _UTC_, _timezone_ and _relative_ (x days ago) values
- `DataTableLevelIndicator`: A simple colored square to indicate `"error" | "warning" | "success" | "info"` data
Usage Example:
```tsx
"use client";
import { DataTableColumnLevelIndicator } from "@/components/data-table/data-table-column/data-table-column-level-indicator";
import type { ColumnDef } from "@tanstack/react-table";
export type ColumnSchema = {
// ...
};
export const columns: ColumnDef<ColumnSchema>[] = [
{
accessorKey: "level",
header: "",
cell: ({ row }) => {
const level = row.getValue("level") as (typeof LEVELS)[number];
return <DataTableColumnLevelIndicator level={level} />;
},
},
];
```
@@ -1,10 +0,0 @@
export function DataTableColumnLatency({ value }: { value: number }) {
return (
<div className="font-mono">
{new Intl.NumberFormat("en-US", { maximumFractionDigits: 3 }).format(
value,
)}
<span className="text-muted-foreground">ms</span>
</div>
);
}
@@ -1,17 +0,0 @@
import { _LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
import { getLevelColor } from 'components/interfaces/DataTableDemo/lib/request/level'
import { cn } from 'ui'
export function DataTableColumnLevelIndicator({
value,
className,
}: {
value: (typeof _LEVELS)[number]
className?: string
}) {
return (
<div className={cn('flex items-center justify-center', className)}>
<div className={cn('h-2.5 w-2.5 rounded-[2px]', getLevelColor(value).bg)} />
</div>
)
}
@@ -1,30 +0,0 @@
import { regions } from 'components/interfaces/DataTableDemo/constants/region'
interface DataTableColumnRegionProps {
value: string
showFlag?: boolean
reverse?: boolean
}
export function DataTableColumnRegion({
value,
showFlag = false,
reverse = false,
}: DataTableColumnRegionProps) {
const region = regions[value] ?? { label: value, flag: '' }
return reverse ? (
<>
<span className="text-xs text-muted-foreground">
{showFlag && region.flag} {region.label}
</span>{' '}
<span>{value}</span>
</>
) : (
<>
<span>{value}</span>{' '}
<span className="text-xs text-muted-foreground">
{showFlag && region.flag} {region.label}
</span>
</>
)
}
@@ -1,36 +0,0 @@
import { getStatusColor } from 'components/interfaces/DataTableDemo/lib/request/status-code'
import { cn } from 'ui'
import { Minus } from 'lucide-react'
import { color } from 'framer-motion'
export function DataTableColumnStatusCode({
value,
level,
className,
}: {
value?: number | string
level?: string
className?: string
}) {
// console.log('level', level, level === 'warning' ? value : 'no warning')
const colors = getStatusColor(level)
if (!value) {
return <Minus className="h-4 w-4 text-muted-foreground/50" />
}
return (
<div className={cn('flex items-center relative', className)}>
<div
className={cn(
'px-1 py-[0.03rem] rounded-md',
'flex items-center justify-center relative font-mono',
colors.text,
colors.bg,
colors.border
)}
>
{value}
</div>
</div>
)
}
@@ -1,75 +0,0 @@
'use client'
import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
import { cn } from 'ui'
import { UTCDate } from '@date-fns/utc'
import { HoverCardPortal } from '@radix-ui/react-hover-card'
import { format, formatDistanceToNowStrict } from 'date-fns'
import { Check, Copy } from 'lucide-react'
import type { ComponentPropsWithoutRef } from 'react'
type HoverCardContentProps = ComponentPropsWithoutRef<typeof HoverCardContent>
interface DataTableColumnTimestampProps {
date: Date
side?: HoverCardContentProps['side']
sideOffset?: HoverCardContentProps['sideOffset']
align?: HoverCardContentProps['align']
alignOffset?: HoverCardContentProps['alignOffset']
className?: string
}
export function DataTableColumnTimestamp({
date,
side = 'right',
align = 'start',
alignOffset = -4,
sideOffset,
className,
}: DataTableColumnTimestampProps) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
return (
<HoverCard openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<div className={cn('whitespace-nowrap font-mono', className)}>
{format(date, 'LLL dd, y HH:mm:ss')}
</div>
</HoverCardTrigger>
{/* REMINDER: allows us to port the content to the document.body, which is helpful when using opacity-50 on the row element */}
<HoverCardPortal>
<HoverCardContent className="z-10 w-auto p-2" {...{ side, align, alignOffset, sideOffset }}>
<dl className="flex flex-col gap-1">
<Row value={String(date.getTime())} label="Timestamp" />
<Row value={format(new UTCDate(date), 'LLL dd, y HH:mm:ss')} label="UTC" />
<Row value={format(date, 'LLL dd, y HH:mm:ss')} label={timezone} />
<Row value={formatDistanceToNowStrict(date, { addSuffix: true })} label="Relative" />
</dl>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
)
}
function Row({ value, label }: { value: string; label: string }) {
const { copy, isCopied } = useCopyToClipboard()
return (
<div
className="group flex items-center justify-between gap-4 text-sm"
onClick={(e) => {
e.stopPropagation()
copy(value)
}}
>
<dt className="text-muted-foreground">{label}</dt>
<dd className="flex items-center gap-1 truncate font-mono">
<span className="invisible group-hover:visible">
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
</span>
{value}
</dd>
</div>
)
}
@@ -1,122 +0,0 @@
'use client'
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { Checkbox_Shadcn_ as Checkbox } from 'ui'
import { Label_Shadcn_ as Label } from 'ui'
import { Skeleton } from 'ui'
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
import { cn } from 'ui'
import { Search } from 'lucide-react'
import { useState } from 'react'
import type { DataTableCheckboxFilterField } from './types'
export function DataTableFilterCheckbox<TData>({
value: _value,
options,
component,
}: DataTableCheckboxFilterField<TData>) {
const value = _value as string
const [inputValue, setInputValue] = useState('')
const { table, columnFilters, isLoading, getFacetedUniqueValues } = useDataTable()
const column = table.getColumn(value)
// REMINDER: avoid using column?.getFilterValue()
const filterValue = columnFilters.find((i) => i.id === value)?.value
const facetedValue = getFacetedUniqueValues?.(table, value) || column?.getFacetedUniqueValues()
const Component = component
// filter out the options based on the input value
const filterOptions = options?.filter(
(option) => inputValue === '' || option.label.toLowerCase().includes(inputValue.toLowerCase())
)
// CHECK: it could be filterValue or searchValue
const filters = filterValue ? (Array.isArray(filterValue) ? filterValue : [filterValue]) : []
// REMINDER: if no options are defined, while fetching data, we should show a skeleton
if (isLoading && !filterOptions?.length)
return (
<div className="grid divide-y rounded-lg border border-border">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="flex items-center justify-between gap-2 px-2 py-2.5">
<Skeleton className="h-4 w-4 rounded-sm" />
<Skeleton className="h-4 w-full rounded-sm" />
</div>
))}
</div>
)
if (!filterOptions?.length) return null
return (
<div className="grid gap-2">
{options && options.length > 4 ? (
<InputWithAddons
placeholder="Search"
leading={<Search className="mt-0.5 h-4 w-4" />}
containerClassName="h-9 rounded-lg"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
) : null}
{/* FIXME: due to the added max-h and overflow-y-auto, the hover state and border is laying on top of the scroll bar */}
<div className="max-h-[200px] overflow-y-auto rounded-lg border border-border empty:border-none">
{filterOptions
// TODO: we shoudn't sort the options here, instead filterOptions should be sorted by default
// .sort((a, b) => a.label.localeCompare(b.label))
.map((option, index) => {
const checked = filters.includes(option.value)
return (
<div
key={String(option.value)}
className={cn(
'group relative flex items-center space-x-2 px-2 py-2.5 hover:bg-accent/50',
index !== filterOptions.length - 1 ? 'border-b' : undefined
)}
>
<Checkbox
id={`${value}-${option.value}`}
checked={checked}
onCheckedChange={(checked) => {
const newValue = checked
? [...(filters || []), option.value]
: filters?.filter((value) => option.value !== value)
column?.setFilterValue(newValue?.length ? newValue : undefined)
}}
/>
<Label
htmlFor={`${value}-${option.value}`}
className="flex w-full items-center justify-center gap-1 truncate text-foreground/70 group-hover:text-accent-foreground"
>
{Component ? (
<Component {...option} />
) : (
<span className="truncate font-normal">{option.label}</span>
)}
<span className="ml-auto flex items-center justify-center font-mono text-xs">
{isLoading ? (
<Skeleton className="h-4 w-4" />
) : facetedValue?.has(option.value) ? (
formatCompactNumber(facetedValue.get(option.value) || 0)
) : null}
</span>
<button
type="button"
onClick={() => column?.setFilterValue([option.value])}
className={cn(
'absolute inset-y-0 right-0 hidden font-normal text-muted-foreground backdrop-blur-sm hover:text-foreground group-hover:block',
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
)}
>
<span className="px-2">only</span>
</button>
</Label>
</div>
)
})}
</div>
</div>
)
}
@@ -1,400 +0,0 @@
'use client'
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import {
Command_Shadcn_ as Command,
CommandEmpty_Shadcn_ as CommandEmpty,
CommandGroup_Shadcn_ as CommandGroup,
CommandInput_Shadcn_ as CommandInput,
CommandItem_Shadcn_ as CommandItem,
CommandList_Shadcn_ as CommandList,
CommandSeparator_Shadcn_ as CommandSeparator,
} from 'ui'
import { Separator } from 'ui'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
import { cn } from 'ui'
import { formatDistanceToNow } from 'date-fns'
import { LoaderCircle, Search, X } from 'lucide-react'
import { ParserBuilder } from 'nuqs'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import type { DataTableFilterField } from '../types'
import {
columnFiltersParser,
getFieldOptions,
getFilterValue,
getWordByCaretPosition,
replaceInputByFieldType,
} from './utils'
// FIXME: there is an issue on cmdk if I wanna only set a single slider value...
interface DataTableFilterCommandProps {
// TODO: maybe use generics for the parser
searchParamsParser: Record<string, ParserBuilder<any>>
}
export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCommandProps) {
const { table, isLoading, filterFields: _filterFields, getFacetedUniqueValues } = useDataTable()
const columnFilters = table.getState().columnFilters
const inputRef = useRef<HTMLInputElement>(null)
const [open, setOpen] = useState<boolean>(false)
const [currentWord, setCurrentWord] = useState<string>('')
const filterFields = useMemo(
() => _filterFields?.filter((i) => !i.commandDisabled),
[_filterFields]
)
const columnParser = useMemo(
() => columnFiltersParser({ searchParamsParser, filterFields }),
[searchParamsParser, filterFields]
)
const [inputValue, setInputValue] = useState<string>(columnParser.serialize(columnFilters))
const [lastSearches, setLastSearches] = useLocalStorage<
{
search: string
timestamp: number
}[]
>('data-table-command', [])
useEffect(() => {
// TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing
if (currentWord !== '' && open) return
// reset
if (currentWord !== '' && !open) setCurrentWord('')
// avoid recursion
if (inputValue.trim() === '' && !open) return
const searchParams = columnParser.parse(inputValue)
const currentFilters = table.getState().columnFilters
const currentEnabledFilters = currentFilters.filter((filter) => {
const field = _filterFields?.find((field) => field.value === filter.id)
return !field?.commandDisabled
})
const currentDisabledFilters = currentFilters.filter((filter) => {
const field = _filterFields?.find((field) => field.value === filter.id)
return field?.commandDisabled
})
const commandDisabledFilterKeys = currentDisabledFilters.reduce(
(prev, curr) => {
prev[curr.id] = curr.value
return prev
},
{} as Record<string, unknown>
)
for (const key of Object.keys(searchParams)) {
const value = searchParams[key as keyof typeof searchParams]
table.getColumn(key)?.setFilterValue(value)
}
const currentFiltersToReset = currentEnabledFilters.filter((filter) => {
return !(filter.id in searchParams)
})
for (const filter of currentFiltersToReset) {
table.getColumn(filter.id)?.setFilterValue(undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue, open, currentWord])
useEffect(() => {
// REMINDER: only update the input value if the command is closed (avoids jumps while open)
if (!open) {
setInputValue(columnParser.serialize(columnFilters))
}
}, [columnFilters, filterFields, open])
useHotKey(() => setOpen((open) => !open), 'k')
useEffect(() => {
if (open) {
inputRef?.current?.focus()
}
}, [open])
return (
<div>
<button
type="button"
className={cn(
'group flex w-full items-center rounded-lg border border-input bg-background px-3 text-muted-foreground ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 hover:bg-accent/50 hover:text-accent-foreground',
open ? 'hidden' : 'visible'
)}
onClick={() => setOpen(true)}
>
{isLoading ? (
<LoaderCircle className="mr-2 h-4 w-4 shrink-0 animate-spin text-muted-foreground opacity-50 group-hover:text-popover-foreground" />
) : (
<Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground opacity-50 group-hover:text-popover-foreground" />
)}
<span className="h-11 w-full max-w-sm truncate py-3 text-left text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 md:max-w-xl lg:max-w-4xl xl:max-w-5xl">
{inputValue.trim() ? (
<span className="text-foreground">{inputValue}</span>
) : (
<span>Search data table...</span>
)}
</span>
<Kbd className="ml-auto text-muted-foreground group-hover:text-accent-foreground">
<span className="mr-1">⌘</span>
<span>K</span>
</Kbd>
</button>
<Command
className={cn(
'overflow-visible rounded-lg border border-border shadow-md [&>div]:border-none bg',
open ? 'visible' : 'hidden'
)}
filter={(value, search, keywords) =>
getFilterValue({ value, search, keywords, currentWord })
}
// loop
>
<CommandInput
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onKeyDown={(e) => {
if (e.key === 'Escape') inputRef?.current?.blur()
}}
onBlur={() => {
setOpen(false)
// FIXME: doesnt reflect the jumps
// FIXME: will save non-existing searches
// TODO: extract into function
const search = inputValue.trim()
if (!search) return
const timestamp = Date.now()
const searchIndex = lastSearches.findIndex((item) => item.search === search)
if (searchIndex !== -1) {
lastSearches[searchIndex].timestamp = timestamp
setLastSearches(lastSearches)
return
}
setLastSearches([...lastSearches, { search, timestamp }])
return
}}
onInput={(e) => {
const caretPosition = e.currentTarget?.selectionStart || -1
const value = e.currentTarget?.value || ''
const word = getWordByCaretPosition({ value, caretPosition })
setCurrentWord(word)
}}
placeholder="Search data table..."
className="text-foreground"
/>
<div className="relative">
<div className="absolute top-2 z-50 w-full overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md outline-none animate-in">
{/* default height is 300px but in case of more, we'd like to tease the user */}
<CommandList className="max-h-[310px] bg-surface-100">
<CommandGroup heading="Filter">
{filterFields.map((field) => {
if (typeof field.value !== 'string') return null
if (inputValue.includes(`${field.value}:`)) return null
// TBD: should we handle this in the component?
return (
<CommandItem
key={field.value}
value={field.value}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={(value) => {
setInputValue((prev) => {
if (currentWord.trim() === '') {
const input = `${prev}${value}`
return `${input}:`
}
// lots of cheat
const isStarting = currentWord === prev
const prefix = isStarting ? '' : ' '
const input = prev.replace(`${prefix}${currentWord}`, `${prefix}${value}`)
return `${input}:`
})
setCurrentWord(`${value}:`)
}}
className="group"
>
{field.value}
<CommandItemSuggestions field={field} />
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Query">
{filterFields?.map((field) => {
if (typeof field.value !== 'string') return null
if (!currentWord.includes(`${field.value}:`)) return null
const column = table.getColumn(field.value)
const facetedValue =
getFacetedUniqueValues?.(table, field.value) || column?.getFacetedUniqueValues()
const options = getFieldOptions({ field })
return options.map((optionValue) => {
return (
<CommandItem
key={`${String(field.value)}:${optionValue}`}
value={`${String(field.value)}:${optionValue}`}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={(value) => {
setInputValue((prev) =>
replaceInputByFieldType({
prev,
currentWord,
optionValue,
value,
field,
})
)
setCurrentWord('')
}}
>
{`${optionValue}`}
{facetedValue?.has(optionValue) ? (
<span className="ml-auto font-mono text-muted-foreground">
{formatCompactNumber(facetedValue.get(optionValue) || 0)}
</span>
) : null}
</CommandItem>
)
})
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Suggestions">
{lastSearches
?.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 5)
.map((item) => {
return (
<CommandItem
key={`suggestion:${item.search}`}
value={`suggestion:${item.search}`}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={(value) => {
const search = value.replace('suggestion:', '')
setInputValue(`${search} `)
setCurrentWord('')
}}
className="group"
>
{item.search}
<span className="ml-auto truncate text-muted-foreground/80 group-aria-[selected=true]:block">
{formatDistanceToNow(item.timestamp, {
addSuffix: true,
})}
</span>
<button
type="button"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
// TODO: extract into function
setLastSearches(lastSearches.filter((i) => i.search !== item.search))
}}
className="ml-1 hidden rounded-md p-0.5 hover:bg-background group-aria-[selected=true]:block"
>
<X className="h-4 w-4" />
</button>
</CommandItem>
)
})}
</CommandGroup>
<CommandEmpty>No results found.</CommandEmpty>
</CommandList>
<div
className="bg-surface-100 flex flex-wrap justify-between gap-3 border-t bg-accent/50 px-2 py-1.5 text-sm text-accent-foreground"
cmdk-footer=""
>
<div className="flex flex-wrap gap-3">
<span>
Use <Kbd variant="outline">↑</Kbd> <Kbd variant="outline">↓</Kbd> to navigate
</span>
<span>
<Kbd variant="outline">Enter</Kbd> to query
</span>
<span>
<Kbd variant="outline">Esc</Kbd> to close
</span>
<Separator orientation="vertical" className="my-auto h-3" />
<span>
Union: <Kbd variant="outline">regions:a,b</Kbd>
</span>
<span>
Range: <Kbd variant="outline">p95:59-340</Kbd>
</span>
</div>
{lastSearches.length ? (
<button
type="button"
className="text-muted-foreground hover:text-accent-foreground"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => setLastSearches([])}
>
Clear suggestions
</button>
) : null}
</div>
</div>
</div>
</Command>
</div>
)
}
// function CommandItemType<TData>
function CommandItemSuggestions<TData>({ field }: { field: DataTableFilterField<TData> }) {
const { table, getFacetedMinMaxValues, getFacetedUniqueValues } = useDataTable()
const value = field.value as string
switch (field.type) {
case 'checkbox': {
return (
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
{getFacetedUniqueValues
? Array.from(getFacetedUniqueValues(table, value)?.keys() || [])
.map((value) => `[${value}]`)
.join(' ')
: field.options?.map(({ value }) => `[${value}]`).join(' ')}
</span>
)
}
case 'slider': {
const [min, max] = getFacetedMinMaxValues?.(table, value) || [field.min, field.max]
return (
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
[{min} - {max}]
</span>
)
}
case 'input': {
return (
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
[{`${String(field.value)}`} input]
</span>
)
}
default: {
return null
}
}
}
@@ -1,262 +0,0 @@
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
SLIDER_DELIMITER,
} from 'components/interfaces/DataTableDemo/lib/delimiters'
import { isArrayOfDates } from 'components/interfaces/DataTableDemo/lib/is-array'
import { ColumnFiltersState } from '@tanstack/react-table'
import { ParserBuilder } from 'nuqs'
import type { DataTableFilterField } from '../types'
/**
* Extracts the word from the given string at the specified caret position.
*/
export function getWordByCaretPosition({
value,
caretPosition,
}: {
value: string
caretPosition: number
}) {
let start = caretPosition
let end = caretPosition
while (start > 0 && value[start - 1] !== ' ') start--
while (end < value.length && value[end] !== ' ') end++
const word = value.substring(start, end)
return word
}
export function replaceInputByFieldType<TData>({
prev,
currentWord,
optionValue,
value,
field,
}: {
prev: string
currentWord: string
optionValue?: string | number | boolean | undefined // FIXME: use DataTableFilterField<TData>["options"][number];
value: string
field: DataTableFilterField<TData>
}) {
switch (field.type) {
case 'checkbox': {
if (currentWord.includes(ARRAY_DELIMITER)) {
const words = currentWord.split(ARRAY_DELIMITER)
words[words.length - 1] = `${optionValue}`
const input = prev.replace(currentWord, words.join(ARRAY_DELIMITER))
return `${input.trim()} `
}
}
case 'slider': {
if (currentWord.includes(SLIDER_DELIMITER)) {
const words = currentWord.split(SLIDER_DELIMITER)
words[words.length - 1] = `${optionValue}`
const input = prev.replace(currentWord, words.join(SLIDER_DELIMITER))
return `${input.trim()} `
}
}
case 'timerange': {
if (currentWord.includes(RANGE_DELIMITER)) {
const words = currentWord.split(RANGE_DELIMITER)
words[words.length - 1] = `${optionValue}`
const input = prev.replace(currentWord, words.join(RANGE_DELIMITER))
return `${input.trim()} `
}
}
default: {
const input = prev.replace(currentWord, value)
return `${input.trim()} `
}
}
}
export function getFieldOptions<TData>({ field }: { field: DataTableFilterField<TData> }) {
switch (field.type) {
case 'slider': {
return field.options?.length
? field.options
.map(({ value }) => value)
.sort((a, b) => Number(a) - Number(b))
.filter(notEmpty)
: Array.from({ length: field.max - field.min + 1 }, (_, i) => field.min + i) || []
}
default: {
return field.options?.map(({ value }) => value).filter(notEmpty) || []
}
}
}
export function getFilterValue({
value,
search,
currentWord,
}: {
value: string
search: string
keywords?: string[] | undefined
currentWord: string
}): number {
/**
* @example value "suggestion:public:true regions,ams,gru,fra"
*/
if (value.startsWith('suggestion:')) {
const rawValue = value.toLowerCase().replace('suggestion:', '')
if (rawValue.includes(search)) return 1
return 0
}
/** */
if (value.toLowerCase().includes(currentWord.toLowerCase())) return 1
/**
* @example checkbox [filter, query] = ["regions", "ams,gru,fra"]
* @example slider [filter, query] = ["p95", "0-3000"]
* @example input [filter, query] = ["name", "api"]
*/
const [filter, query] = currentWord.toLowerCase().split(':')
if (query && value.startsWith(`${filter}:`)) {
if (query.includes(ARRAY_DELIMITER)) {
/**
* array of n elements
* @example queries = ["ams", "gru", "fra"]
*/
const queries = query.split(ARRAY_DELIMITER)
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
if (queries.some((item, i) => item === rawValue && i !== queries.length - 1)) return 0
if (queries.some((item) => rawValue.includes(item))) return 1
}
if (query.includes(SLIDER_DELIMITER)) {
/**
* range between 2 elements
* @example queries = ["0", "3000"]
*/
const queries = query.split(SLIDER_DELIMITER)
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
const rawValueAsNumber = Number.parseInt(rawValue)
const queryAsNumber = Number.parseInt(queries[0])
if (queryAsNumber < rawValueAsNumber) {
if (rawValue.includes(queries[1])) return 1
return 0
}
return 0
}
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
if (rawValue.includes(query)) return 1
}
return 0
}
export function getFieldValueByType<TData>({
field,
value,
}: {
field?: DataTableFilterField<TData>
value: unknown
}) {
if (!field) return null
switch (field.type) {
case 'slider': {
if (Array.isArray(value)) {
return value.join(SLIDER_DELIMITER)
}
return value
}
case 'checkbox': {
if (Array.isArray(value)) {
return value.join(ARRAY_DELIMITER)
}
// REMINER: inversed logic
if (typeof value === 'string') {
return value.split(ARRAY_DELIMITER)
}
return value
}
case 'timerange': {
if (Array.isArray(value)) {
if (isArrayOfDates(value)) {
return value.map((date) => date.getTime()).join(RANGE_DELIMITER)
}
return value.join(RANGE_DELIMITER)
}
if (value instanceof Date) {
return value.getTime()
}
return value
}
default: {
return value
}
}
}
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined
}
export function columnFiltersParser<TData>({
searchParamsParser,
filterFields,
}: {
searchParamsParser: Record<string, ParserBuilder<any>>
filterFields: DataTableFilterField<TData>[]
}) {
return {
parse: (inputValue: string) => {
// Use regex to properly extract field:value pairs
// This properly handles spaces within values
const filterPairs: Record<string, string> = {}
// This regex properly extracts field:value pairs where:
// 1. Values can contain spaces (both normal spaces and URL-encoded + characters)
// 2. It finds word characters followed by a colon as the field name
// 3. It then captures everything (including spaces and + characters) until
// it finds another field:value pattern or the end of string
// This solves the issue with values like "edge function" or "edge+function" in URLs
const regex = /(\w+):([^]*?)(?=\s+\w+:|$)/g
let match
console.log('DataTableFilterCommand parsing input:', inputValue)
while ((match = regex.exec(inputValue)) !== null) {
const [_, fieldName, fieldValue] = match
if (fieldName && fieldValue) {
filterPairs[fieldName] = fieldValue.trim()
console.log(`Parsed filter pair: ${fieldName} = ${fieldValue.trim()}`)
}
}
const searchParams = Object.entries(filterPairs).reduce(
(prev, [key, value]) => {
const parser = searchParamsParser[key]
if (!parser) return prev
prev[key] = parser.parse(value)
return prev
},
{} as Record<string, unknown>
)
return searchParams
},
serialize: (columnFilters: ColumnFiltersState) => {
const values = columnFilters.reduce((prev, curr) => {
const { commandDisabled } = filterFields?.find((field) => curr.id === field.value) || {
commandDisabled: true,
} // if column filter is not found, disable the command by default
const parser = searchParamsParser[curr.id]
if (commandDisabled || !parser) return prev
return `${prev}${curr.id}:${parser.serialize(curr.value)} `
}, '')
return values
},
}
}
@@ -1,76 +0,0 @@
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
DrawerFooter,
DrawerClose,
DrawerTrigger,
} from 'ui'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { Button } from 'ui'
import { FilterIcon } from 'lucide-react'
import { DataTableFilterControls } from './data-table-filter-controls'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import React from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { useMediaQuery } from 'components/interfaces/DataTableDemo/hooks/use-media-query'
export function DataTableFilterControlsDrawer() {
const triggerButtonRef = React.useRef<HTMLButtonElement>(null)
const isMobile = useMediaQuery('(max-width: 640px)')
useHotKey(() => {
triggerButtonRef.current?.click()
}, 'b')
return (
<Drawer>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DrawerTrigger asChild>
<Button
ref={isMobile ? triggerButtonRef : null}
variant="ghost"
size="icon"
className="h-9 w-9"
>
<FilterIcon className="w-4 h-4" />
</Button>
</DrawerTrigger>
</TooltipTrigger>
<TooltipContent side="right">
<p>
Toggle controls with{' '}
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
<span className="mr-1">⌘</span>
<span>B</span>
</Kbd>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DrawerContent className="max-h-[calc(100dvh-4rem)]">
<VisuallyHidden>
<DrawerHeader>
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription>Adjust your table filters</DrawerDescription>
</DrawerHeader>
</VisuallyHidden>
<div className="px-4 flex-1 overflow-y-auto">
<DataTableFilterControls />
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline" className="w-full">
Close
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
@@ -1,75 +0,0 @@
'use client'
import {
Accordion_Shadcn_ as Accordion,
AccordionContent_Shadcn_ as AccordionContent,
AccordionItem_Shadcn_ as AccordionItem,
AccordionTrigger_Shadcn_ as AccordionTrigger,
} from 'ui'
import type React from 'react'
import { DataTableFilterResetButton } from './data-table-filter-reset-button'
import { DataTableFilterCheckbox } from './data-table-filter-checkbox'
import { DataTableFilterSlider } from './data-table-filter-slider'
import { DataTableFilterInput } from './data-table-filter-input'
import { DataTableFilterTimerange } from './data-table-filter-timerange'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
// FIXME: use @container (especially for the slider element) to restructure elements
// TODO: only pass the columns to generate the filters!
// https://tanstack.com/table/v8/docs/framework/react/examples/filters
export function DataTableFilterControls() {
const { filterFields } = useDataTable()
return (
<Accordion
type="multiple"
defaultValue={filterFields
?.filter(({ defaultOpen }) => defaultOpen)
?.map(({ value }) => value as string)}
>
{filterFields?.map((field) => {
const value = field.value as string
return (
<AccordionItem key={value} value={value} className="border-none">
<AccordionTrigger className="w-full px-2 py-0 hover:no-underline data-[state=closed]:text-muted-foreground data-[state=open]:text-foreground focus-within:data-[state=closed]:text-foreground hover:data-[state=closed]:text-foreground">
<div className="flex w-full items-center justify-between gap-2 truncate py-2 pr-2">
<div className="flex items-center gap-2 truncate">
<p className="text-sm font-medium">{field.label}</p>
{value !== field.label.toLowerCase() && !field.commandDisabled ? (
<p className="mt-px truncate font-mono text-[10px] text-muted-foreground">
{value}
</p>
) : null}
</div>
<DataTableFilterResetButton {...field} />
</div>
</AccordionTrigger>
<AccordionContent>
{/* REMINDER: avoid the focus state to be cut due to overflow-hidden */}
{/* REMINDER: need to move within here because of accordion height animation */}
<div className="p-1">
{(() => {
switch (field.type) {
case 'checkbox': {
return <DataTableFilterCheckbox {...field} />
}
case 'slider': {
return <DataTableFilterSlider {...field} />
}
case 'input': {
return <DataTableFilterInput {...field} />
}
case 'timerange': {
return <DataTableFilterTimerange {...field} />
}
}
})()}
</div>
</AccordionContent>
</AccordionItem>
)
})}
</Accordion>
)
}
@@ -1,53 +0,0 @@
'use client'
import type { DataTableInputFilterField } from './types'
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
import { Label_Shadcn_ as Label } from 'ui'
import { Search } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useDebounce } from 'components/interfaces/DataTableDemo/hooks/use-debounce'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
function getFilter(filterValue: unknown) {
return typeof filterValue === 'string' ? filterValue : null
}
export function DataTableFilterInput<TData>({ value: _value }: DataTableInputFilterField<TData>) {
const value = _value as string
const { table, columnFilters } = useDataTable()
const column = table.getColumn(value)
const filterValue = columnFilters.find((i) => i.id === value)?.value
const filters = getFilter(filterValue)
const [input, setInput] = useState<string | null>(filters)
const debouncedInput = useDebounce(input, 500)
useEffect(() => {
const newValue = debouncedInput?.trim() === '' ? null : debouncedInput
if (debouncedInput === null) return
column?.setFilterValue(newValue)
}, [debouncedInput])
useEffect(() => {
if (debouncedInput?.trim() !== filters) {
setInput(filters)
}
}, [filters])
return (
<div className="grid w-full gap-1.5">
<Label htmlFor={value} className="sr-only px-2 text-muted-foreground">
{value}
</Label>
<InputWithAddons
placeholder="Search"
leading={<Search className="mt-0.5 h-4 w-4" />}
containerClassName="h-9 rounded-lg"
name={value}
id={value}
value={input || ''}
onChange={(e) => setInput(e.target.value)}
/>
</div>
)
}
@@ -1,42 +0,0 @@
'use client'
import type { DataTableFilterField } from './types'
import { Button } from 'ui'
import { X } from 'lucide-react'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
export function DataTableFilterResetButton<TData>({ value: _value }: DataTableFilterField<TData>) {
const { columnFilters, table } = useDataTable()
const value = _value as string
const column = table.getColumn(value)
const filterValue = columnFilters.find((f) => f.id === value)?.value
// TODO: check if we could useMemo
const filters = filterValue ? (Array.isArray(filterValue) ? filterValue : [filterValue]) : []
if (filters.length === 0) return null
return (
<Button
type="outline"
className="h-5 rounded-full px-1.5 py-1 font-mono text-[10px]"
onClick={(e) => {
e.stopPropagation()
column?.setFilterValue(undefined)
}}
onKeyDown={(e) => {
e.stopPropagation()
if (e.code === 'Enter') {
column?.setFilterValue(undefined)
}
}}
icon={<X className="h-2.5 w-2.5 text-muted-foreground" />}
asChild
>
{/* REMINDER: `AccordionTrigger` is also a button(!) and we get Hydration error when rendering button within button */}
<div role="button" tabIndex={0}>
<span>{filters.length}</span>
</div>
</Button>
)
}
@@ -1,100 +0,0 @@
'use client'
import type { DataTableSliderFilterField } from './types'
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
import { Label_Shadcn_ as Label } from 'ui'
import { Slider } from 'components/interfaces/DataTableDemo/components/custom/slider'
import { isArrayOfNumbers } from 'components/interfaces/DataTableDemo/lib/is-array'
import { useEffect, useState } from 'react'
import { useDebounce } from 'components/interfaces/DataTableDemo/hooks/use-debounce'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
function getFilter(filterValue: unknown) {
return typeof filterValue === 'number'
? [filterValue, filterValue]
: Array.isArray(filterValue) && isArrayOfNumbers(filterValue)
? filterValue.length === 1
? [filterValue[0], filterValue[0]]
: filterValue
: null
}
// TODO: discuss if we even need the `defaultMin` and `defaultMax`
export function DataTableFilterSlider<TData>({
value: _value,
min: defaultMin,
max: defaultMax,
}: DataTableSliderFilterField<TData>) {
const value = _value as string
const { table, columnFilters, getFacetedMinMaxValues } = useDataTable()
const column = table.getColumn(value)
const filterValue = columnFilters.find((i) => i.id === value)?.value
const filters = getFilter(filterValue)
const [input, setInput] = useState<number[] | null>(filters)
const [min, max] = getFacetedMinMaxValues?.(table, value) ||
column?.getFacetedMinMaxValues() || [defaultMin, defaultMax]
const debouncedInput = useDebounce(input, 500)
useEffect(() => {
if (debouncedInput?.length === 2) {
column?.setFilterValue(debouncedInput)
}
}, [debouncedInput])
useEffect(() => {
if (debouncedInput?.length !== 2) {
} else if (!filters) {
setInput(null)
} else if (debouncedInput[0] !== filters[0] || debouncedInput[1] !== filters[1]) {
setInput(filters)
}
}, [filters])
return (
<div className="grid gap-2">
<div className="flex items-center gap-4">
<div className="grid w-full gap-1.5">
<Label htmlFor={`min-${value}`} className="px-2 text-muted-foreground">
Min.
</Label>
<InputWithAddons
placeholder="from"
trailing="ms"
containerClassName="mb-2 h-9 rounded-lg"
type="number"
name={`min-${value}`}
id={`min-${value}`}
value={`${input?.[0] ?? min}`}
min={min}
max={max}
onChange={(e) => setInput((prev) => [Number(e.target.value), prev?.[1] || max])}
/>
</div>
<div className="grid w-full gap-1.5">
<Label htmlFor={`max-${value}`} className="px-2 text-muted-foreground">
Max.
</Label>
<InputWithAddons
placeholder="to"
trailing="ms"
containerClassName="mb-2 h-9 rounded-lg"
type="number"
name={`max-${value}`}
id={`max-${value}`}
value={`${input?.[1] ?? max}`}
min={min}
max={max}
onChange={(e) => setInput((prev) => [prev?.[0] || min, Number(e.target.value)])}
/>
</div>
</div>
<Slider
min={min}
max={max}
value={input?.length === 2 ? input : [min, max]}
onValueChange={(values) => setInput(values)}
/>
</div>
)
}
@@ -1,40 +0,0 @@
'use client'
import { useMemo } from 'react'
import type { DataTableTimerangeFilterField } from './types'
import { isArrayOfDates } from 'components/interfaces/DataTableDemo/lib/is-array'
import { DatePickerWithRange } from 'components/interfaces/DataTableDemo/components/custom/date-picker-with-range'
import type { DateRange } from 'react-day-picker'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
export function DataTableFilterTimerange<TData>({
value: _value,
presets,
}: DataTableTimerangeFilterField<TData>) {
const value = _value as string
const { table, columnFilters } = useDataTable()
const column = table.getColumn(value)
const filterValue = columnFilters.find((i) => i.id === value)?.value
const date: DateRange | undefined = useMemo(
() =>
filterValue instanceof Date
? { from: filterValue, to: undefined }
: Array.isArray(filterValue) && isArrayOfDates(filterValue)
? { from: filterValue?.[0], to: filterValue?.[1] }
: undefined,
[filterValue]
)
const setDate = (date: DateRange | undefined) => {
if (!date) return // TODO: remove from search params if columnFilter is removed
if (date.from && !date.to) {
column?.setFilterValue([date.from])
}
if (date.to && date.from) {
column?.setFilterValue([date.from, date.to])
}
}
return <DatePickerWithRange {...{ date, setDate, presets }} />
}
@@ -1,93 +0,0 @@
'use client'
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
import {
Select_Shadcn_ as Select,
SelectContent_Shadcn_ as SelectContent,
SelectItem_Shadcn_ as SelectItem,
SelectTrigger_Shadcn_ as SelectTrigger,
SelectValue_Shadcn_ as SelectValue,
} from 'ui'
import { Button } from 'ui'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { useMemo } from 'react'
export function DataTablePagination() {
const { table, pagination, columnFilters } = useDataTable()
const pageCount = useMemo(() => table.getPageCount(), [columnFilters])
return (
<div className="flex items-center justify-end space-x-4 md:space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-center text-sm font-medium">
Page {pagination.pageIndex + 1} of {pageCount}
</div>
<div className="flex items-center space-x-2">
<Button
type="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
table.setPageIndex(0)
}}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
type="outline"
className="h-8 w-8 p-0"
onClick={() => {
table.previousPage()
}}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
type="outline"
className="h-8 w-8 p-0"
onClick={() => {
table.nextPage()
}}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
type="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
table.setPageIndex(table.getPageCount() - 1)
}}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
)
}
@@ -1,92 +0,0 @@
import { DataTableFilterField } from 'components/interfaces/DataTableDemo/components/data-table/types'
import type {
ColumnDef,
ColumnFiltersState,
PaginationState,
RowSelectionState,
SortingState,
Table,
VisibilityState,
} from '@tanstack/react-table'
import { createContext, useContext, useMemo } from 'react'
import { ControlsProvider } from 'components/interfaces/DataTableDemo/providers/controls'
// REMINDER: read about how to move controlled state out of the useReactTable hook
// https://github.com/TanStack/table/discussions/4005#discussioncomment-7303569
interface DataTableStateContextType {
columnFilters: ColumnFiltersState
sorting: SortingState
rowSelection: RowSelectionState
columnOrder: string[]
columnVisibility: VisibilityState
pagination: PaginationState
enableColumnOrdering: boolean
}
interface DataTableBaseContextType<TData = unknown, TValue = unknown> {
table: Table<TData>
filterFields: DataTableFilterField<TData>[]
columns: ColumnDef<TData, TValue>[]
isLoading?: boolean
getFacetedUniqueValues?: (table: Table<TData>, columnId: string) => Map<string, number>
getFacetedMinMaxValues?: (table: Table<TData>, columnId: string) => undefined | [number, number]
}
interface DataTableContextType<TData = unknown, TValue = unknown>
extends DataTableStateContextType,
DataTableBaseContextType<TData, TValue> {}
export const DataTableContext = createContext<DataTableContextType<any, any> | null>(null)
export function DataTableProvider<TData, TValue>({
children,
...props
}: Partial<DataTableStateContextType> &
DataTableBaseContextType<TData, TValue> & {
children: React.ReactNode
}) {
const value = useMemo(
() => ({
...props,
columnFilters: props.columnFilters ?? [],
sorting: props.sorting ?? [],
rowSelection: props.rowSelection ?? {},
columnOrder: props.columnOrder ?? [],
columnVisibility: props.columnVisibility ?? {},
pagination: props.pagination ?? { pageIndex: 0, pageSize: 10 },
enableColumnOrdering: props.enableColumnOrdering ?? false,
}),
[
props.columnFilters,
props.sorting,
props.rowSelection,
props.columnOrder,
props.columnVisibility,
props.pagination,
props.table,
props.filterFields,
props.columns,
props.enableColumnOrdering,
props.isLoading,
props.getFacetedUniqueValues,
props.getFacetedMinMaxValues,
]
)
return (
<DataTableContext.Provider value={value}>
<ControlsProvider>{children}</ControlsProvider>
</DataTableContext.Provider>
)
}
export function useDataTable<TData, TValue>() {
const context = useContext(DataTableContext)
if (!context) {
throw new Error('useDataTable must be used within a DataTableProvider')
}
return context as DataTableContextType<TData, TValue>
}
@@ -1,39 +0,0 @@
'use client'
import { X } from 'lucide-react'
import { Button } from 'ui'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
export function DataTableResetButton() {
const { table } = useDataTable()
useHotKey(table.resetColumnFilters, 'Escape')
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="text"
size="small"
onClick={() => table.resetColumnFilters()}
icon={<X className="h-4 w-4" />}
>
Reset
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>
Reset filters with{' '}
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
<span className="mr-1">⌘</span>
<span>Esc</span>
</Kbd>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
@@ -1,85 +0,0 @@
'use client'
import * as React from 'react'
import { cn } from 'ui'
import { Table } from '@tanstack/react-table'
import { DataTableSheetRowAction } from './data-table-sheet-row-action'
import { DataTableFilterField, SheetField } from '../types'
import { SheetDetailsContentSkeleton } from './data-table-sheet-skeleton'
interface DataTableSheetContentProps<TData, TMeta> extends React.HTMLAttributes<HTMLDListElement> {
data?: TData
table: Table<TData>
fields: SheetField<TData, TMeta>[]
filterFields: DataTableFilterField<TData>[]
// totalRows: number;
// filterRows: number;
// totalRowsFetched: number;
metadata?: TMeta & {
totalRows: number
filterRows: number
totalRowsFetched: number
}
}
export function DataTableSheetContent<TData, TMeta>({
data,
table,
className,
fields,
filterFields,
metadata,
...props
}: DataTableSheetContentProps<TData, TMeta>) {
if (!data) return <SheetDetailsContentSkeleton fields={fields} />
return (
<dl className={cn('divide-y', className)} {...props}>
{fields.map((field) => {
if (field.condition && !field.condition(data)) return null
const Component = field.component
const value = String(data[field.id])
return (
<div key={field.id.toString()}>
{field.type === 'readonly' ? (
<div
className={cn(
'flex gap-4 my-1 py-1 text-sm justify-between items-center w-full',
field.className
)}
>
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
<dd className="font-mono w-full text-right">
{Component ? <Component {...data} metadata={metadata} /> : value}
</dd>
</div>
) : (
<DataTableSheetRowAction
fieldValue={field.id}
filterFields={filterFields}
value={value}
table={table}
className={cn(
'flex gap-4 my-1 py-1 text-sm justify-between items-center w-full',
field.className
)}
>
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
<dd className="font-mono w-full text-right">
{Component ? <Component {...data} metadata={metadata} /> : value}
</dd>
</DataTableSheetRowAction>
)}
</div>
)
})}
</dl>
)
}
export const MemoizedDataTableSheetContent = React.memo(DataTableSheetContent, (prev, next) => {
// REMINDER: only check if data is the same, rest is useless
return prev.data === next.data
}) as typeof DataTableSheetContent
@@ -1,174 +0,0 @@
'use client'
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { ChevronDown, ChevronUp, X } from 'lucide-react'
import * as React from 'react'
import {
Button,
cn,
Separator,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
Skeleton,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from 'ui'
export interface DataTableSheetDetailsProps {
title?: React.ReactNode
titleClassName?: string
children?: React.ReactNode
}
export function DataTableSheetDetails({
title,
titleClassName,
children,
}: DataTableSheetDetailsProps) {
const { table, rowSelection, isLoading } = useDataTable()
const selectedRowKey = Object.keys(rowSelection)?.[0]
const selectedRow = React.useMemo(() => {
if (isLoading && !selectedRowKey) return
return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
}, [selectedRowKey, isLoading])
const index = table.getCoreRowModel().flatRows.findIndex((row) => row.id === selectedRow?.id)
const nextId = React.useMemo(
() => table.getCoreRowModel().flatRows[index + 1]?.id,
[index, isLoading]
)
const prevId = React.useMemo(
() => table.getCoreRowModel().flatRows[index - 1]?.id,
[index, isLoading]
)
const onPrev = React.useCallback(() => {
if (prevId) table.setRowSelection({ [prevId]: true })
}, [prevId, isLoading])
const onNext = React.useCallback(() => {
if (nextId) table.setRowSelection({ [nextId]: true })
}, [nextId, isLoading])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (!selectedRowKey) return
// REMINDER: prevent dropdown navigation inside of sheet to change row selection
const activeElement = document.activeElement
const isMenuActive = activeElement?.closest('[role="menu"]')
if (isMenuActive) return
if (e.key === 'ArrowUp') {
e.preventDefault()
onPrev()
}
if (e.key === 'ArrowDown') {
e.preventDefault()
onNext()
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [selectedRowKey, onNext, onPrev])
return (
// <Sheet
// open={!!selectedRowKey}
// onOpenChange={() => {
// // REMINDER: focus back to the row that was selected
// // We need to manually focus back due to missing Trigger component
// const el = selectedRowKey ? document.getElementById(selectedRowKey) : null
// table.resetRowSelection()
// // REMINDER: when navigating between tabs in the sheet and exit the sheet, the tab gets lost
// // We need a minimal delay to allow the sheet to close before focusing back to the row
// setTimeout(() => el?.focus(), 0)
// }}
// >
// <SheetContent
// // onCloseAutoFocus={(e) => e.preventDefault()}
// className="overflow-y-auto p-0 sm:max-w-md"
// hideClose
// >
// <SheetHeader className="sticky top-0 z-10 border-b bg-background p-4">
<div className="relative bg-sidebar">
<div className="flex items-center justify-between gap-2 px-5 py-1">
<h5 className={cn(titleClassName, 'truncate text-left')}>
{isLoading && !selectedRowKey ? <Skeleton className="h-7 w-36" /> : title}
</h5>
<div className="flex h-7 items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="tiny"
type="text"
disabled={!prevId}
onClick={onPrev}
className="px-0"
icon={<ChevronUp />}
>
<span className="sr-only">Previous</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
Navigate <Kbd variant="outline">↑</Kbd>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="tiny"
type="text"
disabled={!nextId}
onClick={onNext}
className="px-0"
icon={<ChevronDown />}
>
<span className="sr-only">Next</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
Navigate <Kbd variant="outline">↓</Kbd>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Separator orientation="vertical" className="mx-1" />
{/* <SheetClose autoFocus={true} asChild> */}
<Button
size="tiny"
type="text"
onClick={() => table.resetRowSelection()}
className="px-0"
icon={<X />}
>
<span className="sr-only">Close</span>
</Button>
{/* </SheetClose> */}
</div>
</div>
<Separator />
{children}
</div>
)
}
@@ -1,178 +0,0 @@
import { DropdownMenu } from 'ui'
import { Copy } from 'lucide-react'
import { DropdownMenuContent, DropdownMenuSeparator } from 'ui'
import { DropdownMenuGroup, DropdownMenuItem } from 'ui'
import { DropdownMenuTrigger } from 'ui'
import { CalendarSearch, ChevronLeft, ChevronRight, Equal, Search } from 'lucide-react'
import { CalendarDays } from 'lucide-react'
import { startOfDay } from 'date-fns'
import { startOfHour } from 'date-fns'
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
import { endOfDay } from 'date-fns'
import { Table } from '@tanstack/react-table'
import { CalendarClock } from 'lucide-react'
import { endOfHour } from 'date-fns'
import { cn } from 'ui'
import { DataTableFilterField } from '../types'
interface DataTableSheetRowActionProps<TData, TFields extends DataTableFilterField<TData>>
extends React.ComponentPropsWithRef<typeof DropdownMenuTrigger> {
fieldValue: TFields['value']
filterFields: TFields[]
value: string | number
table: Table<TData>
}
export function DataTableSheetRowAction<TData, TFields extends DataTableFilterField<TData>>({
fieldValue,
filterFields,
value,
children,
className,
table,
onKeyDown,
...props
}: DataTableSheetRowActionProps<TData, TFields>) {
const { copy, isCopied } = useCopyToClipboard()
const field = filterFields.find((field) => field.value === fieldValue)
const column = table.getColumn(fieldValue.toString())
if (!field || !column) return null
function renderOptions() {
if (!field) return null
switch (field.type) {
case 'checkbox':
return (
<DropdownMenuItem
onClick={() => {
// FIXME:
const filterValue = column?.getFilterValue() as undefined | Array<unknown>
const newValue = filterValue?.includes(value)
? filterValue
: [...(filterValue || []), value]
column?.setFilterValue(newValue)
}}
className="flex items-center gap-2"
>
<Search size={16} />
Include
</DropdownMenuItem>
)
case 'input':
return (
<DropdownMenuItem
onClick={() => column?.setFilterValue(value)}
className="flex items-center gap-2"
>
<Search size={16} />
Include
</DropdownMenuItem>
)
case 'slider':
return (
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => column?.setFilterValue([0, value])}
className="flex items-center gap-2"
>
{/* FIXME: change icon as it is not clear */}
<ChevronLeft size={16} />
Less or equal than
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column?.setFilterValue([value, 5000])}
className="flex items-center gap-2"
>
{/* FIXME: change icon as it is not clear */}
<ChevronRight size={16} />
Greater or equal than
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column?.setFilterValue([value])}
className="flex items-center gap-2"
>
<Equal size={16} />
Equal to
</DropdownMenuItem>
</DropdownMenuGroup>
)
case 'timerange':
const date = new Date(value)
return (
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => column?.setFilterValue([date])}
className="flex items-center gap-2"
>
<CalendarSearch size={16} />
Exact timestamp
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const start = startOfHour(date)
const end = endOfHour(date)
column?.setFilterValue([start, end])
}}
className="flex items-center gap-2"
>
<CalendarClock size={16} />
Same hour
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const start = startOfDay(date)
const end = endOfDay(date)
column?.setFilterValue([start, end])
}}
className="flex items-center gap-2"
>
<CalendarDays size={16} />
Same day
</DropdownMenuItem>
</DropdownMenuGroup>
)
default:
return null
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'relative',
className
)}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
// REMINDER: default behavior is to open the dropdown menu
// But because we use it to navigate between rows, we need to prevent it
// and only use "Enter" to select the option
e.preventDefault()
}
onKeyDown?.(e)
}}
{...props}
>
{children}
{isCopied ? (
<div className="absolute inset-0 bg-background/70 place-content-center">Value copied</div>
) : null}
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="left">
{renderOptions()}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => copy(String(value), { timeout: 1000 })}
className="flex items-center gap-2"
>
<Copy size={16} />
Copy value
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,27 +0,0 @@
import { Skeleton } from 'ui'
import { cn } from 'ui'
import { SheetField } from '../types'
interface SheetDetailsContentSkeletonProps<TData, TMeta> {
fields: SheetField<TData, TMeta>[]
}
export function SheetDetailsContentSkeleton<TData, TMeta>({
fields,
}: SheetDetailsContentSkeletonProps<TData, TMeta>) {
return (
<dl className="divide-y">
{fields.map((field) => (
<div
key={field.id.toString()}
className="flex gap-4 py-2 text-sm justify-between items-center"
>
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
<div>
<Skeleton className={cn('h-5 w-52', field.skeletonClassName)} />
</div>
</div>
))}
</dl>
)
}
@@ -1,62 +0,0 @@
import { Skeleton } from 'ui'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'components/interfaces/DataTableDemo/components/custom/table'
interface DataTableSkeletonProps {
/**
* Number of rows to render
* @default 10
*/
rows?: number
}
export function DataTableSkeleton({ rows = 10 }: DataTableSkeletonProps) {
return (
<Table>
<TableHeader className="bg-muted/50">
<TableRow className="hover:bg-transparent">
<TableHead>
<Skeleton className="my-1.5 h-4 w-24" />
</TableHead>
<TableHead className="hidden sm:table-cell">
<Skeleton className="my-1.5 h-4 w-32" />
</TableHead>
<TableHead className="hidden md:table-cell">
<Skeleton className="my-1.5 h-4 w-16" />
</TableHead>
<TableHead>
<Skeleton className="my-1.5 h-4 w-20" />
</TableHead>
<TableHead className="flex items-center justify-end" />
</TableRow>
</TableHeader>
<TableBody>
{new Array(rows).fill(0).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent">
<TableCell>
<Skeleton className="my-1.5 h-4 w-full max-w-[10rem]" />
</TableCell>
<TableCell className="hidden sm:table-cell">
<Skeleton className="my-1.5 h-4 w-full max-w-[13rem]" />
</TableCell>
<TableCell className="hidden md:table-cell">
<Skeleton className="my-1.5 h-4 w-24" />
</TableCell>
<TableCell>
<Skeleton className="my-1.5 h-4 w-full max-w-[10rem]" />
</TableCell>
<TableCell className="flex justify-end">
<Skeleton className="my-1.5 h-5 w-5" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
@@ -1,91 +0,0 @@
'use client'
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { Button } from 'ui'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
import { useControls } from 'components/interfaces/DataTableDemo/providers/controls'
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
import { useMemo } from 'react'
import { DataTableFilterControlsDrawer } from './data-table-filter-controls-drawer'
import { DataTableResetButton } from './data-table-reset-button'
import { DataTableViewOptions } from './data-table-view-options'
interface DataTableToolbarProps {
renderActions?: () => React.ReactNode
}
export function DataTableToolbar({ renderActions }: DataTableToolbarProps) {
const { table, isLoading, columnFilters } = useDataTable()
const { open, setOpen } = useControls()
useHotKey(() => setOpen((prev) => !prev), 'b')
const filters = table.getState().columnFilters
const rows = useMemo(
() => ({
total: table.getCoreRowModel().rows.length,
filtered: table.getFilteredRowModel().rows.length,
}),
[isLoading, columnFilters]
)
return (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="tiny"
type="outline"
icon={<PanelLeftClose className="h-4 w-4" />}
onClick={() => setOpen((prev) => !prev)}
className="hidden gap-2 sm:flex"
>
{open ? (
<>
<span className="hidden md:block">Hide Controls</span>
</>
) : (
<>
<span className="hidden md:block">Show Controls</span>
</>
)}
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>
Toggle controls with{' '}
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
<span className="mr-1">⌘</span>
<span>B</span>
</Kbd>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="block sm:hidden">
<DataTableFilterControlsDrawer />
</div>
<div>
<p className="hidden text-sm text-muted-foreground sm:block">
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span> of{' '}
<span className="font-mono font-medium">{formatCompactNumber(rows.total)}</span> row(s){' '}
<span className="sr-only sm:not-sr-only">filtered</span>
</p>
<p className="block text-sm text-muted-foreground sm:hidden">
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span>{' '}
row(s)
</p>
</div>
</div>
<div className="ml-auto flex items-center gap-2">
{filters.length ? <DataTableResetButton /> : null}
{renderActions?.()}
<DataTableViewOptions />
</div>
</div>
)
}
@@ -1,109 +0,0 @@
'use client'
import { Check, GripVertical, Settings2 } from 'lucide-react'
import { Button } from 'ui'
import {
Command_Shadcn_ as Command,
CommandEmpty_Shadcn_ as CommandEmpty,
CommandGroup_Shadcn_ as CommandGroup,
CommandInput_Shadcn_ as CommandInput,
CommandItem_Shadcn_ as CommandItem,
CommandList_Shadcn_ as CommandList,
} from 'ui'
import {
Popover_Shadcn_ as Popover,
PopoverContent_Shadcn_ as PopoverContent,
PopoverTrigger_Shadcn_ as PopoverTrigger,
} from 'ui'
import { useMemo, useState } from 'react'
import { cn } from 'ui'
import {
Sortable,
SortableDragHandle,
SortableItem,
} from 'components/interfaces/DataTableDemo/components/custom/sortable'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
export function DataTableViewOptions() {
const { table, enableColumnOrdering } = useDataTable()
const [open, setOpen] = useState(false)
const [drag, setDrag] = useState(false)
const [search, setSearch] = useState('')
const columnOrder = table.getState().columnOrder
const sortedColumns = useMemo(
() =>
table.getAllColumns().sort((a, b) => {
return columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id)
}),
[columnOrder]
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button type="outline" size="tiny" role="combobox" aria-expanded={open} className="h-9 w-9">
<Settings2 className="h-4 w-4" />
<span className="sr-only">View</span>
</Button>
</PopoverTrigger>
<PopoverContent side="bottom" align="end" className="w-[200px] p-0">
<Command>
<CommandInput value={search} onValueChange={setSearch} placeholder="Search options..." />
<CommandList>
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
<Sortable
value={sortedColumns.map((c) => ({ id: c.id }))}
onValueChange={(items) => table.setColumnOrder(items.map((c) => c.id))}
overlay={<div className="h-8 w-full rounded-md bg-muted/60" />}
onDragStart={() => setDrag(true)}
onDragEnd={() => setDrag(false)}
onDragCancel={() => setDrag(false)}
>
{sortedColumns
.filter(
(column) => typeof column.accessorFn !== 'undefined' && column.getCanHide()
)
.map((column) => (
<SortableItem key={column.id} value={column.id} asChild>
<CommandItem
value={column.id}
onSelect={() => column.toggleVisibility(!column.getIsVisible())}
className={'capitalize'}
disabled={drag}
>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
column.getIsVisible()
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<Check className={cn('h-4 w-4')} />
</div>
<span>{column.columnDef.meta?.label || column.id}</span>
{enableColumnOrdering && !search ? (
<SortableDragHandle
type="text"
size="tiny"
className="ml-auto size-5 text-muted-foreground hover:text-foreground focus:bg-muted focus:text-foreground"
>
<GripVertical className="size-4" aria-hidden="true" />
</SortableDragHandle>
) : null}
</CommandItem>
</SortableItem>
))}
</Sortable>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -1,91 +0,0 @@
import type { JSX } from "react";
export type SearchParams = {
[key: string]: string | string[] | undefined;
};
export type DatePreset = {
label: string;
from: Date;
to: Date;
shortcut: string;
};
// TODO: we could type the value(!) especially when using enums
export type Option = {
label: string;
value: string | boolean | number | undefined;
};
export type Input = {
type: "input";
options?: Option[];
};
export type Checkbox = {
type: "checkbox";
component?: (props: Option) => JSX.Element | null;
options?: Option[];
};
export type Slider = {
type: "slider";
min: number;
max: number;
// if options is undefined, we will provide all the steps between min and max
options?: Option[];
};
export type Timerange = {
type: "timerange";
options?: Option[]; // required for TS
presets?: DatePreset[];
};
export type Base<TData> = {
label: string;
value: keyof TData;
/**
* Defines if the accordion in the filter bar is open by default
*/
defaultOpen?: boolean;
/**
* Defines if the command input is disabled for this field
*/
commandDisabled?: boolean;
};
export type DataTableCheckboxFilterField<TData> = Base<TData> & Checkbox;
export type DataTableSliderFilterField<TData> = Base<TData> & Slider;
export type DataTableInputFilterField<TData> = Base<TData> & Input;
export type DataTableTimerangeFilterField<TData> = Base<TData> & Timerange;
export type DataTableFilterField<TData> =
| DataTableCheckboxFilterField<TData>
| DataTableSliderFilterField<TData>
| DataTableInputFilterField<TData>
| DataTableTimerangeFilterField<TData>;
/** ----------------------------------------- */
export type SheetField<TData, TMeta = Record<string, unknown>> = {
id: keyof TData;
label: string;
// FIXME: rethink that! I dont think we need this as there is no input type
// REMINDER: readonly if we only want to copy the value (e.g. uuid)
// TODO: we might have some values that are not in the data but can be computed
type: "readonly" | "input" | "checkbox" | "slider" | "timerange";
component?: (
// REMINDER: this is used to pass additional data like the `InfiniteQueryMeta`
props: TData & {
metadata?: {
totalRows: number;
filterRows: number;
totalRowsFetched: number;
} & TMeta;
}
) => JSX.Element | null | string;
condition?: (props: TData) => boolean;
className?: string;
skeletonClassName?: string;
};
@@ -1,66 +0,0 @@
// TODO: check if we can move to /data-table-filter-command/utils.ts
import type { ColumnFiltersState } from "@tanstack/react-table";
import { z } from "zod";
import type { DataTableFilterField } from "./types";
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
SLIDER_DELIMITER,
} from "@/lib/delimiters";
export function deserialize<T extends z.AnyZodObject>(schema: T) {
const castToSchema = z.preprocess((val) => {
if (typeof val !== "string") return val;
return val
.trim()
.split(" ")
.reduce((prev, curr) => {
const [name, value] = curr.split(":");
if (!value || !name) return prev;
prev[name] = value;
return prev;
}, {} as Record<string, unknown>);
}, schema);
return (value: string) => castToSchema.safeParse(value);
}
// export function serialize<T extends z.AnyZodObject>(schema: T) {
// return (value: z.infer<T>) =>
// schema
// .transform((val) => {
// Object.keys(val).reduce((prev, curr) => {
// if (Array.isArray(val[curr])) {
// return `${prev}${curr}:${val[curr].join(",")} `;
// }
// return `${prev}${curr}:${val[curr]} `;
// }, "");
// })
// .safeParse(value);
// }
export function serializeColumFilters<TData>(
columnFilters: ColumnFiltersState,
filterFields?: DataTableFilterField<TData>[]
) {
return columnFilters.reduce((prev, curr) => {
const { type, commandDisabled } = filterFields?.find(
(field) => curr.id === field.value
) || { commandDisabled: true }; // if column filter is not found, disable the command by default
if (commandDisabled) return prev;
if (Array.isArray(curr.value)) {
if (type === "slider") {
return `${prev}${curr.id}:${curr.value.join(SLIDER_DELIMITER)} `;
}
if (type === "checkbox") {
return `${prev}${curr.id}:${curr.value.join(ARRAY_DELIMITER)} `;
}
if (type === "timerange") {
return `${prev}${curr.id}:${curr.value.join(RANGE_DELIMITER)} `;
}
}
return `${prev}${curr.id}:${curr.value} `;
}, "");
}
@@ -1,16 +0,0 @@
export function Bluesky(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Bluesky</title>
<path
fill="currentColor"
d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
/>
</svg>
);
}
@@ -1,16 +0,0 @@
export function Github(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>GitHub</title>
<path
fill="currentColor"
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
);
}
@@ -1,16 +0,0 @@
export function X(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>X</title>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
);
}
@@ -1,35 +0,0 @@
import { ModeToggle } from '@/components/theme/toggle-mode'
import { Button } from 'ui'
import NextLink from 'next/link'
import { Link } from '@/components/custom/link'
import { Github } from '@/components/icons/github'
import { X } from '@/components/icons/x'
import { Bluesky } from '@/components/icons/bluesky'
export function SocialsFooter() {
return (
<div className="flex flex-col gap-1">
<div className="flex justify-center items-center gap-2 p-1">
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
<NextLink href="https://github.com/openstatusHQ/data-table-filters">
<Github className="h-4 w-4" />
</NextLink>
</Button>
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
<NextLink href="https://twitter.com/openstatusHQ">
<X className="h-4 w-4" />
</NextLink>
</Button>
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
<NextLink href="https://bsky.app/profile/openstatus.dev">
<Bluesky className="h-4 w-4" />
</NextLink>
</Button>
<ModeToggle className="[&>svg]:h-4 [&>svg]:w-4" />
</div>
<p className="text-muted-foreground text-center text-sm">
Powered by <Link href="https://openstatus.dev">OpenStatus</Link>
</p>
</div>
)
}
@@ -1,24 +0,0 @@
import { ArrowRight, ChevronRight } from "lucide-react";
import { Link } from "@/components/custom/link";
export function TopBanner() {
return (
<div className="group bg-muted/50 text-foreground border-b">
<div className="container mx-auto px-4 py-2 sm:px-16">
<div className="max-w-7xl mx-auto px-4 sm:px-8">
<p className="flex items-center justify-center gap-2">
<Link href="/infinite" className="no-underline">
<span className="font-semibold">New Example: </span>
<span className="underline underline-offset-4 decoration-muted-foreground group-hover:decoration-foreground">
Infinite Scroll
</span>{" "}
<span className="mr-1">Now Available, Inspired by Vercel</span>
<ArrowRight className="relative mb-[1px] inline h-4 w-0 transition-all group-hover:w-4" />
<ChevronRight className="relative mb-[1px] inline h-4 w-4 transition-all group-hover:w-0" />
</Link>
</p>
</div>
</div>
</div>
);
}
@@ -1,9 +0,0 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
@@ -1,39 +0,0 @@
'use client'
import * as React from 'react'
import { useTheme } from 'next-themes'
import { Button, ButtonProps } from 'ui'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import { Laptop, Moon, Sun } from 'lucide-react'
import { cn } from 'ui'
export function ModeToggle({ className, ...props }: ButtonProps) {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" size="small" className={cn('w-9 px-0', className)} {...props}>
<Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,54 +0,0 @@
'use client'
import * as React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import { cn } from 'ui'
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 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="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="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -1,33 +0,0 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from 'ui'
const badgeVariants = cva(
'inline-flex items-center rounded-full 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 hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground 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 }
@@ -1,49 +0,0 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from 'ui'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
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 }
@@ -1,59 +0,0 @@
'use client'
import * as React from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { DayPicker } from 'react-day-picker'
import { cn } from 'ui'
import { buttonVariants } from 'ui'
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-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
),
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 opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
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: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = 'Calendar'
export { Calendar }
@@ -1,79 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
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<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", 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, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -1,365 +0,0 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/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(
"flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.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 [&_.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(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background 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(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
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="font-mono font-medium tabular-nums text-foreground">
{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(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{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,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/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(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
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 }
@@ -1,155 +0,0 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[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) => (
<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(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground 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(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
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("-mx-1 h-px bg-border", 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(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
@@ -1,122 +0,0 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
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(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<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-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
@@ -1,118 +0,0 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/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(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{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-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
@@ -1,185 +0,0 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from 'ui'
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(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</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(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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}
/>
))
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(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 [&_svg]:pointer-events-none [&_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(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground 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(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground 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('-mx-1 my-1 h-px bg-muted', 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,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
@@ -1,29 +0,0 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/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(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }
@@ -1,25 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
@@ -1,26 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/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 }
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
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(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
@@ -1,160 +0,0 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/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(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 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(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground 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",
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("py-1.5 pl-8 pr-2 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(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground 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">
<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("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
@@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/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(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
@@ -1,124 +0,0 @@
'use client'
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from 'ui'
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(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
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}>
{children}
<SheetPrimitive.Close
autoFocus={true} // REMINDER: added
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</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-lg font-semibold text-foreground', 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-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
@@ -1,15 +0,0 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
@@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/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="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
@@ -1,31 +0,0 @@
"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 };
@@ -1,91 +0,0 @@
import * as React from 'react'
import { cn } from 'ui'
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('border-t bg-muted/50 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(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
)
)
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
))
TableCaption.displayName = 'TableCaption'
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
@@ -1,55 +0,0 @@
'use client'
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from 'ui'
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(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
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(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
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(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/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.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -1,6 +0,0 @@
import * as React from "react"
import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden"
const VisuallyHidden = VisuallyHiddenPrimitive.Root
export { VisuallyHidden }
@@ -1,41 +0,0 @@
import { DatePreset } from "@/components/data-table/types";
import { addDays, addHours, endOfDay, startOfDay } from "date-fns";
export const presets = [
{
label: "Today",
from: startOfDay(new Date()),
to: endOfDay(new Date()),
shortcut: "d", // day
},
{
label: "Yesterday",
from: startOfDay(addDays(new Date(), -1)),
to: endOfDay(addDays(new Date(), -1)),
shortcut: "y",
},
{
label: "Last hour",
from: addHours(new Date(), -1),
to: new Date(),
shortcut: "h",
},
{
label: "Last 7 days",
from: startOfDay(addDays(new Date(), -7)),
to: endOfDay(new Date()),
shortcut: "w",
},
{
label: "Last 14 days",
from: startOfDay(addDays(new Date(), -14)),
to: endOfDay(new Date()),
shortcut: "b", // bi-weekly
},
{
label: "Last 30 days",
from: startOfDay(addDays(new Date(), -30)),
to: endOfDay(new Date()),
shortcut: "m",
},
] satisfies DatePreset[];
@@ -1,4 +0,0 @@
export const LEVELS = ["success", "warning", "error"] as const;
// FIXME: improve
export const _LEVELS = [...LEVELS, "info"] as const;
@@ -1 +0,0 @@
export const METHODS = ["GET", "POST", "PUT", "DELETE"] as const;
@@ -1,49 +0,0 @@
export const REGIONS = ["ams", "fra", "gru", "hkg", "iad", "syd"] as const;
export const VERCEL_EDGE_REGIONS = [
"hnd1",
"sin1",
"cpt1",
"fra1",
"hkg1",
"syd1",
"gru1",
"dub1",
"sfo1",
"cdg1",
"icn1",
"kix1",
"iad1",
"arn1",
"bom1",
"lhr1",
"cle1",
] as const;
export const regions: Record<string, { label: string; flag: string }> = {
// REGIONS
ams: { label: "Amsterdam", flag: "🇳🇱" },
fra: { label: "Frankfurt", flag: "🇩🇪" },
gru: { label: "Sao Paulo", flag: "🇧🇷" },
hkg: { label: "Hong Kong", flag: "🇭🇰" },
iad: { label: "Washington D.C.", flag: "🇺🇸" },
syd: { label: "Sydney", flag: "🇦🇺" },
// VERCEL EDGE REGIONS
hnd1: { label: "Tokyo", flag: "🇯🇵" },
sin1: { label: "Singapore", flag: "🇸🇬" },
cpt1: { label: "Cape Town", flag: "🇿🇦" },
fra1: { label: "Paris", flag: "🇫🇷" },
hkg1: { label: "Hong Kong", flag: "🇭🇰" },
syd1: { label: "Sydney", flag: "🇦🇺" },
gru1: { label: "Sao Paulo", flag: "🇧🇷" },
dub1: { label: "Dublin", flag: "🇮🇪" },
sfo1: { label: "San Francisco", flag: "🇺🇸" },
cdg1: { label: "Paris", flag: "🇫🇷" },
icn1: { label: "Seoul", flag: "🇰🇷" },
kix1: { label: "Osaka", flag: "🇯🇵" },
iad1: { label: "Washington D.C.", flag: "🇺🇸" },
arn1: { label: "Stockholm", flag: "🇸🇪" },
bom1: { label: "Mumbai", flag: "🇮🇳" },
lhr1: { label: "London", flag: "🇬🇧" },
cle1: { label: "Cleveland", flag: "🇺🇸" },
};
@@ -1,24 +0,0 @@
export const TAGS = ["web", "api", "enterprise", "app"] as const;
export const tagColor: Record<string, Record<"badge" | "dot", string>> = {
api: {
badge:
"text-[#10b981] bg-[#10b981]/10 border-[#10b981]/20 hover:bg-[#10b981]/10",
dot: "bg-[#10b981]",
},
web: {
badge:
"text-[#0ea5e9] bg-[#0ea5e9]/10 border-[#0ea5e9]/20 hover:bg-[#0ea5e9]/10",
dot: "bg-[#0ea5e9]",
},
enterprise: {
badge:
"text-[#ec4899] bg-[#ec4899]/10 border-[#ec4899]/20 hover:bg-[#ec4899]/10",
dot: "bg-[#ec4899]",
},
app: {
badge:
"text-[#f97316] bg-[#f97316]/10 border-[#f97316]/20 hover:bg-[#f97316]/10",
dot: "bg-[#f97316]",
},
};
@@ -1,158 +0,0 @@
'use client'
import { DataTableColumnHeader } from '../components/data-table/data-table-column-header'
import { Badge } from '../components/ui/badge'
import { tagColor } from '../constants/tag'
import { isArrayOfDates, isArrayOfNumbers } from '../lib/is-array'
import type { ColumnDef } from '@tanstack/react-table'
import { format, isSameDay } from 'date-fns'
import { Check, Minus } from 'lucide-react'
import type { ColumnSchema } from 'components/interfaces/DataTableDemo/default/types'
export const columns: ColumnDef<ColumnSchema>[] = [
{
accessorKey: 'name',
header: 'Name',
enableHiding: false,
},
{
accessorKey: 'url',
header: 'URL',
cell: ({ row }) => {
const value = row.getValue('url')
return <div className="max-w-[200px] truncate">{`${value}`}</div>
},
},
{
accessorKey: 'regions',
header: 'Regions',
cell: ({ row }) => {
const value = row.getValue('regions')
if (Array.isArray(value)) {
return <div className="text-muted-foreground">{value.join(', ')}</div>
}
return <div className="text-muted-foreground">{`${value}`}</div>
},
filterFn: (row, id, value) => {
const array = row.getValue(id) as string[]
if (typeof value === 'string') return array.includes(value)
// up to the user to define either `.some` or `.every`
if (Array.isArray(value)) return value.some((i) => array.includes(i))
return false
},
},
{
accessorKey: 'tags',
header: 'Tags',
cell: ({ row }) => {
const value = row.getValue('tags') as string | string[]
if (Array.isArray(value)) {
return (
<div className="flex flex-wrap gap-1">
{value.map((v) => (
<Badge key={v} className={tagColor[v].badge}>
{v}
</Badge>
))}
</div>
)
}
return <Badge className={tagColor[value].badge}>{value}</Badge>
},
filterFn: (row, id, value) => {
const array = row.getValue(id) as string[]
if (typeof value === 'string') return array.includes(value)
// up to the user to define either `.some` or `.every`
if (Array.isArray(value)) return value.some((i) => array.includes(i))
return false
},
},
{
accessorKey: 'p95',
header: ({ column }) => <DataTableColumnHeader column={column} title="P95" />,
cell: ({ row }) => {
const value = row.getValue('p95')
if (typeof value === 'undefined') {
return <Minus className="h-4 w-4 text-muted-foreground/50" />
}
return (
<div>
<span className="font-mono">{`${value}`}</span> ms
</div>
)
},
filterFn: (row, id, value) => {
const rowValue = row.getValue(id) as number
if (typeof value === 'number') return value === Number(rowValue)
if (Array.isArray(value) && isArrayOfNumbers(value)) {
if (value.length === 1) {
return value[0] === rowValue
} else {
const sorted = value.sort((a, b) => a - b)
return sorted[0] <= rowValue && rowValue <= sorted[1]
}
}
return false
},
},
{
accessorKey: 'active',
header: 'Active',
cell: ({ row }) => {
const value = row.getValue('active')
if (value) return <Check className="h-4 w-4" />
return <Minus className="h-4 w-4 text-muted-foreground/50" />
},
filterFn: (row, id, value) => {
const rowValue = row.getValue(id)
if (typeof value === 'string') return value === String(rowValue)
if (typeof value === 'boolean') return value === rowValue
if (Array.isArray(value)) return value.includes(rowValue)
return false
},
},
{
accessorKey: 'public',
header: 'Public',
cell: ({ row }) => {
const value = row.getValue('public')
if (value) return <Check className="h-4 w-4" />
return <Minus className="h-4 w-4 text-muted-foreground/50" />
},
filterFn: (row, id, value) => {
const rowValue = row.getValue(id)
if (typeof value === 'string') return value === String(rowValue)
if (typeof value === 'boolean') return value === rowValue
if (Array.isArray(value)) return value.includes(rowValue)
return false
},
},
{
accessorKey: 'date',
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
cell: ({ row }) => {
const value = row.getValue('date')
return (
<div className="text-xs text-muted-foreground" suppressHydrationWarning>
{format(new Date(`${value}`), 'LLL dd, y HH:mm')}
</div>
)
},
filterFn: (row, id, value) => {
const rowValue = row.getValue(id)
if (value instanceof Date && rowValue instanceof Date) {
return isSameDay(value, rowValue)
}
if (Array.isArray(value)) {
if (isArrayOfDates(value) && rowValue instanceof Date) {
const sorted = value.sort((a, b) => a.getTime() - b.getTime())
// TODO: check length
return (
sorted[0]?.getTime() <= rowValue.getTime() && rowValue.getTime() <= sorted[1]?.getTime()
)
}
}
return false
},
},
]
@@ -1,69 +0,0 @@
'use client'
import type { DataTableFilterField, Option } from '../components/data-table/types'
import { REGIONS } from '../constants/region'
import { tagColor, TAGS } from '../constants/tag'
import { cn } from '../lib/utils'
import { data } from './data'
import type { ColumnSchema } from './types'
export const filterFields = [
{
label: 'Time Range',
value: 'date',
type: 'timerange',
defaultOpen: true,
commandDisabled: true,
},
{
label: 'URL',
value: 'url',
type: 'input',
options: data.map(({ url }) => ({ label: url, value: url })),
},
{
label: 'Public',
value: 'public',
type: 'checkbox',
options: [true, false].map((bool) => ({ label: `${bool}`, value: bool })),
},
{
label: 'Active',
value: 'active',
type: 'checkbox',
options: [true, false].map((bool) => ({ label: `${bool}`, value: bool })),
},
{
label: 'P95',
value: 'p95',
type: 'slider',
min: 0,
max: 3000,
options: data.map(({ p95 }) => ({ label: `${p95}`, value: p95 })),
defaultOpen: true,
},
{
label: 'Regions',
value: 'regions',
type: 'checkbox',
options: REGIONS.map((region) => ({ label: region, value: region })),
},
{
label: 'Tags',
value: 'tags',
type: 'checkbox',
defaultOpen: true,
// REMINDER: "use client" needs to be declared in the file - otherwise getting serialization error from Server Component
component: (props: Option) => {
if (typeof props.value === 'boolean') return null
if (typeof props.value === 'undefined') return null
return (
<div className="flex w-full items-center justify-between gap-2">
<span className="truncate font-normal">{props.value}</span>
<span className={cn('h-2 w-2 rounded-full', tagColor[props.value].dot)} />
</div>
)
},
options: TAGS.map((tag) => ({ label: tag, value: tag })),
},
] satisfies DataTableFilterField<ColumnSchema>[]
@@ -1,184 +0,0 @@
'use client'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../components/custom/table'
import { DataTableFilterCommand } from '../components/data-table/data-table-filter-command'
import { DataTableFilterControls } from '../components/data-table/data-table-filter-controls'
import { DataTablePagination } from '../components/data-table/data-table-pagination'
import { DataTableProvider } from '../components/data-table/data-table-provider'
import { DataTableToolbar } from '../components/data-table/data-table-toolbar'
import type { DataTableFilterField } from '../components/data-table/types'
import { useLocalStorage } from '../hooks/use-local-storage'
import { cn } from '../lib/utils'
import type {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
Table as TTable,
VisibilityState,
} from '@tanstack/react-table'
import {
flexRender,
getCoreRowModel,
getFacetedMinMaxValues,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import { useQueryStates } from 'nuqs'
import * as React from 'react'
import { searchParamsParser } from 'components/interfaces/DataTableDemo/default/search-params'
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
defaultColumnFilters?: ColumnFiltersState
// TODO: add sortingColumnFilters
filterFields?: DataTableFilterField<TData>[]
}
export function DataTable<TData, TValue>({
columns,
data,
defaultColumnFilters = [],
filterFields = [],
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(defaultColumnFilters)
const [sorting, setSorting] = React.useState<SortingState>([])
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>(
'data-table-visibility',
{}
)
const [_, setSearch] = useQueryStates(searchParamsParser)
const table = useReactTable({
data,
columns,
state: { columnFilters, sorting, columnVisibility, pagination },
onColumnVisibilityChange: setColumnVisibility,
onColumnFiltersChange: setColumnFilters,
onSortingChange: setSorting,
onPaginationChange: setPagination,
getSortedRowModel: getSortedRowModel(),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
// REMINDER: it doesn't support array of strings (WARNING: might not work for other types)
getFacetedUniqueValues: (table: TTable<TData>, columnId: string) => () => {
const facets = getFacetedUniqueValues<TData>()(table, columnId)()
const customFacets = new Map()
for (const [key, value] of facets as any) {
if (Array.isArray(key)) {
for (const k of key) {
const prevValue = customFacets.get(k) || 0
customFacets.set(k, prevValue + value)
}
} else {
const prevValue = customFacets.get(key) || 0
customFacets.set(key, prevValue + value)
}
}
return customFacets
},
})
React.useEffect(() => {
const columnFiltersWithNullable = filterFields.map((field) => {
const filterValue = columnFilters.find((filter) => filter.id === field.value)
if (!filterValue) return { id: field.value, value: null }
return { id: field.value, value: filterValue.value }
})
const search = columnFiltersWithNullable.reduce(
(prev, curr) => {
prev[curr.id as string] = curr.value
return prev
},
{} as Record<string, unknown>
)
setSearch(search)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnFilters])
return (
<DataTableProvider
table={table}
columns={columns}
filterFields={filterFields}
columnFilters={columnFilters}
sorting={sorting}
pagination={pagination}
>
<div className="flex h-full w-full flex-col gap-3 sm:flex-row">
<div
className={cn(
'hidden w-full p-1 sm:block sm:min-w-52 sm:max-w-52 sm:self-start md:min-w-64 md:max-w-64',
'group-data-[expanded=false]/controls:hidden'
)}
>
<DataTableFilterControls />
</div>
<div className="flex max-w-full flex-1 flex-col gap-4 overflow-hidden p-1">
<DataTableFilterCommand searchParamsParser={searchParamsParser} />
<DataTableToolbar />
<div className="rounded-md border">
<Table>
<TableHeader className="bg-muted/50">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-transparent">
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination />
</div>
</div>
</DataTableProvider>
)
}
@@ -1,215 +0,0 @@
import { subDays, subHours, subMinutes } from "date-fns";
import type { ColumnSchema } from "./types";
export const data = [
{
name: "Edge Api",
url: "edge-api.acme.com/health",
p95: 140,
public: true,
active: true,
regions: ["ams", "gru", "syd"],
tags: ["api", "enterprise"],
date: subHours(new Date(), 1),
},
{
name: "Lambda Api",
url: "lambda-api.acme.com/health",
p95: 203,
public: true,
active: true,
regions: ["ams", "gru", "syd"],
tags: ["api"],
date: subHours(new Date(), 10),
},
{
name: "Storybook",
url: "storybook.acme.com",
p95: 1252,
public: false,
active: true,
regions: ["iad"],
tags: ["web"],
date: subMinutes(new Date(), 10),
},
{
name: "Marketing",
url: "acme.com",
p95: 659,
public: true,
active: true,
regions: ["hkg", "fra", "iad"],
tags: ["web"],
date: subDays(new Date(), 1),
},
{
name: "App",
url: "app.acme.com",
p95: 1301,
public: false,
active: true,
regions: ["iad", "fra"],
tags: ["app"],
date: subHours(new Date(), 13),
},
{
name: "Demo",
url: "demo.acme.com",
p95: 2420,
public: true,
active: true,
regions: ["iad"],
tags: ["web", "enterprise"],
date: subDays(new Date(), 4),
},
{
name: "Documentation",
url: "docs.acme.com",
p95: 943,
public: true,
active: true,
regions: ["ams"],
tags: ["api", "web"],
date: subDays(new Date(), 6),
},
{
name: "Boilerplate",
url: "boilerplate.acme.com",
p95: undefined,
public: true,
active: false,
regions: ["gru", "fra"],
tags: ["web"],
date: subDays(new Date(), 10),
},
{
name: "Dashboard",
url: "app.acme.com/dashboard",
p95: 967,
public: false,
active: true,
regions: ["iad", "fra"],
tags: ["web"],
date: subHours(new Date(), 28),
},
{
name: "E2E Testing",
url: "staging-cypress-e2e.acme.com",
p95: 1954,
public: false,
active: true,
regions: ["iad"],
tags: ["web"],
date: subDays(new Date(), 12),
},
{
name: "Web App",
url: "web-app.acme.com",
p95: 1043,
public: true,
active: true,
regions: ["iad"],
tags: ["web"],
date: subDays(new Date(), 15),
},
{
name: "Admin Panel",
url: "admin.acme.com",
p95: 1342,
public: false,
active: true,
regions: ["gru", "syd"],
tags: ["web"],
date: subHours(new Date(), 5),
},
{
name: "API Gateway",
url: "api-gateway.acme.com/health",
p95: 190,
public: true,
active: true,
regions: ["ams", "hkg"],
tags: ["api", "enterprise"],
date: subHours(new Date(), 3),
},
{
name: "Analytics Service",
url: "analytics.acme.com",
p95: 810,
public: true,
active: true,
regions: ["iad", "fra", "hkg"],
tags: ["app", "enterprise"],
date: subDays(new Date(), 2),
},
{
name: "Support Portal",
url: "support.acme.com",
p95: 752,
public: true,
active: true,
regions: ["gru", "iad"],
tags: ["web"],
date: subMinutes(new Date(), 30),
},
{
name: "User Management",
url: "user-mgmt.acme.com",
p95: 980,
public: false,
active: true,
regions: ["gru", "syd", "fra"],
tags: ["app"],
date: subDays(new Date(), 7),
},
{
name: "Payment Gateway",
url: "payments.acme.com",
p95: 156,
public: true,
active: true,
regions: ["ams", "hkg", "syd"],
tags: ["api", "enterprise"],
date: subHours(new Date(), 8),
},
{
name: "Notification Service",
url: "notify.acme.com",
p95: 345,
public: false,
active: true,
regions: ["iad"],
tags: ["api"],
date: subDays(new Date(), 11),
},
{
name: "File Storage",
url: "storage.acme.com",
p95: 1220,
public: true,
active: true,
regions: ["gru", "hkg"],
tags: ["web", "enterprise"],
date: subDays(new Date(), 3),
},
{
name: "CDN",
url: "cdn.acme.com",
p95: 89,
public: true,
active: true,
regions: ["ams", "iad", "hkg"],
tags: ["web"],
date: subDays(new Date(), 2),
},
{
name: "Auth Service",
url: "auth.acme.com",
p95: 542,
public: false,
active: true,
regions: ["gru", "syd"],
tags: ["api"],
date: subHours(new Date(), 16),
},
] satisfies ColumnSchema[];
@@ -1,13 +0,0 @@
import * as React from 'react'
import { SocialsFooter } from '../components/layout/socials-footer'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<main className="container mx-auto flex min-h-screen flex-col gap-4 p-4 sm:p-8">
<div className="flex flex-col gap-4 sm:gap-8 w-full max-w-7xl mx-auto relative min-h-full h-full rounded-lg border border-border/50 bg-background/50 p-4 backdrop-blur-[2px] sm:p-8">
{children}
</div>
<SocialsFooter />
</main>
)
}
@@ -1,27 +0,0 @@
import * as React from 'react'
import { columns } from './columns'
import { filterFields } from './constants'
import { data } from './data'
import { DataTable } from './data-table'
import { useSearchParams } from './search-params'
import { Skeleton } from './skeleton'
export default function TableDemo() {
const [search] = useSearchParams()
return (
<React.Suspense fallback={<Skeleton />}>
<DataTable
columns={columns}
data={data}
filterFields={filterFields}
defaultColumnFilters={Object.entries(search)
.map(([key, value]) => ({
id: key,
value,
}))
.filter(({ value }) => value ?? undefined)}
/>
</React.Suspense>
)
}
@@ -1,41 +0,0 @@
import { REGIONS } from '../constants/region'
import { TAGS } from '../constants/tag'
import { ARRAY_DELIMITER, RANGE_DELIMITER, SLIDER_DELIMITER } from '../lib/delimiters'
import {
createParser,
parseAsArrayOf,
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
parseAsTimestamp,
useQueryState,
useQueryStates,
} from 'nuqs'
export const parseAsSort = createParser({
parse(queryValue) {
const [id, desc] = queryValue.split('.')
if (!id && !desc) return null
return { id, desc: desc === 'desc' }
},
serialize(value) {
return `${value.id}.${value.desc ? 'desc' : 'asc'}`
},
})
export const searchParamsParser = {
// FILTERS
url: parseAsString,
// TODO: limit to two array entries
p95: parseAsArrayOf(parseAsInteger, SLIDER_DELIMITER),
public: parseAsArrayOf(parseAsBoolean, ARRAY_DELIMITER),
active: parseAsArrayOf(parseAsBoolean, ARRAY_DELIMITER),
regions: parseAsArrayOf(parseAsStringLiteral(REGIONS), ARRAY_DELIMITER),
tags: parseAsArrayOf(parseAsStringLiteral(TAGS), ARRAY_DELIMITER),
date: parseAsArrayOf(parseAsTimestamp, RANGE_DELIMITER),
} as const
export function useSearchParams() {
return useQueryStates(searchParamsParser)
}
@@ -1,42 +0,0 @@
import { DataTableSkeleton } from '../components/data-table/data-table-skeleton'
import { Skeleton as DefaultSkeleton } from 'ui'
export function Skeleton() {
return (
<div className="flex w-full h-full flex-col gap-3 sm:flex-row">
<div className="w-full p-1 sm:min-w-52 sm:max-w-52 sm:self-start md:min-w-64 md:max-w-64">
<div className="-m-1 h-full p-1">
<div className="flex flex-col gap-4">
<div className="h-11 w-full flex items-center">
<DefaultSkeleton className="h-6 w-12" />
</div>
<div className="grid gap-2">
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
<DefaultSkeleton className="h-7 w-full" />
</div>
</div>
</div>
</div>
<div className="flex max-w-full flex-1 flex-col gap-4 p-1">
<DefaultSkeleton className="h-11 w-full border border-border" />
<div className="flex justify-between items-center h-9">
<DefaultSkeleton className="h-full max-w-36 w-full" />
<DefaultSkeleton className="h-full max-w-20 w-full" />
</div>
<div className="rounded-md border">
<DataTableSkeleton />
</div>
</div>
</div>
)
}
@@ -1,13 +0,0 @@
import { REGIONS } from '../constants/region'
import { TAGS } from '../constants/tag'
export type ColumnSchema = {
name: string
url: string
public: boolean
active: boolean
regions: (typeof REGIONS)[number][]
tags: (typeof TAGS)[number][]
date: Date
p95?: number | undefined
}
@@ -1,45 +0,0 @@
import { useCallback, useState } from "react";
import { toast } from "sonner";
export function useCopyToClipboard() {
const [text, setText] = useState<string | null>(null);
const copy = useCallback(
async (
text: string,
{ timeout, withToast }: { timeout?: number; withToast?: boolean } = {
timeout: 3000,
withToast: false,
}
) => {
if (!navigator?.clipboard) {
console.warn("Clipboard not supported");
return false;
}
try {
await navigator.clipboard.writeText(text);
setText(text);
if (timeout) {
setTimeout(() => {
setText(null);
}, timeout);
}
if (withToast) {
toast.success("Copied to clipboard");
}
return true;
} catch (error) {
console.warn("Copy failed", error);
setText(null);
return false;
}
},
[]
);
return { text, copy, isCopied: text !== null };
}
@@ -1,16 +0,0 @@
import * as React from "react";
// consider using https://github.com/xnimorz/use-debounce
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
@@ -1,17 +0,0 @@
import { useEffect } from "react";
export function useHotKey(callback: () => void, key: string): void {
useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === key && (e.metaKey || e.ctrlKey)) {
// e.preventDefault();
callback();
}
}
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}, [key]);
}
@@ -1,46 +0,0 @@
"use client";
import { useEffect, useState, useCallback } from "react";
function getItemFromLocalStorage(key: string) {
const item = window?.localStorage.getItem(key);
if (item) return JSON.parse(item);
return null;
}
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState(initialValue);
useEffect(() => {
// initialize
if (typeof window !== "undefined") {
const stored = getItemFromLocalStorage(key);
if (stored !== null) setStoredValue(stored);
}
}, [key]);
const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback(
(value) => {
if (value instanceof Function) {
setStoredValue((prev: T) => {
const newValue = value(prev);
// Save to localStorage
window.localStorage.setItem(key, JSON.stringify(newValue));
return newValue;
});
} else {
setStoredValue(value);
// Save to localStorage
window.localStorage.setItem(key, JSON.stringify(value));
}
return setStoredValue;
},
[key, setStoredValue]
);
return [storedValue, setValue];
}
@@ -1,19 +0,0 @@
import * as React from "react"
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
const result = matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)
return () => result.removeEventListener("change", onChange)
}, [query])
return value
}
@@ -1,9 +0,0 @@
### Non-standard components
All components within the `_components` folder are non-standard and are specific for the `/infinite` example.
The goal is to create a collection of components that can be used in any project. The components will be mostly used for very specific use cases either in the `filterFields[number].component` (see `constants.tsx`), `columns[0].cell` (see `columns.tsx`) or in the `DataTableSheetDetails` (`popover-percentile.tsx`).
The components are not part of the `@/components` folder because they are not generic enough to be used in other projects.
For more generic components, we use the `@/components/custom` and `@/components/data-table/{data-table-column}` folders.
@@ -1,26 +0,0 @@
import { format } from 'date-fns'
import { cn } from 'ui'
import { TimestampInfo } from 'ui-patterns'
interface HoverCardTimestampProps {
date: Date
className?: string
}
export function HoverCardTimestamp({ date, className }: HoverCardTimestampProps) {
return (
<div className="relative w-fit">
<TimestampInfo utcTimestamp={date.getTime()}>
<div className={cn('font-mono flex gap-1.5', className)}>
<span className="text-foreground/50 uppercase">{format(date, 'MMM')}</span>
<span className="text-foreground/50 uppercase"> {format(date, 'dd')} </span>
<span>
{format(date, 'HH:mm:ss')}
<span className="text-foreground/50">.{format(date, 'SS')}</span>
</span>
</div>
</TimestampInfo>
</div>
)
}
@@ -1,56 +0,0 @@
import {
Table,
TableBody,
TableCell,
TableRow,
} from "@/components/custom/table";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { Check } from "lucide-react";
import { Copy } from "lucide-react";
interface KeyValueTableProps {
data: Record<string, string>;
}
export function KeyValueTable({ data }: KeyValueTableProps) {
return (
<div className="mx-auto max-w-lg">
<div className="overflow-hidden rounded-lg border border-border bg-background">
<Table>
<TableBody>
{Object.entries(data).map(([key, value]) => {
return <RowAction key={key} label={key} value={value} />;
})}
</TableBody>
</Table>
</div>
</div>
);
}
function RowAction({ label, value }: { label: string; value: string }) {
const { copy, isCopied } = useCopyToClipboard();
return (
<TableRow
className="group *:border-border hover:bg-transparent [&>:not(:last-child)]:border-r text-left"
onClick={(e) => {
e.stopPropagation();
copy(value);
}}
>
<TableCell className="bg-muted/50 py-1 font-medium font-mono">
{label}
</TableCell>
<TableCell className="relative py-1 font-mono">
{value}
<div className="absolute top-1.5 right-1.5 invisible group-hover:visible backdrop-blur-sm bg-background p-0.5 rounded-sm border border-border">
{!isCopied ? (
<Copy className="h-3 w-3" />
) : (
<Check className="h-3 w-3" />
)}
</div>
</TableCell>
</TableRow>
);
}
@@ -1,75 +0,0 @@
'use client'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { Button } from 'ui'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import { cn } from 'ui'
import type { FetchPreviousPageOptions } from '@tanstack/react-query'
import { CirclePause, CirclePlay } from 'lucide-react'
import { useQueryStates } from 'nuqs'
import * as React from 'react'
import { searchParamsParser } from '../search-params'
const REFRESH_INTERVAL = 10_000
interface LiveButtonProps {
fetchPreviousPage?: (options?: FetchPreviousPageOptions | undefined) => Promise<unknown>
}
export function LiveButton({ fetchPreviousPage }: LiveButtonProps) {
const [{ live, date, sort }, setSearch] = useQueryStates(searchParamsParser)
const { table } = useDataTable()
useHotKey(handleClick, 'j')
React.useEffect(() => {
let timeoutId: NodeJS.Timeout
async function fetchData() {
if (live) {
await fetchPreviousPage?.()
timeoutId = setTimeout(fetchData, REFRESH_INTERVAL)
} else {
clearTimeout(timeoutId)
}
}
fetchData()
return () => {
clearTimeout(timeoutId)
}
}, [live, fetchPreviousPage])
// REMINDER: make sure to reset live when date is set
// TODO: test properly
React.useEffect(() => {
if ((date || sort) && live) {
setSearch((prev) => ({ ...prev, live: null }))
}
}, [date, sort])
function handleClick() {
setSearch((prev) => ({
...prev,
live: !prev.live,
date: null,
sort: null,
}))
table.getColumn('date')?.setFilterValue(undefined)
table.resetSorting()
}
return (
<Button
className={cn(live && 'border-info text-info hover:text-info')}
onClick={handleClick}
type={live ? 'primary' : 'default'}
size="small"
icon={
live ? <CirclePause className="mr-2 h-4 w-4" /> : <CirclePlay className="mr-2 h-4 w-4" />
}
>
Live
</Button>
)
}
@@ -1,19 +0,0 @@
import { TableCell, TableRow } from 'ui'
import { DataTableColumnLevelIndicator } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-level-indicator'
import { columns } from '../columns'
export function LiveRow() {
return (
<TableRow>
<TableCell className="w-[--header-level-size] min-w-[--header-level-size] max-w-[--header-level-size] border-b border-l border-r border-t border-info border-r-info/50">
<DataTableColumnLevelIndicator value="info" />
</TableCell>
<TableCell
colSpan={columns.length - 1}
className="border-b border-r border-t border-info font-medium text-info"
>
Live Mode
</TableCell>
</TableRow>
)
}
@@ -1,88 +0,0 @@
'use client'
import { ColumnSchema } from '../schema'
import { FunctionSquare } from 'lucide-react'
import * as React from 'react'
import { cn } from 'ui'
import {
formatCompactNumber,
formatMilliseconds,
} from 'components/interfaces/DataTableDemo/lib/format'
import {
Percentile,
getPercentileColor,
} from 'components/interfaces/DataTableDemo/lib/request/percentile'
import {
Popover_Shadcn_ as Popover,
PopoverContent_Shadcn_ as PopoverContent,
PopoverTrigger_Shadcn_ as PopoverTrigger,
} from 'ui'
interface PopoverPercentileProps {
data?: ColumnSchema
percentiles?: Record<Percentile, number>
filterRows: number
className?: string
}
export function PopoverPercentile({
data,
percentiles,
filterRows,
className,
}: PopoverPercentileProps) {
let percentileArray = percentiles
? Object.entries(percentiles).map(([percentile, latency]) => [parseInt(percentile), latency])
: []
data?.percentile ? percentileArray.push([data.percentile, data.latency]) : null
percentileArray.sort((a, b) => a[0] - b[0])
return (
<Popover>
<PopoverTrigger
className={cn(
'font-mono flex items-center gap-1 rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
>
<FunctionSquare
className={cn(
'h-4 w-4',
data?.percentile ? getPercentileColor(data.percentile).text : 'text-muted-foreground'
)}
/>
{!data?.percentile ? 'N/A' : `P${Math.round(data.percentile)}`}
</PopoverTrigger>
<PopoverContent className="w-40 flex flex-col gap-2 p-2 text-xs" align="end">
<p>
Calculated from filtered result of{' '}
<span className="font-medium font-mono">{formatCompactNumber(filterRows)}</span> rows.
</p>
<div className="flex flex-col gap-0.5">
{percentileArray.map(([key, value]) => {
const active = data?.percentile && data.percentile === key && value === data.latency
return (
<div
key={`${key}-${value}`}
className={cn(
'flex items-center justify-between px-1 py-0.5 rounded-md',
active && data.percentile
? `border ${getPercentileColor(data.percentile).border}`
: null
)}
>
<div
className={cn('font-mono', !active && 'text-muted-foreground')}
>{`P${Math.round(key)}`}</div>
<div className="font-mono">
{formatMilliseconds(Math.round(value))}
<span className="text-muted-foreground">ms</span>
</div>
</div>
)
})}
</div>
</PopoverContent>
</Popover>
)
}
@@ -1,27 +0,0 @@
import { Button } from 'ui'
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { LoaderCircle, RefreshCcw } from 'lucide-react'
interface RefreshButtonProps {
onClick: () => void
}
export function RefreshButton({ onClick }: RefreshButtonProps) {
const { isLoading } = useDataTable()
return (
<Button
type="outline"
size="small"
disabled={isLoading}
onClick={onClick}
className="h-9 w-9 px-0"
>
{isLoading ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<RefreshCcw className="h-4 w-4" />
)}
</Button>
)
}
@@ -1,44 +0,0 @@
import { TimingPhase, timingPhases } from 'components/interfaces/DataTableDemo/lib/request/timing'
import {
getTimingColor,
getTimingLabel,
getTimingPercentage,
} from 'components/interfaces/DataTableDemo/lib/request/timing'
import { cn } from 'ui'
import { formatMilliseconds } from 'components/interfaces/DataTableDemo/lib/format'
export function SheetTimingPhases({
latency,
timing,
className,
}: {
latency: number
timing: Record<TimingPhase, number>
className?: string
}) {
const timingPercentage = getTimingPercentage(timing, latency)
return (
<div className={cn('space-y-1 w-full text-left', className)}>
{timingPhases.map((phase) => (
<div key={phase} className="grid grid-cols-3 gap-2 text-xs justify-between items-center">
<div className="text-foreground uppercase truncate font-mono">
{getTimingLabel(phase)}
</div>
<div className="flex gap-2 col-span-2">
<div className="font-mono text-muted-foreground mr-8">{timingPercentage[phase]}</div>
<div className="flex flex-1 gap-2 items-center justify-end">
<div className="font-mono">
{formatMilliseconds(timing[phase])}
<span className="text-muted-foreground">ms</span>
</div>
</div>
<div
className={cn(getTimingColor(phase), 'h-4')}
style={{ width: `${(timing[phase] / latency) * 100}%` }}
/>
</div>
</div>
))}
</div>
)
}
@@ -1,111 +0,0 @@
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
import { Link } from 'components/interfaces/DataTableDemo/components/custom/link'
import { Bluesky } from 'components/interfaces/DataTableDemo/components/icons/bluesky'
import { Github } from 'components/interfaces/DataTableDemo/components/icons/github'
import { X } from 'components/interfaces/DataTableDemo/components/icons/x'
import { ModeToggle } from 'components/interfaces/DataTableDemo/components/theme/toggle-mode'
import { Button } from 'ui'
import {
Popover_Shadcn_ as Popover,
PopoverContent_Shadcn_ as PopoverContent,
PopoverTrigger_Shadcn_ as PopoverTrigger,
} from 'ui'
import { Book, Command } from 'lucide-react'
import NextLink from 'next/link'
export function SocialsFooter() {
return (
<div className="flex flex-col gap-2">
<div className="grid w-full grid-cols-3 items-center justify-center gap-2 p-1 md:grid-cols-6">
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
<NextLink href="https://github.com/openstatusHQ/data-table-filters">
<Github className="h-4 w-4" />
</NextLink>
</Button>
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
<NextLink href="https://twitter.com/openstatusHQ">
<X className="h-4 w-4" />
</NextLink>
</Button>
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
<NextLink href="https://bsky.app/profile/openstatus.dev">
<Bluesky className="h-4 w-4" />
</NextLink>
</Button>
<ModeToggle className="h-8 w-8 [&>svg]:h-4 [&>svg]:w-4" />
<Popover>
<PopoverTrigger asChild>
<Button type="text" size="small" className="h-8 w-8 px-0">
<Command className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto px-2 py-1">
<HotkeyOverview />
</PopoverContent>
</Popover>
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
<NextLink href="/guide">
<Book className="h-4 w-4" />
</NextLink>
</Button>
</div>
<p className="text-center text-xs text-muted-foreground">
Powered by{' '}
<Link href="https://openstatus.dev" hideArrow>
OpenStatus
</Link>
</p>
<p className="text-center text-[10px] text-muted-foreground">
The project is in active development. For feedback, please{' '}
<Link
href="https://github.com/openstatusHQ/data-table-filters/issues/new"
className="text-muted-foreground"
hideArrow
>
open an issue
</Link>{' '}
on GitHub.
</p>
</div>
)
}
const hotkeys = [
{ key: 'K', description: 'Toggle command input' },
{ key: 'B', description: 'Toggle sidebar controls' },
{
key: 'U',
description: 'Undo column state (order, visibility)',
},
{
key: 'J',
description: 'Toggle live mode',
},
{ key: 'Esc', description: 'Reset table filters' },
{
key: '.',
description: 'Reset element focus to start',
},
]
function HotkeyOverview() {
return (
<ul className="divide-y">
{hotkeys.map((props) => {
return (
<li key={props.key} className="grid grid-cols-4 gap-2 py-0.5">
<span className="col-span-1 text-left">
<Kbd className="ml-1">
<span className="mr-1">⌘</span>
<span>{props.key}</span>
</Kbd>
</span>
<span className="col-span-3 place-content-center text-xs text-muted-foreground">
{props.description}
</span>
</li>
)
})}
</ul>
)
}
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more