From e57610a27c556e005b9b3b68115477ba1fdd3f1b Mon Sep 17 00:00:00 2001 From: mono Date: Fri, 16 Oct 2026 12:03:16 +0000 Subject: [PATCH] chore: complete clean Agent Desk source snapshot --- web/components/ui/menubar.tsx | 280 + web/components/ui/message-scroller.tsx | 131 + web/components/ui/message.tsx | 92 + web/components/ui/popover.tsx | 90 + web/components/ui/resizable.tsx | 50 + web/components/ui/scroll-area.tsx | 55 + web/components/ui/select.tsx | 201 + web/components/ui/separator.tsx | 25 + web/components/ui/sheet.tsx | 135 + web/components/ui/sidebar.tsx | 744 ++ web/components/ui/skeleton.tsx | 13 + web/components/ui/sonner.tsx | 49 + web/components/ui/switch.tsx | 32 + web/components/ui/table.tsx | 116 + web/components/ui/tabs.tsx | 82 + web/components/ui/textarea.tsx | 18 + web/components/ui/toggle-group.tsx | 89 + web/components/ui/toggle.tsx | 45 + web/components/ui/tooltip.tsx | 66 + web/components/workbench-header.tsx | 142 + web/components/workbench-rail.tsx | 86 + web/components/workspace-switcher.test.mjs | 39 + web/components/workspace-switcher.tsx | 233 + web/e2e/workbench-function.spec.ts | 127 + web/embed.go | 8 + web/embed_dev.go | 19 + web/embed_test.go | 20 + web/eslint.config.mjs | 18 + web/hooks/use-agent-conversation-realtime.ts | 286 + web/hooks/use-lg-media.ts | 25 + web/hooks/use-mobile.ts | 19 + web/i18n/config.test.mjs | 57 + web/i18n/config.ts | 51 + web/i18n/legal-messages.test.mjs | 91 + web/i18n/message-integrity.test.mjs | 174 + web/i18n/messages.ts | 53 + web/i18n/provider.tsx | 87 + web/lib/agent-activity-label.test.mjs | 107 + web/lib/agent-activity-label.ts | 88 + web/lib/agent-activity-state.test.mjs | 74 + web/lib/agent-activity-state.ts | 36 + web/lib/agent-conversation-realtime.test.mjs | 27 + web/lib/agent-conversation-realtime.ts | 18 + web/lib/api/admin.ts | 2654 +++++ web/lib/api/agent.ts | 277 + web/lib/api/auth.ts | 75 + web/lib/api/client.ts | 152 + web/lib/api/company.ts | 77 + web/lib/api/config.ts | 21 + web/lib/api/customer-contact.ts | 77 + web/lib/api/customer.ts | 108 + web/lib/api/dashboard.ts | 81 + web/lib/api/im.ts | 595 + web/lib/api/notification.ts | 82 + web/lib/api/organization.ts | 31 + web/lib/api/setup-readiness.ts | 134 + web/lib/api/support-community.ts | 176 + web/lib/api/support-config.ts | 19 + web/lib/api/support.ts | 90 + web/lib/api/ticket.ts | 239 + web/lib/api/types.ts | 5 + web/lib/api/websocket.ts | 16 + web/lib/auth.ts | 68 + web/lib/enums.ts | 15 + web/lib/fonts.ts | 16 + web/lib/generated/enums.ts | 391 + web/lib/im-editor-image.ts | 232 + web/lib/im-message-merge.ts | 177 + web/lib/im-message.ts | 153 + web/lib/im-realtime-state.ts | 172 + web/lib/im-realtime.ts | 40 + web/lib/knowledge-i18n.test.mjs | 56 + web/lib/knowledge-i18n.ts | 48 + web/lib/navigation-active.test.mjs | 92 + web/lib/navigation-active.ts | 40 + web/lib/navigation.tsx | 352 + web/lib/notification-i18n.test.mjs | 118 + web/lib/notification-i18n.ts | 117 + web/lib/operator-presence-state.test.mjs | 187 + web/lib/operator-presence-state.ts | 83 + web/lib/permission-i18n.test.mjs | 71 + web/lib/permission-i18n.ts | 223 + web/lib/realtime-connection.test.mjs | 99 + web/lib/realtime-connection.ts | 216 + web/lib/role-i18n.test.mjs | 54 + web/lib/role-i18n.ts | 27 + web/lib/sdk/agent-desk-sdk.test.mjs | 305 + web/lib/sdk/agent-desk-sdk.ts | 772 ++ web/lib/sdk/config-types.ts | 60 + web/lib/sdk/runtime-config.ts | 61 + web/lib/services/notification.ts | 42 + web/lib/sound-alert.test.mjs | 193 + web/lib/sound-alert.ts | 91 + web/lib/stores/agent-conversations.ts | 672 ++ web/lib/stores/support-chat.ts | 1007 ++ web/lib/support-article.ts | 13 + web/lib/support-conversation-i18n.test.mjs | 127 + web/lib/support-conversation-i18n.ts | 46 + web/lib/support-conversation-list.test.mjs | 269 + web/lib/support-conversation-list.ts | 199 + web/lib/support-host-bridge.ts | 110 + web/lib/support-slug.ts | 5 + web/lib/tag-tree.test.mjs | 115 + web/lib/tag-tree.ts | 126 + web/lib/utils.test.mjs | 31 + web/lib/utils.ts | 78 + web/messages/en-US.json | 3525 ++++++ web/messages/ru-RU.json | 3525 ++++++ web/messages/zh-CN.json | 3525 ++++++ web/next.config.ts | 51 + web/package.json | 106 + web/pnpm-lock.yaml | 10047 +++++++++++++++++ web/pnpm-workspace.yaml | 7 + web/postcss.config.mjs | 7 + web/public/images/login-illustration.svg | 89 + web/public/images/logo.jpg | Bin 0 -> 18574 bytes web/public/images/logo.png | Bin 0 -> 61669 bytes web/public/images/logo.svg | 41 + web/public/sdk/agent-desk-sdk.min.js | 1 + web/scripts/build-sdk.mjs | 47 + web/styles/main.scss | 30 + web/tsconfig.json | 34 + web/types/styles.d.ts | 2 + 123 files changed, 37513 insertions(+) create mode 100644 web/components/ui/menubar.tsx create mode 100644 web/components/ui/message-scroller.tsx create mode 100644 web/components/ui/message.tsx create mode 100644 web/components/ui/popover.tsx create mode 100644 web/components/ui/resizable.tsx create mode 100644 web/components/ui/scroll-area.tsx create mode 100644 web/components/ui/select.tsx create mode 100644 web/components/ui/separator.tsx create mode 100644 web/components/ui/sheet.tsx create mode 100644 web/components/ui/sidebar.tsx create mode 100644 web/components/ui/skeleton.tsx create mode 100644 web/components/ui/sonner.tsx create mode 100644 web/components/ui/switch.tsx create mode 100644 web/components/ui/table.tsx create mode 100644 web/components/ui/tabs.tsx create mode 100644 web/components/ui/textarea.tsx create mode 100644 web/components/ui/toggle-group.tsx create mode 100644 web/components/ui/toggle.tsx create mode 100644 web/components/ui/tooltip.tsx create mode 100644 web/components/workbench-header.tsx create mode 100644 web/components/workbench-rail.tsx create mode 100644 web/components/workspace-switcher.test.mjs create mode 100644 web/components/workspace-switcher.tsx create mode 100644 web/e2e/workbench-function.spec.ts create mode 100644 web/embed.go create mode 100644 web/embed_dev.go create mode 100644 web/embed_test.go create mode 100644 web/eslint.config.mjs create mode 100644 web/hooks/use-agent-conversation-realtime.ts create mode 100644 web/hooks/use-lg-media.ts create mode 100644 web/hooks/use-mobile.ts create mode 100644 web/i18n/config.test.mjs create mode 100644 web/i18n/config.ts create mode 100644 web/i18n/legal-messages.test.mjs create mode 100644 web/i18n/message-integrity.test.mjs create mode 100644 web/i18n/messages.ts create mode 100644 web/i18n/provider.tsx create mode 100644 web/lib/agent-activity-label.test.mjs create mode 100644 web/lib/agent-activity-label.ts create mode 100644 web/lib/agent-activity-state.test.mjs create mode 100644 web/lib/agent-activity-state.ts create mode 100644 web/lib/agent-conversation-realtime.test.mjs create mode 100644 web/lib/agent-conversation-realtime.ts create mode 100644 web/lib/api/admin.ts create mode 100644 web/lib/api/agent.ts create mode 100644 web/lib/api/auth.ts create mode 100644 web/lib/api/client.ts create mode 100644 web/lib/api/company.ts create mode 100644 web/lib/api/config.ts create mode 100644 web/lib/api/customer-contact.ts create mode 100644 web/lib/api/customer.ts create mode 100644 web/lib/api/dashboard.ts create mode 100644 web/lib/api/im.ts create mode 100644 web/lib/api/notification.ts create mode 100644 web/lib/api/organization.ts create mode 100644 web/lib/api/setup-readiness.ts create mode 100644 web/lib/api/support-community.ts create mode 100644 web/lib/api/support-config.ts create mode 100644 web/lib/api/support.ts create mode 100644 web/lib/api/ticket.ts create mode 100644 web/lib/api/types.ts create mode 100644 web/lib/api/websocket.ts create mode 100644 web/lib/auth.ts create mode 100644 web/lib/enums.ts create mode 100644 web/lib/fonts.ts create mode 100644 web/lib/generated/enums.ts create mode 100644 web/lib/im-editor-image.ts create mode 100644 web/lib/im-message-merge.ts create mode 100644 web/lib/im-message.ts create mode 100644 web/lib/im-realtime-state.ts create mode 100644 web/lib/im-realtime.ts create mode 100644 web/lib/knowledge-i18n.test.mjs create mode 100644 web/lib/knowledge-i18n.ts create mode 100644 web/lib/navigation-active.test.mjs create mode 100644 web/lib/navigation-active.ts create mode 100644 web/lib/navigation.tsx create mode 100644 web/lib/notification-i18n.test.mjs create mode 100644 web/lib/notification-i18n.ts create mode 100644 web/lib/operator-presence-state.test.mjs create mode 100644 web/lib/operator-presence-state.ts create mode 100644 web/lib/permission-i18n.test.mjs create mode 100644 web/lib/permission-i18n.ts create mode 100644 web/lib/realtime-connection.test.mjs create mode 100644 web/lib/realtime-connection.ts create mode 100644 web/lib/role-i18n.test.mjs create mode 100644 web/lib/role-i18n.ts create mode 100644 web/lib/sdk/agent-desk-sdk.test.mjs create mode 100644 web/lib/sdk/agent-desk-sdk.ts create mode 100644 web/lib/sdk/config-types.ts create mode 100644 web/lib/sdk/runtime-config.ts create mode 100644 web/lib/services/notification.ts create mode 100644 web/lib/sound-alert.test.mjs create mode 100644 web/lib/sound-alert.ts create mode 100644 web/lib/stores/agent-conversations.ts create mode 100644 web/lib/stores/support-chat.ts create mode 100644 web/lib/support-article.ts create mode 100644 web/lib/support-conversation-i18n.test.mjs create mode 100644 web/lib/support-conversation-i18n.ts create mode 100644 web/lib/support-conversation-list.test.mjs create mode 100644 web/lib/support-conversation-list.ts create mode 100644 web/lib/support-host-bridge.ts create mode 100644 web/lib/support-slug.ts create mode 100644 web/lib/tag-tree.test.mjs create mode 100644 web/lib/tag-tree.ts create mode 100644 web/lib/utils.test.mjs create mode 100644 web/lib/utils.ts create mode 100644 web/messages/en-US.json create mode 100644 web/messages/ru-RU.json create mode 100644 web/messages/zh-CN.json create mode 100644 web/next.config.ts create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/postcss.config.mjs create mode 100644 web/public/images/login-illustration.svg create mode 100644 web/public/images/logo.jpg create mode 100644 web/public/images/logo.png create mode 100644 web/public/images/logo.svg create mode 100644 web/public/sdk/agent-desk-sdk.min.js create mode 100644 web/scripts/build-sdk.mjs create mode 100644 web/styles/main.scss create mode 100644 web/tsconfig.json create mode 100644 web/types/styles.d.ts diff --git a/web/components/ui/menubar.tsx b/web/components/ui/menubar.tsx new file mode 100644 index 0000000..9738fb1 --- /dev/null +++ b/web/components/ui/menubar.tsx @@ -0,0 +1,280 @@ +"use client" + +import * as React from "react" +import { Menu as MenuPrimitive } from "@base-ui/react/menu" +import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar" + +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { CheckIcon } from "lucide-react" + +function Menubar({ className, ...props }: MenubarPrimitive.Props) { + return ( + + ) +} + +function MenubarMenu({ ...props }: React.ComponentProps) { + return +} + +function MenubarGroup({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarPortal({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarContent({ + className, + align = "start", + alignOffset = -4, + sideOffset = 8, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: MenuPrimitive.CheckboxItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function MenubarRadioGroup({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarRadioItem({ + className, + children, + inset, + ...props +}: MenuPrimitive.RadioItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function MenubarLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function MenubarSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarShortcut({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarSub({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarSubTrigger({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function MenubarSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Menubar, + MenubarPortal, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarGroup, + MenubarSeparator, + MenubarLabel, + MenubarItem, + MenubarShortcut, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSub, + MenubarSubTrigger, + MenubarSubContent, +} diff --git a/web/components/ui/message-scroller.tsx b/web/components/ui/message-scroller.tsx new file mode 100644 index 0000000..be2fb5b --- /dev/null +++ b/web/components/ui/message-scroller.tsx @@ -0,0 +1,131 @@ +"use client" + +import * as React from "react" +import { + MessageScroller as MessageScrollerPrimitive, + useMessageScroller, + useMessageScrollerScrollable, + useMessageScrollerVisibility, +} from "@shadcn/react/message-scroller" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ArrowDownIcon } from "lucide-react" + +function MessageScrollerProvider( + props: React.ComponentProps +) { + return +} + +function MessageScroller({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerViewport({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerItem({ + className, + scrollAnchor = false, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerButton({ + direction = "end", + className, + children, + render, + variant = "secondary", + size = "icon-sm", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + } + {...props} + > + {children ?? ( + <> + + + {direction === "end" ? "Scroll to end" : "Scroll to start"} + + + )} + + ) +} + +export { + MessageScrollerProvider, + MessageScroller, + MessageScrollerViewport, + MessageScrollerContent, + MessageScrollerItem, + MessageScrollerButton, + useMessageScroller, + useMessageScrollerScrollable, + useMessageScrollerVisibility, +} diff --git a/web/components/ui/message.tsx b/web/components/ui/message.tsx new file mode 100644 index 0000000..53437e8 --- /dev/null +++ b/web/components/ui/message.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function MessageGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function Message({ + className, + align = "start", + ...props +}: React.ComponentProps<"div"> & { align?: "start" | "end" }) { + return ( +
+ ) +} + +function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + MessageGroup, + Message, + MessageAvatar, + MessageContent, + MessageFooter, + MessageHeader, +} diff --git a/web/components/ui/popover.tsx b/web/components/ui/popover.tsx new file mode 100644 index 0000000..0b73c6b --- /dev/null +++ b/web/components/ui/popover.tsx @@ -0,0 +1,90 @@ +"use client" + +import * as React from "react" +import { Popover as PopoverPrimitive } from "@base-ui/react/popover" + +import { cn } from "@/lib/utils" + +function Popover({ ...props }: PopoverPrimitive.Root.Props) { + return +} + +function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { + return +} + +function PopoverContent({ + className, + align = "center", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + ...props +}: PopoverPrimitive.Popup.Props & + Pick< + PopoverPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { + return ( + + ) +} + +function PopoverDescription({ + className, + ...props +}: PopoverPrimitive.Description.Props) { + return ( + + ) +} + +export { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} diff --git a/web/components/ui/resizable.tsx b/web/components/ui/resizable.tsx new file mode 100644 index 0000000..c9e767a --- /dev/null +++ b/web/components/ui/resizable.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +function ResizablePanelGroup({ + className, + ...props +}: ResizablePrimitive.GroupProps) { + return ( + + ) +} + +function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) { + return +} + +function ResizableHandle({ + withHandle, + className, + ...props +}: ResizablePrimitive.SeparatorProps & { + withHandle?: boolean +}) { + return ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ )} + + ) +} + +export { ResizableHandle, ResizablePanel, ResizablePanelGroup } diff --git a/web/components/ui/scroll-area.tsx b/web/components/ui/scroll-area.tsx new file mode 100644 index 0000000..84c1e9f --- /dev/null +++ b/web/components/ui/scroll-area.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: ScrollAreaPrimitive.Root.Props) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: ScrollAreaPrimitive.Scrollbar.Props) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/web/components/ui/select.tsx b/web/components/ui/select.tsx new file mode 100644 index 0000000..e8021f5 --- /dev/null +++ b/web/components/ui/select.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import { Select as SelectPrimitive } from "@base-ui/react/select" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" + +const Select = SelectPrimitive.Root + +function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { + return ( + + ) +} + +function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { + return ( + + ) +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: SelectPrimitive.Trigger.Props & { + size?: "sm" | "default" +}) { + return ( + + {children} + + } + /> + + ) +} + +function SelectContent({ + className, + children, + side = "bottom", + sideOffset = 4, + align = "center", + alignOffset = 0, + alignItemWithTrigger = true, + ...props +}: SelectPrimitive.Popup.Props & + Pick< + SelectPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" + >) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.GroupLabel.Props) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.Item.Props) { + return ( + + + {children} + + + } + > + + + + ) +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.Separator.Props) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/web/components/ui/separator.tsx b/web/components/ui/separator.tsx new file mode 100644 index 0000000..6e1369e --- /dev/null +++ b/web/components/ui/separator.tsx @@ -0,0 +1,25 @@ +"use client" + +import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + ...props +}: SeparatorPrimitive.Props) { + return ( + + ) +} + +export { Separator } diff --git a/web/components/ui/sheet.tsx b/web/components/ui/sheet.tsx new file mode 100644 index 0000000..78c7fa3 --- /dev/null +++ b/web/components/ui/sheet.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import { Dialog as SheetPrimitive } from "@base-ui/react/dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Sheet({ ...props }: SheetPrimitive.Root.Props) { + return +} + +function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) { + return +} + +function SheetClose({ ...props }: SheetPrimitive.Close.Props) { + return +} + +function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) { + return +} + +function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: SheetPrimitive.Popup.Props & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + } + > + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: SheetPrimitive.Description.Props) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/web/components/ui/sidebar.tsx b/web/components/ui/sidebar.tsx new file mode 100644 index 0000000..475071d --- /dev/null +++ b/web/components/ui/sidebar.tsx @@ -0,0 +1,744 @@ +"use client" + +import * as React from "react" +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { PanelLeftIcon } from "lucide-react" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + +
+ {children} +
+
+ ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + dir, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( +