index.tsx316 lines · main
1import { ChevronRight, ChevronsDown, Loader2, Search } from 'lucide-react'
2import 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
6import { Collapsible as _RadixCollapsible, DropdownMenu as _RadixDropdownMenu } from 'radix-ui'
7import { ElementRef, forwardRef } from 'react'
8import {
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
26import { ShimmeringLoader } from '../ShimmeringLoader'
27
28const 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
44const 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
51const 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
74const 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
87const 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
93const 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
118const 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
149function 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
160const 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
168const 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
215const 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
250const InnerSideBarFilterSortDropdownItem = forwardRef<
251 ElementRef<typeof DropdownMenuRadioItem>,
252 React.ComponentPropsWithoutRef<typeof DropdownMenuRadioItem>
253>((props, ref) => {
254 return <DropdownMenuRadioItem ref={ref} {...props} />
255})
256
257const 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
270const 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
301export {
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}