From a4d0dc67127bd0999bc66d9d926d1299d634f634 Mon Sep 17 00:00:00 2001 From: andresgutgon Date: Tue, 23 Jul 2024 15:16:52 +0200 Subject: [PATCH] feat: wip --- packages/web-ui/package.json | 1 + packages/web-ui/src/ds/atoms/Button/index.tsx | 42 +++- .../atoms/DropdownMenu/Primitives/index.tsx | 201 ++++++++++++++++++ .../src/ds/atoms/DropdownMenu/index.tsx | 122 +++++++++++ packages/web-ui/src/ds/atoms/Icons/index.tsx | 9 + packages/web-ui/src/ds/tokens/colors.ts | 2 + .../sections/Document/Sidebar/Files/index.tsx | 75 ++++--- pnpm-lock.yaml | 94 ++++++++ 8 files changed, 507 insertions(+), 39 deletions(-) create mode 100644 packages/web-ui/src/ds/atoms/DropdownMenu/Primitives/index.tsx create mode 100644 packages/web-ui/src/ds/atoms/DropdownMenu/index.tsx diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 17292ad17..06a753531 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -30,6 +30,7 @@ "@monaco-editor/react": "^4.6.0", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", diff --git a/packages/web-ui/src/ds/atoms/Button/index.tsx b/packages/web-ui/src/ds/atoms/Button/index.tsx index 138858432..14e11ec42 100644 --- a/packages/web-ui/src/ds/atoms/Button/index.tsx +++ b/packages/web-ui/src/ds/atoms/Button/index.tsx @@ -2,6 +2,8 @@ import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react' import { cva, type VariantProps } from 'class-variance-authority' import { Slot, Slottable } from '@radix-ui/react-slot' +import { IconProps, Icons } from '$ui/ds/atoms/Icons' +import { colors } from '$ui/ds/tokens' import { cn } from '$ui/lib/utils' const buttonVariants = cva( @@ -21,11 +23,12 @@ const buttonVariants = cva( 'border border-input 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', + ghost: 'shadow-none bg-transparent', link: 'underline-offset-4 hover:underline text-primary', }, size: { default: 'py-1.5 px-3', + small: 'py-1 px-1.5', }, }, defaultVariants: { @@ -35,20 +38,24 @@ const buttonVariants = cva( }, ) -export interface ButtonProps - extends ButtonHTMLAttributes, - VariantProps { - children: ReactNode - fullWidth?: boolean - asChild?: boolean - isLoading?: boolean -} +export type ButtonProps = ButtonHTMLAttributes & + VariantProps & { + children: ReactNode + icon?: { + name: keyof typeof Icons + props?: IconProps + } + fullWidth?: boolean + asChild?: boolean + isLoading?: boolean + } const Button = forwardRef(function Button( { className, variant, size, + icon, fullWidth = false, asChild = false, isLoading, @@ -58,6 +65,8 @@ const Button = forwardRef(function Button( ref, ) { const Comp = asChild ? Slot : 'button' + const ButtonIcon = icon ? Icons[icon.name] : null + const iconProps = icon?.props ?? {} return ( (function Button( ref={ref} {...props} > - {children} + +
+ {ButtonIcon ? ( + + ) : null} + {children} +
+
) }) diff --git a/packages/web-ui/src/ds/atoms/DropdownMenu/Primitives/index.tsx b/packages/web-ui/src/ds/atoms/DropdownMenu/Primitives/index.tsx new file mode 100644 index 000000000..ae11bacbf --- /dev/null +++ b/packages/web-ui/src/ds/atoms/DropdownMenu/Primitives/index.tsx @@ -0,0 +1,201 @@ +'use client' + +import * as React from 'react' +import { Check, ChevronRight, Circle } from 'lucide-react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' + +import { cn } from '$ui/lib/utils' + +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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) + +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/packages/web-ui/src/ds/atoms/DropdownMenu/index.tsx b/packages/web-ui/src/ds/atoms/DropdownMenu/index.tsx new file mode 100644 index 000000000..471c9540d --- /dev/null +++ b/packages/web-ui/src/ds/atoms/DropdownMenu/index.tsx @@ -0,0 +1,122 @@ +import { ReactNode, useCallback, useState } from 'react' + +import { Button, type ButtonProps } from '$ui/ds/atoms/Button' +import Text from '$ui/ds/atoms/Text' + +import { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, + DropdownMenu as Root, +} from './Primitives' + +export type TriggerButtonProps = Omit & { + label?: string +} +const TriggerButton = ({ + label, + variant = 'ghost', + icon = { name: 'ellipsisVertical', props: { color: 'foregroundMuted' } }, + ...buttonProps +}: TriggerButtonProps) => { + return ( + + + + ) +} + +export type MenuOption = { + label: string + onClick: () => void + type?: 'normal' | 'destructive' + icon?: ReactNode + disabled?: boolean + shortcut?: string +} +function DropdownItem({ + icon, + onClick, + type = 'normal', + label, + shortcut, + disabled, +}: MenuOption) { + const onSelect = useCallback(() => { + if (disabled) return + onClick() + }, [disabled, onClick]) + return ( + + {icon} + + {label} + + {shortcut && {shortcut}} + + ) +} + +type RenderTriggerProps = { open: boolean } +type TriggerButtonPropsFn = (open: boolean) => TriggerButtonProps +type Props = { + triggerButtonProps?: TriggerButtonProps | TriggerButtonPropsFn + trigger?: (renderTriggerProps: RenderTriggerProps) => ReactNode + title?: string + options: MenuOption[] + onOpenChange?: (open: boolean) => void + controlledOpen?: boolean +} +export function DropdownMenu({ + triggerButtonProps, + trigger, + title, + options, + onOpenChange, + controlledOpen, +}: Props) { + const [open, setOpen] = useState(false) + const isFn = typeof triggerButtonProps === 'function' + return ( + { + onOpenChange?.(newOpen) + setOpen(newOpen) + }} + open={controlledOpen !== undefined ? controlledOpen : open} + > + {triggerButtonProps ? ( + + ) : trigger ? ( + trigger({ open }) + ) : ( + + )} + + + {title && ( + <> + {title} + + + )} + + {options.map((option, index) => ( + + ))} + + + + + ) +} diff --git a/packages/web-ui/src/ds/atoms/Icons/index.tsx b/packages/web-ui/src/ds/atoms/Icons/index.tsx index f7cc30831..9a69b33b3 100644 --- a/packages/web-ui/src/ds/atoms/Icons/index.tsx +++ b/packages/web-ui/src/ds/atoms/Icons/index.tsx @@ -2,15 +2,23 @@ import { ChevronDown, ChevronRight, Copy, + EllipsisVertical, File, FolderClosed, FolderOpen, type LucideIcon, } from 'lucide-react' +import { type TextColor } from '$ui/ds/tokens' + import { LatitudeLogo, LatitudeLogoMonochrome } from './custom-icons' export type Icon = LucideIcon +export type IconProps = { + color?: TextColor + widthClass?: string + heightClass?: string +} export const Icons = { logo: LatitudeLogo, @@ -21,4 +29,5 @@ export const Icons = { file: File, folderOpen: FolderOpen, clipboard: Copy, + ellipsisVertical: EllipsisVertical, } diff --git a/packages/web-ui/src/ds/tokens/colors.ts b/packages/web-ui/src/ds/tokens/colors.ts index 55fc2d808..6910002f9 100644 --- a/packages/web-ui/src/ds/tokens/colors.ts +++ b/packages/web-ui/src/ds/tokens/colors.ts @@ -7,6 +7,8 @@ export const colors = { foreground: 'text-foreground', foregroundMuted: 'text-muted-foreground', accent: 'text-accent', + destructive: 'text-destructive', + destructiveForeground: 'text-destructive-foreground', accentForeground: 'text-accent-foreground', }, borderColors: { diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx index 76f05689e..85fbc66ce 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx @@ -1,7 +1,8 @@ 'use client' -import { ReactNode, useCallback, useEffect, useState } from 'react' +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { DropdownMenu, MenuOption } from '$ui/ds/atoms/DropdownMenu' import { Icons } from '$ui/ds/atoms/Icons' import Text from '$ui/ds/atoms/Text' import { cn } from '$ui/lib/utils' @@ -59,7 +60,7 @@ function NodeHeaderWrapper({ return (
( + () => [ + { + label: 'Rename folder', + onClick: () => { }, + }, + { + label: 'New folder', + onClick: () => { }, + }, + { + label: 'New Prompt', + onClick: () => { }, + }, + { + label: 'Delete folder', + type: 'destructive', + onClick: () => { }, + }, + ], + [], + ) return ( -
- - - {node.name} - +
+
+ + + {node.name} + +
+ +
+ +
) } function NodeHeader({ - isLast, selected, node, open, indentation, navigateToDocument, }: { - isLast: boolean selected: boolean node: Node open: boolean @@ -172,18 +198,10 @@ function NodeHeader({ ) } - return ( - - ) + return } function FileNode({ - isLast = false, node, currentPath, indentation = [], @@ -191,7 +209,6 @@ function FileNode({ }: { node: Node currentPath: string | undefined - isLast?: boolean indentation?: IndentType[] navigateToDocument: (documentUuid: string) => void }) { @@ -205,7 +222,6 @@ function FileNode({ return (
@@ -254,6 +269,8 @@ export function FilesTree({ } }, [currentPath, togglePath]) + const onRenameFolder = useCallback(() => { }, []) + return (