index.tsx316 lines · main
| 1 | import { ChevronRight, ChevronsDown, Loader2, Search } from 'lucide-react' |
| 2 | import Link from 'next/link' |
| 3 | // Required to avoid issues: |
| 4 | // The inferred type of InnerSideMenuCollapsible cannot be named without a reference to CollapsibleProps |
| 5 | // The inferred type of InnerSideBarFilterSortDropdown cannot be named without a reference to DropdownMenuProps |
| 6 | import { Collapsible as _RadixCollapsible, DropdownMenu as _RadixDropdownMenu } from 'radix-ui' |
| 7 | import { ElementRef, forwardRef } from 'react' |
| 8 | import { |
| 9 | cn, |
| 10 | Collapsible, |
| 11 | CollapsibleContent, |
| 12 | CollapsibleTrigger, |
| 13 | DropdownMenu, |
| 14 | DropdownMenuContent, |
| 15 | DropdownMenuRadioGroup, |
| 16 | DropdownMenuRadioItem, |
| 17 | DropdownMenuTrigger, |
| 18 | Input, |
| 19 | Skeleton, |
| 20 | Tooltip, |
| 21 | TooltipContent, |
| 22 | TooltipTrigger, |
| 23 | TreeViewItemVariant, |
| 24 | } from 'ui' |
| 25 | |
| 26 | import { ShimmeringLoader } from '../ShimmeringLoader' |
| 27 | |
| 28 | const InnerSideBarTitle = forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>( |
| 29 | (props, ref) => { |
| 30 | const { className, ...restProps } = props |
| 31 | return ( |
| 32 | <span |
| 33 | ref={ref} |
| 34 | {...restProps} |
| 35 | className={cn( |
| 36 | 'w-full flex gap-1 items-center group px-3 text-sm font-normal font-mono uppercase text-lighter tracking-wide group-hover:not-disabled:text-foreground', |
| 37 | className |
| 38 | )} |
| 39 | /> |
| 40 | ) |
| 41 | } |
| 42 | ) |
| 43 | |
| 44 | const InnerSideMenuCollapsible = forwardRef< |
| 45 | ElementRef<typeof Collapsible>, |
| 46 | React.ComponentPropsWithoutRef<typeof Collapsible> |
| 47 | >(({ ...props }, ref) => { |
| 48 | return <Collapsible ref={ref} {...props} className={cn('w-full px-2 group', props.className)} /> |
| 49 | }) |
| 50 | |
| 51 | const InnerSideMenuCollapsibleTrigger = forwardRef< |
| 52 | ElementRef<typeof CollapsibleTrigger>, |
| 53 | React.ComponentPropsWithoutRef<typeof CollapsibleTrigger> |
| 54 | >(({ ...props }, ref) => { |
| 55 | return ( |
| 56 | <CollapsibleTrigger |
| 57 | ref={ref} |
| 58 | {...props} |
| 59 | className={cn( |
| 60 | 'w-full flex gap-1 items-center group px-3 text-sm font-normal font-mono uppercase text-lighter tracking-wide', |
| 61 | props.className |
| 62 | )} |
| 63 | > |
| 64 | <ChevronRight |
| 65 | className="transition-all text-foreground-muted group-data-[state=open]:rotate-90" |
| 66 | size={16} |
| 67 | strokeWidth={1.5} |
| 68 | /> |
| 69 | <span className="group-hover:not-disabled:text-foreground">{props.title}</span> |
| 70 | </CollapsibleTrigger> |
| 71 | ) |
| 72 | }) |
| 73 | |
| 74 | const InnerSideMenuCollapsibleContent = forwardRef< |
| 75 | ElementRef<typeof CollapsibleContent>, |
| 76 | React.ComponentPropsWithoutRef<typeof CollapsibleContent> |
| 77 | >(({ ...props }, ref) => { |
| 78 | return ( |
| 79 | <CollapsibleContent |
| 80 | ref={ref} |
| 81 | {...props} |
| 82 | className={cn('w-full flex flex-col gap-0', props.className)} |
| 83 | /> |
| 84 | ) |
| 85 | }) |
| 86 | |
| 87 | const InnerSideMenuSeparator = forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>( |
| 88 | (props, ref) => { |
| 89 | return <div ref={ref} {...props} className={cn('h-px bg-border-muted', props.className)} /> |
| 90 | } |
| 91 | ) |
| 92 | |
| 93 | const InnerSideMenuItem = forwardRef< |
| 94 | ElementRef<typeof Link>, |
| 95 | React.ComponentPropsWithoutRef<typeof Link> & { |
| 96 | isActive?: boolean |
| 97 | forceHoverState?: boolean | null |
| 98 | } |
| 99 | >(({ className, isActive, forceHoverState, ...props }, ref) => { |
| 100 | return ( |
| 101 | <Link |
| 102 | ref={ref} |
| 103 | {...props} |
| 104 | aria-current={isActive} |
| 105 | className={cn( |
| 106 | 'text-sm', |
| 107 | 'h-7 pl-3 pr-2', |
| 108 | 'flex items-center justify-between rounded-md group relative', |
| 109 | isActive ? 'bg-selection' : 'hover:bg-surface-200', |
| 110 | forceHoverState && 'bg-surface-200', |
| 111 | isActive ? 'text-foreground' : 'text-foreground-light hover:text-foreground', |
| 112 | className |
| 113 | )} |
| 114 | /> |
| 115 | ) |
| 116 | }) |
| 117 | |
| 118 | const InnerSideMenuDataItem = forwardRef< |
| 119 | ElementRef<typeof Link>, |
| 120 | React.ComponentPropsWithoutRef<typeof Link> & { |
| 121 | isActive?: boolean |
| 122 | forceHoverState?: boolean | null |
| 123 | isPreview?: boolean |
| 124 | isOpened?: boolean |
| 125 | } |
| 126 | >(({ isActive = true, forceHoverState, isPreview, isOpened = true, ...props }, ref) => { |
| 127 | return ( |
| 128 | <Link |
| 129 | ref={ref} |
| 130 | {...props} |
| 131 | aria-current={isActive} |
| 132 | className={cn( |
| 133 | TreeViewItemVariant({ |
| 134 | isSelected: isActive && !isPreview, |
| 135 | isOpened: isOpened && !isPreview, |
| 136 | isPreview, |
| 137 | }), |
| 138 | 'px-4', |
| 139 | // forceHoverState && 'bg-surface-200', |
| 140 | props.className |
| 141 | )} |
| 142 | > |
| 143 | {!isPreview && isActive && <div className="absolute left-0 h-full w-0.5 bg-foreground" />} |
| 144 | {props.children} |
| 145 | </Link> |
| 146 | ) |
| 147 | }) |
| 148 | |
| 149 | function InnerSideMenuItemLoading({ |
| 150 | className, |
| 151 | ...props |
| 152 | }: React.ComponentPropsWithoutRef<typeof Skeleton>) { |
| 153 | return ( |
| 154 | <div className="py-0.5 h-7"> |
| 155 | <Skeleton {...props} className={cn('h-full w-full bg-surface-200', className)} /> |
| 156 | </div> |
| 157 | ) |
| 158 | } |
| 159 | |
| 160 | const InnerSideBarFilters = forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>( |
| 161 | (props, ref) => { |
| 162 | return ( |
| 163 | <div ref={ref} {...props} className={cn('flex px-2 gap-2 items-center', props.className)} /> |
| 164 | ) |
| 165 | } |
| 166 | ) |
| 167 | |
| 168 | const InnerSideBarFilterSearchInput = forwardRef< |
| 169 | HTMLInputElement, |
| 170 | React.ComponentPropsWithoutRef<typeof Input> & { |
| 171 | 'aria-labelledby': string |
| 172 | name: string |
| 173 | isLoading?: boolean |
| 174 | } |
| 175 | >(({ children, isLoading = false, ...props }, ref) => { |
| 176 | return ( |
| 177 | <label htmlFor={props.name} className="relative w-full"> |
| 178 | <span className="sr-only">{props['aria-labelledby']}</span> |
| 179 | <Input |
| 180 | ref={ref} |
| 181 | type="text" |
| 182 | className={cn( |
| 183 | 'h-[32px] md:h-[28px] w-full', |
| 184 | 'text-base md:text-xs', |
| 185 | 'pl-7', |
| 186 | 'pr-7', |
| 187 | 'w-full', |
| 188 | 'rounded-sm', |
| 189 | // 'bg-transparent', |
| 190 | // 'border', |
| 191 | // 'border-control', |
| 192 | props.className |
| 193 | )} |
| 194 | {...props} |
| 195 | /> |
| 196 | {children} |
| 197 | {isLoading ? ( |
| 198 | <Loader2 |
| 199 | className="animate-spin absolute left-2 text-foreground-muted" |
| 200 | style={{ top: 7 }} |
| 201 | size={14} |
| 202 | strokeWidth={1.5} |
| 203 | /> |
| 204 | ) : ( |
| 205 | <Search |
| 206 | className="absolute left-2 top-0 bottom-0 my-auto text-foreground-muted" |
| 207 | size={14} |
| 208 | strokeWidth={1.5} |
| 209 | /> |
| 210 | )} |
| 211 | </label> |
| 212 | ) |
| 213 | }) |
| 214 | |
| 215 | const InnerSideBarFilterSortDropdown = forwardRef< |
| 216 | ElementRef<typeof DropdownMenu>, |
| 217 | React.ComponentPropsWithoutRef<typeof DropdownMenu> & { |
| 218 | value: string |
| 219 | onValueChange: (value: string) => void |
| 220 | contentClassName?: string |
| 221 | triggerClassName?: string |
| 222 | } |
| 223 | >(({ value, onValueChange, contentClassName, triggerClassName, ...props }, _ref) => { |
| 224 | return ( |
| 225 | <DropdownMenu modal={false}> |
| 226 | <Tooltip delayDuration={0}> |
| 227 | <DropdownMenuTrigger |
| 228 | asChild |
| 229 | className={cn( |
| 230 | 'absolute right-1 top-[.4rem] md:top-[.3rem]', |
| 231 | 'text-foreground transition-colors hover:text-foreground data-[state=open]:text-foreground', |
| 232 | triggerClassName |
| 233 | )} |
| 234 | > |
| 235 | <TooltipTrigger> |
| 236 | <ChevronsDown size={18} strokeWidth={1} /> |
| 237 | </TooltipTrigger> |
| 238 | </DropdownMenuTrigger> |
| 239 | <TooltipContent side="bottom">Sort By</TooltipContent> |
| 240 | </Tooltip> |
| 241 | <DropdownMenuContent side="bottom" align="end" className={cn('w-48', contentClassName)}> |
| 242 | <DropdownMenuRadioGroup value={value} onValueChange={onValueChange}> |
| 243 | {props.children} |
| 244 | </DropdownMenuRadioGroup> |
| 245 | </DropdownMenuContent> |
| 246 | </DropdownMenu> |
| 247 | ) |
| 248 | }) |
| 249 | |
| 250 | const InnerSideBarFilterSortDropdownItem = forwardRef< |
| 251 | ElementRef<typeof DropdownMenuRadioItem>, |
| 252 | React.ComponentPropsWithoutRef<typeof DropdownMenuRadioItem> |
| 253 | >((props, ref) => { |
| 254 | return <DropdownMenuRadioItem ref={ref} {...props} /> |
| 255 | }) |
| 256 | |
| 257 | const InnerSideBarShimmeringLoaders = forwardRef< |
| 258 | HTMLDivElement, |
| 259 | React.ComponentPropsWithoutRef<'div'> |
| 260 | >((props, ref) => { |
| 261 | return ( |
| 262 | <div ref={ref} {...props} className={cn('flex flex-col px-2 gap-1 pb-4', props.className)}> |
| 263 | <ShimmeringLoader className="w-full h-7 rounded-md" delayIndex={0} /> |
| 264 | <ShimmeringLoader className="w-full h-7 rounded-md" delayIndex={1} /> |
| 265 | <ShimmeringLoader className="w-full h-7 rounded-md" delayIndex={2} /> |
| 266 | </div> |
| 267 | ) |
| 268 | }) |
| 269 | |
| 270 | const InnerSideBarEmptyPanel = forwardRef< |
| 271 | HTMLDivElement, |
| 272 | React.ComponentPropsWithoutRef<'div'> & { |
| 273 | title: string |
| 274 | description?: string | React.ReactNode |
| 275 | illustration?: React.ReactNode |
| 276 | actions?: React.ReactNode |
| 277 | } |
| 278 | >(({ illustration, title, description, actions, ...props }, ref) => { |
| 279 | return ( |
| 280 | <div |
| 281 | ref={ref} |
| 282 | {...props} |
| 283 | className={cn( |
| 284 | 'border border-muted bg-surface-100 dark:bg-surface-75 flex flex-col gap-y-3 items-center justify-center rounded-md px-5 py-4', |
| 285 | props.className |
| 286 | )} |
| 287 | > |
| 288 | <div className="w-full flex flex-col gap-y-1 items-center"> |
| 289 | {illustration} |
| 290 | {title && <p className="text-xs text-foreground-light">{title}</p>} |
| 291 | {description && ( |
| 292 | <p className="text-xs text-foreground-lighter text-center">{description}</p> |
| 293 | )} |
| 294 | {actions && <div className="mt-2">{actions}</div>} |
| 295 | {props.children} |
| 296 | </div> |
| 297 | </div> |
| 298 | ) |
| 299 | }) |
| 300 | |
| 301 | export { |
| 302 | InnerSideBarEmptyPanel, |
| 303 | InnerSideBarFilterSearchInput, |
| 304 | InnerSideBarFilterSortDropdown, |
| 305 | InnerSideBarFilterSortDropdownItem, |
| 306 | InnerSideBarFilters, |
| 307 | InnerSideBarShimmeringLoaders, |
| 308 | InnerSideBarTitle, |
| 309 | InnerSideMenuCollapsible, |
| 310 | InnerSideMenuCollapsibleContent, |
| 311 | InnerSideMenuCollapsibleTrigger, |
| 312 | InnerSideMenuDataItem, |
| 313 | InnerSideMenuItem, |
| 314 | InnerSideMenuItemLoading, |
| 315 | InnerSideMenuSeparator, |
| 316 | } |