From 3e479b83d0848be72e81e8dc5d0253335d1c211d Mon Sep 17 00:00:00 2001 From: Hakan Shehu Date: Mon, 21 Oct 2024 09:58:04 +0200 Subject: [PATCH] Improve sidebar --- desktop/package-lock.json | 46 ++ desktop/package.json | 1 + .../components/accounts/account-logout.tsx | 2 +- .../components/accounts/email-login.tsx | 2 +- .../components/accounts/email-register.tsx | 2 +- .../components/avatars/avatar-upload.tsx | 2 +- .../node-collaborator-create.tsx | 2 +- .../node-collaborators-popover.tsx | 2 +- .../databases/view-create-dialog.tsx | 2 +- .../servers/server-create-dialog.tsx | 2 +- .../renderer/components/ui/collapsible.tsx | 9 + desktop/src/renderer/components/ui/sheet.tsx | 138 ++++ .../src/renderer/components/ui/sidebar.tsx | 771 ++++++++++++++++++ .../src/renderer/components/ui/skeleton.tsx | 17 + .../src/renderer/components/ui/toaster.tsx | 2 +- .../containers/container-header.tsx | 25 +- .../workspaces/sidebars/sidebar-chat-item.tsx | 11 +- .../workspaces/sidebars/sidebar-chats.tsx | 35 +- .../workspaces/sidebars/sidebar-footer.tsx | 81 ++ .../workspaces/sidebars/sidebar-header.tsx | 171 ++-- .../workspaces/sidebars/sidebar-item.tsx | 20 +- .../sidebars/sidebar-space-item.tsx | 197 +++-- .../workspaces/sidebars/sidebar-spaces.tsx | 23 +- .../workspaces/sidebars/sidebar.tsx | 36 +- .../workspaces/workspace-create.tsx | 2 +- .../components/workspaces/workspace-form.tsx | 2 +- .../workspaces/workspace-update.tsx | 2 +- .../workspaces/workspace-user-invite.tsx | 2 +- .../workspace-user-role-dropdown.tsx | 2 +- .../components/workspaces/workspace.tsx | 52 +- desktop/src/renderer/hooks/use-mobile.tsx | 21 + .../{components/ui => hooks}/use-toast.ts | 0 desktop/src/renderer/styles/index.css | 18 +- desktop/tailwind.config.js | 10 + 34 files changed, 1446 insertions(+), 264 deletions(-) create mode 100644 desktop/src/renderer/components/ui/collapsible.tsx create mode 100644 desktop/src/renderer/components/ui/sheet.tsx create mode 100644 desktop/src/renderer/components/ui/sidebar.tsx create mode 100644 desktop/src/renderer/components/ui/skeleton.tsx create mode 100644 desktop/src/renderer/components/workspaces/sidebars/sidebar-footer.tsx create mode 100644 desktop/src/renderer/hooks/use-mobile.tsx rename desktop/src/renderer/{components/ui => hooks}/use-toast.ts (100%) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 961b1670..9ac9532c 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^3.9.0", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", @@ -1791,6 +1792,51 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", + "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index 66c2d167..8e0b6652 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -54,6 +54,7 @@ "@hookform/resolvers": "^3.9.0", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", diff --git a/desktop/src/renderer/components/accounts/account-logout.tsx b/desktop/src/renderer/components/accounts/account-logout.tsx index cd7c2b3e..511191c0 100644 --- a/desktop/src/renderer/components/accounts/account-logout.tsx +++ b/desktop/src/renderer/components/accounts/account-logout.tsx @@ -11,7 +11,7 @@ import { import { Button } from '@/renderer/components/ui/button'; import { Spinner } from '@/renderer/components/ui/spinner'; import { useMutation } from '@/renderer/hooks/use-mutation'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; interface AccountLogoutProps { id: string; diff --git a/desktop/src/renderer/components/accounts/email-login.tsx b/desktop/src/renderer/components/accounts/email-login.tsx index 4bd3d263..14ca5dd0 100644 --- a/desktop/src/renderer/components/accounts/email-login.tsx +++ b/desktop/src/renderer/components/accounts/email-login.tsx @@ -12,7 +12,7 @@ import { import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { Icon } from '@/renderer/components/ui/icon'; import { Server } from '@/types/servers'; import { useMutation } from '@/renderer/hooks/use-mutation'; diff --git a/desktop/src/renderer/components/accounts/email-register.tsx b/desktop/src/renderer/components/accounts/email-register.tsx index 8a74a7ab..6f7c7e7c 100644 --- a/desktop/src/renderer/components/accounts/email-register.tsx +++ b/desktop/src/renderer/components/accounts/email-register.tsx @@ -12,7 +12,7 @@ import { import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { Icon } from '@/renderer/components/ui/icon'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { Server } from '@/types/servers'; diff --git a/desktop/src/renderer/components/avatars/avatar-upload.tsx b/desktop/src/renderer/components/avatars/avatar-upload.tsx index dc497969..24debd4b 100644 --- a/desktop/src/renderer/components/avatars/avatar-upload.tsx +++ b/desktop/src/renderer/components/avatars/avatar-upload.tsx @@ -3,7 +3,7 @@ import { Button } from '@/renderer/components/ui/button'; import { Spinner } from '@/renderer/components/ui/spinner'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { useAccount } from '@/renderer/contexts/account'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; interface AvatarUploadProps { onUpload: (id: string) => void; diff --git a/desktop/src/renderer/components/collaborators/node-collaborator-create.tsx b/desktop/src/renderer/components/collaborators/node-collaborator-create.tsx index 97708190..97de5958 100644 --- a/desktop/src/renderer/components/collaborators/node-collaborator-create.tsx +++ b/desktop/src/renderer/components/collaborators/node-collaborator-create.tsx @@ -5,7 +5,7 @@ import { Button } from '@/renderer/components/ui/button'; import { Spinner } from '@/renderer/components/ui/spinner'; import { NodeCollaboratorRoleDropdown } from '@/renderer/components/collaborators/node-collaborator-role-dropdown'; import { useMutation } from '@/renderer/hooks/use-mutation'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useWorkspace } from '@/renderer/contexts/workspace'; interface NodeCollaboratorCreate { diff --git a/desktop/src/renderer/components/collaborators/node-collaborators-popover.tsx b/desktop/src/renderer/components/collaborators/node-collaborators-popover.tsx index 96fdca70..976283a7 100644 --- a/desktop/src/renderer/components/collaborators/node-collaborators-popover.tsx +++ b/desktop/src/renderer/components/collaborators/node-collaborators-popover.tsx @@ -19,7 +19,7 @@ export const NodeCollaboratorsPopover = ({ diff --git a/desktop/src/renderer/components/databases/view-create-dialog.tsx b/desktop/src/renderer/components/databases/view-create-dialog.tsx index 7d3551ac..5b14d53e 100644 --- a/desktop/src/renderer/components/databases/view-create-dialog.tsx +++ b/desktop/src/renderer/components/databases/view-create-dialog.tsx @@ -25,7 +25,7 @@ import { cn } from '@/lib/utils'; import { Icon } from '@/renderer/components/ui/icon'; import { useDatabase } from '@/renderer/contexts/database'; import { FieldSelect } from '@/renderer/components/databases/fields/field-select'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { useWorkspace } from '@/renderer/contexts/workspace'; diff --git a/desktop/src/renderer/components/servers/server-create-dialog.tsx b/desktop/src/renderer/components/servers/server-create-dialog.tsx index b913278f..8748005c 100644 --- a/desktop/src/renderer/components/servers/server-create-dialog.tsx +++ b/desktop/src/renderer/components/servers/server-create-dialog.tsx @@ -11,7 +11,7 @@ import { Button } from '@/renderer/components/ui/button'; import { Label } from '@/renderer/components/ui/label'; import { Input } from '@/renderer/components/ui/input'; import { Spinner } from '@/renderer/components/ui/spinner'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useMutation } from '@/renderer/hooks/use-mutation'; interface ServerCreateDialogProps { diff --git a/desktop/src/renderer/components/ui/collapsible.tsx b/desktop/src/renderer/components/ui/collapsible.tsx new file mode 100644 index 00000000..9605c4e4 --- /dev/null +++ b/desktop/src/renderer/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/desktop/src/renderer/components/ui/sheet.tsx b/desktop/src/renderer/components/ui/sheet.tsx new file mode 100644 index 00000000..dee0c908 --- /dev/null +++ b/desktop/src/renderer/components/ui/sheet.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { Cross2Icon } from '@radix-ui/react-icons'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, 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=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out', + { + 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, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = 'right', className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = 'SheetHeader'; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = 'SheetFooter'; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/desktop/src/renderer/components/ui/sidebar.tsx b/desktop/src/renderer/components/ui/sidebar.tsx new file mode 100644 index 00000000..6a2465e8 --- /dev/null +++ b/desktop/src/renderer/components/ui/sidebar.tsx @@ -0,0 +1,771 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { VariantProps, cva } from 'class-variance-authority'; + +import { useIsMobile } from '@/renderer/hooks/use-mobile'; +import { cn } from '@/lib/utils'; +import { Button } from '@/renderer/components/ui/button'; +import { Input } from '@/renderer/components/ui/input'; +import { Separator } from '@/renderer/components/ui/separator'; +import { Sheet, SheetContent } from '@/renderer/components/ui/sheet'; +import { Skeleton } from '@/renderer/components/ui/skeleton'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/renderer/components/ui/tooltip'; +import { Icon } from '@/renderer/components/ui/icon'; + +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 SidebarContext = { + 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 Sidebar.'); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + 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)) => { + if (setOpenProp) { + return setOpenProp?.( + typeof value === 'function' ? value(open) : value, + ); + } + + _setOpen(value); + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; 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} +
+
+
+ ); + }, +); +SidebarProvider.displayName = 'SidebarProvider'; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + side?: 'left' | 'right'; + variant?: 'sidebar' | 'floating' | 'inset'; + collapsible?: 'offcanvas' | 'icon' | 'none'; + } +>( + ( + { + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props + }, + ref, + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); + }, +); +Sidebar.displayName = 'Sidebar'; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = 'SidebarTrigger'; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<'button'> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + - - -
- - + {workspace.name} + + ))} + + { + navigate('/create'); + }} + > + +

Create workspace

+
+ + + + ); }; diff --git a/desktop/src/renderer/components/workspaces/sidebars/sidebar-item.tsx b/desktop/src/renderer/components/workspaces/sidebars/sidebar-item.tsx index 47edce65..e5505879 100644 --- a/desktop/src/renderer/components/workspaces/sidebars/sidebar-item.tsx +++ b/desktop/src/renderer/components/workspaces/sidebars/sidebar-item.tsx @@ -16,17 +16,19 @@ export const SidebarItem = ({ node }: SidebarItemProps): React.ReactNode => { const directCount = 0; return ( -
{ - workspace.navigateToNode(node.id); - }} > - + { {node.name ?? 'Unnamed'} {directCount > 0 && ( - + {directCount} )} @@ -46,6 +48,6 @@ export const SidebarItem = ({ node }: SidebarItemProps): React.ReactNode => { className="mr-2 h-3 w-3 p-0.5 text-red-500" /> )} -
+ ); }; diff --git a/desktop/src/renderer/components/workspaces/sidebars/sidebar-space-item.tsx b/desktop/src/renderer/components/workspaces/sidebars/sidebar-space-item.tsx index 1592fa38..e71b9501 100644 --- a/desktop/src/renderer/components/workspaces/sidebars/sidebar-space-item.tsx +++ b/desktop/src/renderer/components/workspaces/sidebars/sidebar-space-item.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { cn } from '@/lib/utils'; import { Avatar } from '@/renderer/components/avatars/avatar'; import { Icon } from '@/renderer/components/ui/icon'; import { @@ -17,6 +16,20 @@ import { PageCreateDialog } from '@/renderer/components/pages/page-create-dialog import { DatabaseCreateDialog } from '@/renderer/components/databases/database-create-dialog'; import { SidebarSpaceNode } from '@/types/workspaces'; import { SidebarItem } from '@/renderer/components/workspaces/sidebars/sidebar-item'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/renderer/components/ui/collapsible'; +import { + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from '@/renderer/components/ui/sidebar'; +import { useWorkspace } from '@/renderer/contexts/workspace'; interface SettingsState { open: boolean; @@ -28,7 +41,8 @@ interface SidebarSpaceNodeProps { } export const SidebarSpaceItem = ({ node }: SidebarSpaceNodeProps) => { - const [isOpen, setIsOpen] = React.useState(true); + const workspace = useWorkspace(); + const [openCreatePage, setOpenCreatePage] = React.useState(false); const [openCreateChannel, setOpenCreateChannel] = React.useState(false); const [openCreateDatabase, setOpenCreateDatabase] = React.useState(false); @@ -41,88 +55,107 @@ export const SidebarSpaceItem = ({ node }: SidebarSpaceNodeProps) => { return ( -
- - setIsOpen(!isOpen)} - > - {node.name ?? 'Unnamed'} - - - - - - - - - {node.name ?? 'Unnamed'} - - setOpenCreatePage(true)}> -
- - Add page -
-
- setOpenCreateChannel(true)}> -
- - Add channel -
-
- setOpenCreateDatabase(true)}> -
- - Add database -
-
- setOpenCreateFolder(true)}> -
- - Add folder -
-
- - setSettingsState({ open: true })}> -
- - Settings -
-
- - setSettingsState({ - open: true, - tab: 'collaborators', - }) - } - > -
- - Add collaborators -
-
-
-
-
- {isOpen && ( -
- {node.children.map((child) => ( - - ))} -
- )} + + + + + + {node.name} + + + + + + + + + + {node.name ?? 'Unnamed'} + + setOpenCreatePage(true)}> +
+ + Add page +
+
+ setOpenCreateChannel(true)}> +
+ + Add channel +
+
+ setOpenCreateDatabase(true)}> +
+ + Add database +
+
+ setOpenCreateFolder(true)}> +
+ + Add folder +
+
+ + setSettingsState({ open: true })} + > +
+ + Settings +
+
+ + setSettingsState({ + open: true, + tab: 'collaborators', + }) + } + > +
+ + Add collaborators +
+
+
+
+ + + + {node.children.map((child) => ( + { + workspace.navigateToNode(child.id); + }} + > + + + + + ))} + + +
+ {openCreateChannel && ( { const workspace = useWorkspace(); const { data } = useQuery({ @@ -12,14 +19,14 @@ export const SidebarSpaces = () => { }); return ( -
-
- Spaces + + Spaces + -
-
+ + {data?.map((space) => )} -
-
+ + ); }; diff --git a/desktop/src/renderer/components/workspaces/sidebars/sidebar.tsx b/desktop/src/renderer/components/workspaces/sidebars/sidebar.tsx index fc053043..f2a1d798 100644 --- a/desktop/src/renderer/components/workspaces/sidebars/sidebar.tsx +++ b/desktop/src/renderer/components/workspaces/sidebars/sidebar.tsx @@ -1,25 +1,31 @@ import React from 'react'; +import { + Sidebar as SidebarWrapper, + SidebarContent as SidebarContentWrapper, + SidebarFooter as SidebarFooterWrapper, + SidebarHeader as SidebarHeaderWrapper, + SidebarRail as SidebarRailWrapper, +} from '@/renderer/components/ui/sidebar'; + import { SidebarHeader } from '@/renderer/components/workspaces/sidebars/sidebar-header'; import { SidebarSpaces } from '@/renderer/components/workspaces/sidebars/sidebar-spaces'; import { SidebarChats } from '@/renderer/components/workspaces/sidebars/sidebar-chats'; -import { Icon } from '@/renderer/components/ui/icon'; +import { SidebarFooter } from '@/renderer/components/workspaces/sidebars/sidebar-footer'; export const Sidebar = () => { return ( -
- -
-
- - Search -
-
- - Inbox -
- + + + + + -
-
+ + + + + + + ); }; diff --git a/desktop/src/renderer/components/workspaces/workspace-create.tsx b/desktop/src/renderer/components/workspaces/workspace-create.tsx index 8c220a09..b7424fed 100644 --- a/desktop/src/renderer/components/workspaces/workspace-create.tsx +++ b/desktop/src/renderer/components/workspaces/workspace-create.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { useAccount } from '@/renderer/contexts/account'; import { useNavigate } from 'react-router-dom'; diff --git a/desktop/src/renderer/components/workspaces/workspace-form.tsx b/desktop/src/renderer/components/workspaces/workspace-form.tsx index 0eabe105..3a3a61c4 100644 --- a/desktop/src/renderer/components/workspaces/workspace-form.tsx +++ b/desktop/src/renderer/components/workspaces/workspace-form.tsx @@ -14,7 +14,7 @@ import { Button } from '@/renderer/components/ui/button'; import { Spinner } from '@/renderer/components/ui/spinner'; import { Input } from '@/renderer/components/ui/input'; import { Textarea } from '@/renderer/components/ui/textarea'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { useAccount } from '@/renderer/contexts/account'; import { Avatar } from '@/renderer/components/avatars/avatar'; diff --git a/desktop/src/renderer/components/workspaces/workspace-update.tsx b/desktop/src/renderer/components/workspaces/workspace-update.tsx index 106025bd..57f1f4f0 100644 --- a/desktop/src/renderer/components/workspaces/workspace-update.tsx +++ b/desktop/src/renderer/components/workspaces/workspace-update.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { useWorkspace } from '@/renderer/contexts/workspace'; import { WorkspaceForm } from './workspace-form'; diff --git a/desktop/src/renderer/components/workspaces/workspace-user-invite.tsx b/desktop/src/renderer/components/workspaces/workspace-user-invite.tsx index 14d22f28..5a3f72a9 100644 --- a/desktop/src/renderer/components/workspaces/workspace-user-invite.tsx +++ b/desktop/src/renderer/components/workspaces/workspace-user-invite.tsx @@ -4,7 +4,7 @@ import { isValidEmail } from '@/lib/utils'; import { Button } from '@/renderer/components/ui/button'; import { useMutation } from '@/renderer/hooks/use-mutation'; import { Spinner } from '@/renderer/components/ui/spinner'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useWorkspace } from '@/renderer/contexts/workspace'; export const WorkspaceUserInvite = () => { diff --git a/desktop/src/renderer/components/workspaces/workspace-user-role-dropdown.tsx b/desktop/src/renderer/components/workspaces/workspace-user-role-dropdown.tsx index 38abea8a..8cea52fa 100644 --- a/desktop/src/renderer/components/workspaces/workspace-user-role-dropdown.tsx +++ b/desktop/src/renderer/components/workspaces/workspace-user-role-dropdown.tsx @@ -9,7 +9,7 @@ import { WorkspaceRole } from '@/types/workspaces'; import { Spinner } from '@/renderer/components/ui/spinner'; import { Icon } from '@/renderer/components/ui/icon'; import { useMutation } from '@/renderer/hooks/use-mutation'; -import { toast } from '@/renderer/components/ui/use-toast'; +import { toast } from '@/renderer/hooks/use-toast'; import { useWorkspace } from '@/renderer/contexts/workspace'; interface WorkspaceRoleItem { diff --git a/desktop/src/renderer/components/workspaces/workspace.tsx b/desktop/src/renderer/components/workspaces/workspace.tsx index 21476339..caf7ad91 100644 --- a/desktop/src/renderer/components/workspaces/workspace.tsx +++ b/desktop/src/renderer/components/workspaces/workspace.tsx @@ -10,6 +10,10 @@ import { import { useAccount } from '@/renderer/contexts/account'; import { Modal } from '@/renderer/components/workspaces/modals/modal'; import { WorkspaceSettingsDialog } from '@/renderer/components/workspaces/workspace-settings-dialog'; +import { + SidebarInset, + SidebarProvider, +} from '@/renderer/components/ui/sidebar'; export const Workspace = () => { const { workspaceId } = useParams<{ workspaceId: string }>(); @@ -45,32 +49,32 @@ export const Workspace = () => { }, }} > -
-
- -
-
- -
- {modal && ( - { - setSearchParams((prev) => { - prev.delete('modal'); - return prev; - }); - }} + + + +
+ +
+ {modal && ( + { + setSearchParams((prev) => { + prev.delete('modal'); + return prev; + }); + }} + /> + )} +
+ {openSettings && ( + )} -
- {openSettings && ( - - )} + ); }; diff --git a/desktop/src/renderer/hooks/use-mobile.tsx b/desktop/src/renderer/hooks/use-mobile.tsx new file mode 100644 index 00000000..82044326 --- /dev/null +++ b/desktop/src/renderer/hooks/use-mobile.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener('change', onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener('change', onChange); + }, []); + + return !!isMobile; +} diff --git a/desktop/src/renderer/components/ui/use-toast.ts b/desktop/src/renderer/hooks/use-toast.ts similarity index 100% rename from desktop/src/renderer/components/ui/use-toast.ts rename to desktop/src/renderer/hooks/use-toast.ts diff --git a/desktop/src/renderer/styles/index.css b/desktop/src/renderer/styles/index.css index 954becb8..b4a445ad 100644 --- a/desktop/src/renderer/styles/index.css +++ b/desktop/src/renderer/styles/index.css @@ -29,6 +29,14 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -56,6 +64,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } @@ -81,4 +97,4 @@ .cm-editor.cm-focused { outline: none; -} \ No newline at end of file +} diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js index ceda62b3..00da3ef9 100644 --- a/desktop/tailwind.config.js +++ b/desktop/tailwind.config.js @@ -51,6 +51,16 @@ module.exports = { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))', }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))', + }, }, borderRadius: { lg: 'var(--radius)',