+
+
,
+}
diff --git a/src/shared/composites/Drawer/Drawer.tsx b/src/shared/composites/Drawer/Drawer.tsx
new file mode 100644
index 0000000..5f8092d
--- /dev/null
+++ b/src/shared/composites/Drawer/Drawer.tsx
@@ -0,0 +1,302 @@
+import { useEffect, useMemo, useRef } from 'react'
+
+import { createPortal } from 'react-dom'
+
+import { XMarkIcon } from '@heroicons/react/24/outline'
+
+import { cva } from 'class-variance-authority'
+
+export interface DrawerProps {
+ isOpen: boolean
+
+ onClose: () => void
+
+ title?: string
+
+ children: React.ReactNode
+
+ footer?: React.ReactNode
+
+ side?: 'left' | 'right' | 'bottom'
+
+ size?: 'sm' | 'md' | 'lg'
+
+ isDismissable?: boolean
+
+ className?: string
+}
+
+const drawerStyles = cva(
+ `
+ fixed
+ z-50
+ bg-bg-primary
+ border-border-secondary
+ shadow-md
+ transition-transform
+ duration-300
+ flex
+ flex-col
+ `,
+ {
+ variants: {
+ side: {
+ left: `
+ left-0
+ top-0
+ h-screen
+ border-r
+ `,
+
+ right: `
+ right-0
+ top-0
+ h-screen
+ border-l
+ `,
+
+ bottom: `
+ bottom-0
+ left-0
+ w-full
+ border-t
+ rounded-t-xl
+ `,
+ },
+
+ size: {
+ sm: '',
+
+ md: '',
+
+ lg: '',
+ },
+
+ open: {
+ true: '',
+
+ false: '',
+ },
+ },
+
+ compoundVariants: [
+ // LEFT
+ {
+ side: 'left',
+ size: 'sm',
+ className: 'w-[320px]',
+ },
+
+ {
+ side: 'left',
+ size: 'md',
+ className: 'w-[480px]',
+ },
+
+ {
+ side: 'left',
+ size: 'lg',
+ className: 'w-[640px]',
+ },
+
+ // RIGHT
+ {
+ side: 'right',
+ size: 'sm',
+ className: 'w-[320px]',
+ },
+
+ {
+ side: 'right',
+ size: 'md',
+ className: 'w-[480px]',
+ },
+
+ {
+ side: 'right',
+ size: 'lg',
+ className: 'w-[640px]',
+ },
+
+ // BOTTOM
+ {
+ side: 'bottom',
+ size: 'sm',
+ className: 'h-[40vh]',
+ },
+
+ {
+ side: 'bottom',
+ size: 'md',
+ className: 'h-[60vh]',
+ },
+
+ {
+ side: 'bottom',
+ size: 'lg',
+ className: 'h-[90vh]',
+ },
+
+ // CLOSED STATES
+ {
+ side: 'left',
+ open: false,
+ className: '-translate-x-full',
+ },
+
+ {
+ side: 'right',
+ open: false,
+ className: 'translate-x-full',
+ },
+
+ {
+ side: 'bottom',
+ open: false,
+ className: 'translate-y-full',
+ },
+
+ // OPEN
+ {
+ open: true,
+ className: 'translate-x-0 translate-y-0',
+ },
+ ],
+
+ defaultVariants: {
+ side: 'right',
+
+ size: 'md',
+
+ open: false,
+ },
+ }
+)
+
+export function Drawer({
+ isOpen,
+
+ onClose,
+
+ title,
+
+ children,
+
+ footer,
+
+ side = 'right',
+
+ size = 'md',
+
+ isDismissable = true,
+
+ className = '',
+}: DrawerProps) {
+ const drawerRef = useRef(null)
+
+ const isMobile = window.innerWidth < 768
+
+ const resolvedSide = useMemo(() => {
+ if (isMobile && side === 'right') {
+ return 'bottom'
+ }
+
+ return side
+ }, [isMobile, side])
+
+ useEffect(() => {
+ if (!isOpen) return
+
+ document.body.style.overflow = 'hidden'
+
+ return () => {
+ document.body.style.overflow = ''
+ }
+ }, [isOpen])
+
+ useEffect(() => {
+ if (!isOpen || !isDismissable) {
+ return
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose()
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [isOpen, isDismissable, onClose])
+
+ useEffect(() => {
+ if (isOpen && drawerRef.current) {
+ drawerRef.current.focus()
+ }
+ }, [isOpen])
+
+ if (!isOpen) {
+ return null
+ }
+
+ return createPortal(
+ <>
+ {/* Overlay */}
+
+ }
+ isActive={item.isActive}
+ badge={collapsed ? undefined : item.badge}
+ badgeVariant={item.badgeVariant}
+ onClick={item.onClick}
+ className={
+ collapsed ? 'hover:bg-bg-tertiary justify-center px-0' : 'hover:bg-bg-tertiary'
+ }
+ />
+ )
+
+ if (collapsed) {
+ return (
+
+ {navItem}
+
+ )
+ }
+
+ return navItem
+ })}
+
+
+ )
+}
+
+export default SidebarNav
diff --git a/src/shared/composites/SidebarNav/index.ts b/src/shared/composites/SidebarNav/index.ts
new file mode 100644
index 0000000..7badc39
--- /dev/null
+++ b/src/shared/composites/SidebarNav/index.ts
@@ -0,0 +1,3 @@
+export { SidebarNav } from './SidebarNav'
+
+export type { SidebarNavProps, SidebarNavLink } from './SidebarNav'
diff --git a/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx b/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx
index 7023112..5c25db4 100644
--- a/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx
+++ b/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx
@@ -6,7 +6,7 @@ import { Badge } from '../../primitives/Badge'
const sidebarNavItem = cva(
[
'w-full',
- 'flex items-center justify-between',
+ 'flex items-center',
'gap-3',
'rounded-lg',
@@ -27,9 +27,14 @@ const sidebarNavItem = cva(
{
variants: {
isActive: {
- true: ['bg-bg-secondary', 'text-text-primary', 'font-medium'],
-
- false: ['text-text-tertiary', 'hover:bg-bg-secondary', 'hover:text-text-primary'],
+ true: ['bg-bg-tertiary', 'text-text-primary', 'font-medium'],
+
+ false: [
+ 'bg-bg-secondary',
+ 'text-text-tertiary',
+ 'hover:bg-bg-secondary',
+ 'hover:text-text-primary',
+ ],
},
},
@@ -53,23 +58,27 @@ export interface SidebarNavItemProps {
badgeVariant?: 'danger' | 'info'
className?: string
+
+ onClick?: () => void
}
export function SidebarNavItem({
- // link,
label,
icon,
isActive = false,
badge,
badgeVariant = 'info',
+ onClick,
className,
}: SidebarNavItemProps) {
return (
- , VariantProps {
children?: React.ReactNode
- dot?: boolean
+ notification?: boolean
}
-export function Badge({ children, variant, size, dot = false, className, ...props }: BadgeProps) {
- const dotSize = size === 'sm' ? 'w-1.5 h-1.5' : 'w-2 h-2'
-
+export function Badge({
+ children,
+ variant,
+ size,
+ notification = false,
+ className,
+ ...props
+}: BadgeProps) {
+ const formattedChildren = typeof children === 'number' && children > 99 ? '99+' : children
return (
- {dot ? : children}
+ {formattedChildren}
)
}
diff --git a/src/shared/primitives/Tooltip/Tooltip.tsx b/src/shared/primitives/Tooltip/Tooltip.tsx
index 888d126..48f919e 100644
--- a/src/shared/primitives/Tooltip/Tooltip.tsx
+++ b/src/shared/primitives/Tooltip/Tooltip.tsx
@@ -12,6 +12,8 @@ export interface TooltipProps {
delay?: number
className?: string
+
+ offset?: number
}
const arrowClasses = {
@@ -77,7 +79,7 @@ export function Tooltip({
position = 'top',
delay = 400,
-
+ offset = 10,
className = '',
}: TooltipProps) {
const [visible, setVisible] = useState(false)
@@ -97,7 +99,7 @@ export function Tooltip({
const rect = triggerRef.current.getBoundingClientRect()
- const spacing = 10
+ const spacing = offset
let top = 0
let left = 0
diff --git a/src/views/LoginPage.tsx b/src/views/LoginPage.tsx
index 0a5672c..fa22f7a 100644
--- a/src/views/LoginPage.tsx
+++ b/src/views/LoginPage.tsx
@@ -1,12 +1,129 @@
import LoginCard from '@/LoginCard'
-import React from 'react'
+
+import { SidebarNav } from '@/shared/composites/SidebarNav'
+
+import {
+ CheckIcon,
+ ExclamationTriangleIcon,
+ PresentationChartLineIcon,
+ QueueListIcon,
+ UsersIcon,
+} from '@heroicons/react/24/outline'
+
+import { useState } from 'react'
const LoginPage = () => {
- return (
-
-
-
- )
+ const [collapsed, setCollapsed] = useState(false)
+ const [activeItem, setActiveItem] = useState('Queue')
+ return (
+
diff --git a/src/shared/composites/Drawer/Drawer.stories.tsx b/src/shared/composites/Drawer/Drawer.stories.tsx
new file mode 100644
index 0000000..fa7a450
--- /dev/null
+++ b/src/shared/composites/Drawer/Drawer.stories.tsx
@@ -0,0 +1,84 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+
+import { useState } from 'react'
+
+import { Drawer } from '.'
+
+import { Button } from '@/shared/primitives/Button'
+
+const meta = {
+ title: 'Shared/Composites/Drawer',
+
+ component: Drawer,
+
+ tags: ['autodocs'],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+function DrawerDemo(props: Partial>) {
+ const [open, setOpen] = useState(false)
+
+ return (
+ <>
+
+
+ {
+ setOpen(false)
+ }}
+ title="Drawer Title"
+ {...props}
+ >
+
+ >
+ )
+}
+
+export const Right: Story = {
+ render: () => ,
+}
+
+export const Left: Story = {
+ render: () => ,
+}
+
+export const Bottom: Story = {
+ render: () => ,
+}
+
+export const WithFooter: Story = {
+ render: () => (
+
+
+
+ }
+ />
+ ),
+}
+
+export const NonDismissable: Story = {
+ render: () =>
+
+ Drawer content
+ +More content
+ +More content
+ +More content
+ +More content
+
{/* Left side */}
@@ -85,7 +94,7 @@ export function SidebarNavItem({
{badge}
)}
-
+
)
}
diff --git a/src/shared/primitives/Badge/Badge.tsx b/src/shared/primitives/Badge/Badge.tsx
index 941e5b8..ffa8228 100644
--- a/src/shared/primitives/Badge/Badge.tsx
+++ b/src/shared/primitives/Badge/Badge.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
const badge = cva(
- 'inline-flex items-center gap-1 font-medium rounded-full border transition-opacity',
+ 'inline-flex items-center justify-center gap-1 font-medium rounded-full border transition-opacity',
{
variants: {
variant: {
@@ -26,8 +26,26 @@ const badge = cva(
},
size: {
- sm: 'px-2 py-0.5 text-xs',
- md: 'px-3 py-1 text-sm',
+ xs: `
+ min-w-4
+ h-4
+ px-1
+ text-[10px]
+`,
+
+ sm: `
+ min-w-6
+ h-6
+ px-2
+ text-xs
+`,
+
+ md: `
+ min-w-7
+ h-7
+ px-3
+ text-sm
+`,
},
},
@@ -41,22 +59,32 @@ const badge = cva(
export interface BadgeProps
extends React.HTMLAttributes
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+
+
+
+ )
}
-export default LoginPage
\ No newline at end of file
+export default LoginPage