From e6f896d002bb7d96f012323dcd756e1335428c0f Mon Sep 17 00:00:00 2001 From: Jakob Helgesson Date: Tue, 3 Dec 2024 13:07:37 +0100 Subject: [PATCH] Introduce Dropdown component --- web/src/components/Navbar.tsx | 48 ++-- web/src/components/ui/Dropdown.tsx | 350 +++++++++++++++++++++++++++ web/src/layouts/SimpleCenterPage.tsx | 11 +- web/src/routes/debug.lazy.tsx | 149 +++++++++++- 4 files changed, 512 insertions(+), 46 deletions(-) create mode 100644 web/src/components/ui/Dropdown.tsx diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index 2af447f..58cda23 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -1,11 +1,11 @@ /* eslint-disable jsx-a11y/alt-text */ /* eslint-disable no-undef */ -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { Link } from '@tanstack/react-router'; import { FileRoutesByPath } from '@tanstack/react-router'; import { useAuth } from '@/api/auth'; import { useApiMe } from '@/api/me'; +import * as DropdownMenu from '@/components/ui/Dropdown'; import { AvatarHolder, getInitials } from './UserProfile'; @@ -75,37 +75,23 @@ export const Navbar = () => { - - + + Sessions + + + Settings + + + + { + clearAuthToken(); + }} > - - - Settings - - - - - Sessions - - - { - clearAuthToken(); - }} - > - Logout - - - + Logout + + )} diff --git a/web/src/components/ui/Dropdown.tsx b/web/src/components/ui/Dropdown.tsx new file mode 100644 index 0000000..f9be038 --- /dev/null +++ b/web/src/components/ui/Dropdown.tsx @@ -0,0 +1,350 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { + ComponentPropsWithoutRef, + ElementRef, + forwardRef, + HTMLAttributes, +} from 'react'; +import { FaCheck, FaChevronRight, FaCircle } from 'react-icons/fa6'; + +import { cn } from '@/util/style'; + +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 = forwardRef< + ElementRef, + ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...properties }, reference) => ( + + {children} + + +)); + +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...properties }, reference) => ( + +)); + +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...properties }, reference) => ( + + + +)); + +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...properties }, reference) => ( + +)); + +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, checked, ...properties }, reference) => ( + + + + + + + {children} + +)); + +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, children, ...properties }, reference) => ( + + + + + + + {children} + +)); + +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = forwardRef< + ElementRef, + ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...properties }, reference) => ( + +)); + +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...properties }, reference) => ( + +)); + +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...properties +}: HTMLAttributes) => { + return ( + + ); +}; + +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export const Root = DropdownMenu; +export const CheckboxItem = DropdownMenuCheckboxItem; +export const Content = DropdownMenuContent; +export const Group = DropdownMenuGroup; +export const Item = DropdownMenuItem; +export const Label = DropdownMenuLabel; +export const RadioGroup = DropdownMenuRadioGroup; +export const RadioItem = DropdownMenuRadioItem; +export const Separator = DropdownMenuSeparator; +export const Shortcut = DropdownMenuShortcut; +export const Sub = DropdownMenuSub; +export const SubContent = DropdownMenuSubContent; +export const SubTrigger = DropdownMenuSubTrigger; +export const Trigger = DropdownMenuTrigger; + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/web/src/layouts/SimpleCenterPage.tsx b/web/src/layouts/SimpleCenterPage.tsx index 5e86190..b8cadf7 100644 --- a/web/src/layouts/SimpleCenterPage.tsx +++ b/web/src/layouts/SimpleCenterPage.tsx @@ -1,10 +1,13 @@ -import clsx from 'clsx'; +import type { ClassValue } from 'clsx'; import { FC, PropsWithChildren, ReactNode } from 'react'; +import { cn } from '@/util/style'; + export type SCPageProperties = PropsWithChildren<{ title: string; width?: 'xl' | '2xl' | '3xl' | '4xl'; suffix?: ReactNode; + className?: ClassValue; }>; export const SCPage: FC = ({ @@ -12,15 +15,17 @@ export const SCPage: FC = ({ title, width = '4xl', suffix, + className, }) => { return (
diff --git a/web/src/routes/debug.lazy.tsx b/web/src/routes/debug.lazy.tsx index 5964f34..26d0dcb 100644 --- a/web/src/routes/debug.lazy.tsx +++ b/web/src/routes/debug.lazy.tsx @@ -1,6 +1,6 @@ import { createLazyFileRoute } from '@tanstack/react-router'; import { cva, VariantProps } from 'class-variance-authority'; -import { ReactNode } from 'react'; +import { ReactNode, useState } from 'react'; import { FaGear } from 'react-icons/fa6'; import { @@ -19,6 +19,7 @@ import { buttonVariants, buttonVariantsConfig, } from '@/components/ui/Button'; +import * as Dropdown from '@/components/ui/Dropdown'; import { Input } from '@/components/ui/Input'; import { Label } from '@/components/ui/Label'; import { SCPage } from '@/layouts/SimpleCenterPage'; @@ -104,13 +105,21 @@ const DebugVariants = <
{displayName} {/*
*/} - {Object.entries(combinations).map(([variant, combos]) => { + {Object.entries(combinations).map(([variant, combos], index) => { return ( - <> - {variant} -
- {combos.map((combo) => ( -
+
+ + {variant} + +
+ {combos.map((combo, index) => ( +
{children({ variant, ...(combo as any), @@ -118,7 +127,7 @@ const DebugVariants = <
))}
- +
); })}
@@ -143,24 +152,49 @@ const VariantTitle = ({ children }: { children: ReactNode }) => { ); }; -const CustomComponentSection = ({ +const CustomComponentSection = < + TStates extends Record | undefined +>({ displayName, children, + states, }: { displayName: string; - children: ReactNode; + states?: TStates; + children: TStates extends undefined + ? ReactNode + : ( + _properties: { + [K in keyof TStates]: TStates[K]; + } & { + [SetK in keyof TStates & + string as `set${Capitalize}`]: ( + _value: TStates[SetK] + ) => void; + } + ) => ReactNode; }) => { + const stateObject: Record = {}; + + for (const [key, defaultValue] of Object.entries(states ?? {})) { + const [state, setState] = useState(defaultValue); + + stateObject[key] = state; + stateObject[`set${key.charAt(0).toUpperCase() + key.slice(1)}`] = + setState; + } + return (
{displayName} - {children} + {typeof children === 'function' ? children(stateObject) : children}
); }; const component = () => { return ( - + { + + + {(properties) => ( + <> + Default + + + + + + My Account + + Profile + Billing + Team + Subscription + + + + With Checkboxes + + + + + + Appearance + + + properties.setShowStatusBar(value) + } + > + Status Bar + + + properties.setShowActivityBar(value) + } + > + Activity Bar + + + properties.setShowPanel(value) + } + > + Panel + + + + + With Radio Group + + + + + + Panel Position + + + properties.setPosition(value) + } + > + + Top + + + Bottom + + + Right + + + + + + )} + ); };